From 9384df7927a1306633691801373442184e9ffa8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D0=BD=D0=B8=D0=BD=20=D0=A1=D1=82=D0=B5=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B8=D1=87?= Date: Wed, 20 May 2026 18:36:04 +0300 Subject: [PATCH 01/20] refactor(code structure): TopDownParser Split `TopDownParser` into separate files for better modularity and maintainability. Added implementations for expressions, declarations, and statements handling. --- .../Parser/Impl/TopDownParser.Declaration.cs | 231 +++++ .../Parser/Impl/TopDownParser.Expression.cs | 431 ++++++++++ .../Parser/Impl/TopDownParser.Statement.cs | 162 ++++ .../Parser/Impl/TopDownParser.cs | 801 +----------------- 4 files changed, 825 insertions(+), 800 deletions(-) create mode 100644 src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Declaration.cs create mode 100644 src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs create mode 100644 src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Statement.cs diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Declaration.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Declaration.cs new file mode 100644 index 00000000..792641a5 --- /dev/null +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Declaration.cs @@ -0,0 +1,231 @@ +using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes; +using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Declarations; +using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Declarations.AfterTypesAreLoaded; +using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Expressions; +using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Expressions.PrimaryExpressions; + +namespace HydraScript.Domain.FrontEnd.Parser.Impl; + +public partial class TopDownParser +{ + /// + /// Declaration -> LexicalDeclaration + /// FunctionDeclaration + /// TypeDeclaration + /// + private Declaration Declaration() + { + if (CurrentIsKeyword("function")) + { + return FunctionDeclaration(); + } + + if (CurrentIsKeyword("let") || CurrentIsKeyword("const")) + { + return LexicalDeclaration(); + } + + if (CurrentIsKeyword("type")) + { + return TypeDeclaration(); + } + + return null!; + } + + /// + /// FunctionDeclaration -> 'function' "Ident" '(' FunctionParameters? ')' Type? BlockStatement + /// + private FunctionDeclaration FunctionDeclaration() + { + Expect("Keyword", "function"); + var ident = Expect("Ident"); + + Expect("LeftParen"); + var args = new List(); + var indexOfFirstDefaultArgument = int.MaxValue; + while (CurrentIs("Ident")) + { + var arg = Expect("Ident").Value; + if (CurrentIs("Colon")) + { + Expect("Colon"); + var type = TypeValue(); + args.Add(new NamedArgument(arg, type)); + } + else if (CurrentIs("Assign")) + { + Expect("Assign"); + var value = LiteralNode(); + indexOfFirstDefaultArgument = args.Count < indexOfFirstDefaultArgument + ? args.Count + : indexOfFirstDefaultArgument; + args.Add(new DefaultValueArgument(arg, value)); + } + + if (!CurrentIs("RightParen")) + Expect("Comma"); + } + + var rp = Expect("RightParen"); + + TypeValue returnType = new TypeIdentValue( + TypeId: new IdentifierReference(name: "undefined") + { Segment = rp.Segment }); + + if (CurrentIs("Colon")) + { + Expect("Colon"); + returnType = TypeValue(); + } + + var name = new IdentifierReference(ident.Value) { Segment = ident.Segment }; + return new FunctionDeclaration(name, returnType, args, BlockStatement(), indexOfFirstDefaultArgument) + { Segment = ident.Segment }; + } + + /// + /// LexicalDeclaration -> LetOrConst "Ident" Initialization (',' "Ident" Initialization)* + /// + private LexicalDeclaration LexicalDeclaration() + { + var readOnly = CurrentIsKeyword("const"); + Expect("Keyword", readOnly ? "const" : "let"); + var declaration = new LexicalDeclaration(readOnly); + + AddToDeclaration(declaration); + + while (CurrentIs("Comma")) + { + Expect("Comma"); + AddToDeclaration(declaration); + } + + return declaration; + } + + /// + /// Initialization -> Typed | Initializer + /// Typed -> Type Initializer? + /// Initializer -> '=' Expression + /// + private void AddToDeclaration(LexicalDeclaration declaration) + { + var ident = Expect("Ident"); + var identRef = new IdentifierReference(ident.Value) { Segment = ident.Segment }; + var assignment = new AssignmentExpression( + new MemberExpression(identRef), + new ImplicitLiteral(TypeIdentValue.Undefined)) + { Segment = ident.Segment }; + + if (CurrentIs("Assign")) + { + var assignSegment = Expect("Assign").Segment; + var expression = Expression(); + assignment = new AssignmentExpression( + new MemberExpression(identRef), expression) { Segment = assignSegment }; + } + else if (CurrentIs("Colon")) + { + Expect("Colon"); + var type = TypeValue(); + if (CurrentIs("Assign")) + { + var assignSegment = Expect("Assign").Segment; + var expression = Expression(); + assignment = new AssignmentExpression( + new MemberExpression(identRef), + expression, type) { Segment = assignSegment }; + } + else + { + var expression = new ImplicitLiteral(type); + assignment = new AssignmentExpression( + lhs: new MemberExpression(identRef), + expression, + type); + } + } + + declaration.AddAssignment(assignment); + } + + /// + /// TypeDeclaration -> 'type' "Ident" = TypeValue + /// + private TypeDeclaration TypeDeclaration() + { + var typeWord = Expect("Keyword", "type"); + var ident = Expect("Ident"); + Expect("Assign"); + var type = TypeValue(); + + var typeId = new IdentifierReference(name: ident.Value) + { Segment = ident.Segment }; + + return new TypeDeclaration(typeId, type) { Segment = typeWord.Segment + ident.Segment }; + } + + /// + /// TypeValue -> TypeValueBase TypeValueSuffix* + /// + private TypeValue TypeValue() + { + if (CurrentIs("Ident")) + { + var ident = Expect("Ident"); + var identType = new TypeIdentValue( + TypeId: new IdentifierReference(name: ident.Value) + { Segment = ident.Segment }); + + return WithSuffix(identType); + } + + if (CurrentIs("LeftCurl")) + { + Expect("LeftCurl"); + var propertyTypes = new List(); + while (CurrentIs("Ident")) + { + var ident = Expect("Ident"); + Expect("Colon"); + var propType = TypeValue(); + propertyTypes.Add( + new PropertyTypeValue( + ident.Value, + propType)); + Expect("SemiColon"); + } + + Expect("RightCurl"); + + return WithSuffix(new ObjectTypeValue(propertyTypes)); + } + + return null!; + } + + /// + /// TypeValueSuffix -> '['']' | '?' + /// + private TypeValue WithSuffix(TypeValue baseType) + { + var type = baseType; + while (CurrentIs("LeftBracket") || CurrentIs("QuestionMark")) + { + if (CurrentIs("LeftBracket")) + { + Expect("LeftBracket"); + Expect("RightBracket"); + type = new ArrayTypeValue(type); + } + else if (CurrentIs("QuestionMark")) + { + Expect("QuestionMark"); + type = new NullableTypeValue(type); + } + } + + return type; + } +} \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs new file mode 100644 index 00000000..eab10f9d --- /dev/null +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs @@ -0,0 +1,431 @@ +using System.Globalization; +using System.Text.RegularExpressions; +using HydraScript.Domain.FrontEnd.Lexer; +using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Expressions; +using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Expressions.AccessExpressions; +using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Expressions.ComplexLiterals; +using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Expressions.PrimaryExpressions; + +namespace HydraScript.Domain.FrontEnd.Parser.Impl; + +public partial class TopDownParser +{ + /// + /// Expression -> CastExpression | AssignmentExpression + /// + private Expression Expression() + { + var expr = CastExpression(); + if (expr is LeftHandSideExpression lhs && CurrentIs("Assign")) + { + var assign = Expect("Assign"); + return new AssignmentExpression(lhs, Expression()) + { Segment = assign.Segment }; + } + + return expr; + } + + /// + /// CallExpression -> MemberExpression Arguments (Arguments | '[' Expression ']' | '.' 'Ident')* + /// + private Expression CallExpression() + { + var member = MemberExpression(); + if (CurrentIs("LeftParen")) + { + Expect("LeftParen"); + var expressions = new List(); + if (CurrentIsExpression()) + { + expressions.Add(Expression()); + } + + while (CurrentIs("Comma")) + { + Expect("Comma"); + expressions.Add(Expression()); + } + + var rp = Expect("RightParen"); + return new CallExpression((member as MemberExpression)!, expressions) + { Segment = member.Segment + rp.Segment }; + } + + return member; + } + + /// + /// MemberExpression -> "Ident" ('[' Expression ']' | '.' 'Ident')* + /// + private Expression MemberExpression() + { + var primary = PrimaryExpression(); + + if (!CurrentIs("LeftBracket") && !CurrentIs("Dot") && + !CurrentIs("Assign") && !CurrentIs("LeftParen")) + return primary; + + var identRef = (primary as IdentifierReference)!; + var accessChain = new List(); + while (CurrentIs("LeftBracket") || CurrentIs("Dot")) + { + Token access; + if (CurrentIs("LeftBracket")) + { + access = Expect("LeftBracket"); + var lb = access.Segment; + var expr = Expression(); + var rb = Expect("RightBracket").Segment; + accessChain.Add( + new IndexAccess(expr, accessChain.LastOrDefault()) { Segment = lb + rb }); + } + else if (CurrentIs("Dot")) + { + access = Expect("Dot"); + var identToken = Expect("Ident"); + var idRef = new IdentifierReference(identToken.Value) + { Segment = identToken.Segment }; + accessChain.Add( + new DotAccess(idRef, accessChain.LastOrDefault()) { Segment = access.Segment }); + } + } + + return new MemberExpression( + identRef, + accessChain.FirstOrDefault(), + tail: accessChain.LastOrDefault()) + { + Segment = identRef.Segment + }; + } + + /// + /// CastExpression -> WithExpression 'as' 'string' + /// + private Expression CastExpression() + { + var withExpr = WithExpression(); + if (CurrentIsKeyword("as")) + { + var asKeyword = Expect("Keyword", "as"); + var type = TypeValue(); + return new CastAsExpression(withExpr, type) { Segment = asKeyword.Segment }; + } + + return withExpr; + } + + /// + /// WithExpression -> ConditionalExpression 'with' ObjectLiteral + /// + private Expression WithExpression() + { + var cond = ConditionalExpression(); + if (CurrentIsKeyword("with")) + { + var withKeyword = Expect("Keyword", "with"); + var objectLiteral = ObjectLiteral(); + return new WithExpression(cond, objectLiteral) { Segment = withKeyword.Segment }; + } + + return cond; + } + + /// + /// ConditionalExpression -> OrExpression ('?' Expression ':' Expression)? + /// + private Expression ConditionalExpression() + { + var test = OrExpression(); + if (CurrentIs("QuestionMark")) + { + Expect("QuestionMark"); + var consequent = Expression(); + Expect("Colon"); + var alternate = Expression(); + return new ConditionalExpression(test, consequent, alternate) + { + Segment = consequent.Segment + alternate.Segment + }; + } + + return test; + } + + /// + /// OrExpression -> AndExpression ('||' AndExpression)* + /// + private Expression OrExpression() + { + var left = AndExpression(); + while (CurrentIsOperator("||")) + { + var op = Expect("Operator"); + var right = AndExpression(); + left = new BinaryExpression(left, op.Value, right) + { + Segment = op.Segment + }; + } + + return left; + } + + /// + /// AndExpression -> EqExpression ('&&' EqExpression)* + /// + private Expression AndExpression() + { + var left = EqualityExpression(); + while (CurrentIsOperator("&&")) + { + var op = Expect("Operator"); + var right = EqualityExpression(); + left = new BinaryExpression(left, op.Value, right) + { + Segment = op.Segment + }; + } + + return left; + } + + /// + /// EqExpression -> RelExpression (('=='|'!=') RelExpression)* + /// + private Expression EqualityExpression() + { + var left = RelationExpression(); + while (CurrentIsOperator("==") || CurrentIsOperator("!=")) + { + var op = Expect("Operator"); + var right = RelationExpression(); + left = new BinaryExpression(left, op.Value, right) + { + Segment = op.Segment + }; + } + + return left; + } + + /// + /// RelExpression -> AddExpression (('<'|'>'|'≤'|'≥') AddExpression)* + /// + private Expression RelationExpression() + { + var left = AdditiveExpression(); + while (CurrentIsOperator(">") || CurrentIsOperator("<") || CurrentIsOperator(">=") || + CurrentIsOperator("<=")) + { + var op = Expect("Operator"); + var right = AdditiveExpression(); + left = new BinaryExpression(left, op.Value, right) + { + Segment = op.Segment + }; + } + + return left; + } + + /// + /// AddExpression -> MulExpression (('+'|'-') MulExpression)* + /// + private Expression AdditiveExpression() + { + var left = MultiplicativeExpression(); + while (CurrentIsOperator("+") || CurrentIsOperator("-")) + { + var op = Expect("Operator"); + var right = MultiplicativeExpression(); + left = new BinaryExpression(left, op.Value, right) + { + Segment = op.Segment + }; + } + + return left; + } + + /// + /// MulExpression -> UnaryExpression (('*'|'/'|'%'|'++'|'::') UnaryExpression)* + /// + private Expression MultiplicativeExpression() + { + var left = UnaryExpression(); + while (CurrentIsOperator("*") || CurrentIsOperator("/") || CurrentIsOperator("%") + || CurrentIsOperator("++") || CurrentIsOperator("::")) + { + var op = Expect("Operator"); + var right = UnaryExpression(); + left = new BinaryExpression(left, op.Value, right) + { + Segment = op.Segment + }; + } + + return left; + } + + /// + /// UnaryExpression -> LeftHandSideExpression | ('-'|'!'|'~') UnaryExpression + /// + private Expression UnaryExpression() + { + if (CurrentIsUnaryOperator(expectEnv: false)) + { + var op = Expect("Operator"); + return new UnaryExpression(op.Value, UnaryExpression()) + { + Segment = op.Segment + }; + } + + return LeftHandSideExpression(); + } + + /// + /// LeftHandSideExpression -> MemberExpression | CallExpression + /// + private Expression LeftHandSideExpression() + { + return CallExpression(); + } + + /// + /// PrimaryExpression -> "Ident" | EnvVar | Literal | '(' Expression ')' | ObjectLiteral | ArrayLiteral + /// EnvVar -> '$' "Ident" + /// + private Expression PrimaryExpression() + { + if (CurrentIs("LeftParen")) + { + Expect("LeftParen"); + var expr = Expression(); + Expect("RightParen"); + return expr; + } + + if (CurrentIs("Ident")) + { + var ident = Expect("Ident"); + return new IdentifierReference(ident.Value) + { + Segment = ident.Segment + }; + } + + if (CurrentIsOperator("$")) + { + var dollar = Expect("Operator"); + var ident = Expect("Ident"); + return new EnvVarReference(ident.Value) + { + Segment = dollar.Segment + ident.Segment + }; + } + + if (CurrentIsLiteral()) + { + return LiteralNode(); + } + + if (CurrentIs("LeftCurl")) + { + return ObjectLiteral(); + } + + if (CurrentIs("LeftBracket")) + { + return ArrayLiteral(); + } + + return null!; + } + + /// + /// Literal -> "NullLiteral" + /// "IntegerLiteral" + /// "FloatLiteral" + /// "StringLiteral" + /// "BooleanLiteral" + /// + private Literal LiteralNode() + { + var segment = _tokens.Current.Segment; + if (CurrentIs("StringLiteral")) + { + var str = Expect("StringLiteral"); + return Literal.String( + value: Regex.Unescape(str.Value.Trim('"')), + segment, + label: str.Value + .Replace(@"\", @"\\") + .Replace(@"""", @"\""")); + } + + if (CurrentIs("NullLiteral")) + { + Expect("NullLiteral"); + return Literal.Null(segment); + } + + return _tokens.Current.Type.Tag switch + { + "IntegerLiteral" => Literal.Number(value: double.Parse(Expect("IntegerLiteral").Value), segment), + "FloatLiteral" => Literal.Number( + value: double.Parse( + Expect("FloatLiteral").Value, + CultureInfo.InvariantCulture), + segment), + "BooleanLiteral" => Literal.Boolean(value: bool.Parse(Expect("BooleanLiteral").Value), segment), + _ => throw new ParserException("There are no more supported literals") + }; + } + + /// + /// ObjectLiteral -> '{' PropertyDefinitionList '}' + /// + private ObjectLiteral ObjectLiteral() + { + Expect("LeftCurl"); + var properties = new List(); + while (CurrentIs("Ident")) + { + var idToken = Expect("Ident"); + var id = new IdentifierReference(idToken.Value) + { Segment = idToken.Segment }; + + Expect("Colon"); + var expr = Expression(); + properties.Add(new Property(id, expr) { Segment = idToken.Segment }); + + Expect("SemiColon"); + } + + Expect("RightCurl"); + return new ObjectLiteral(properties); + } + + /// + /// ArrayLiteral -> '[' ElementList ']' + /// + private ArrayLiteral ArrayLiteral() + { + var lb = Expect("LeftBracket").Segment; + var expressions = new List(); + while (CurrentIsExpression()) + { + expressions.Add(Expression()); + if (!CurrentIs("RightBracket")) + { + Expect("Comma"); + } + } + + var rb = Expect("RightBracket").Segment; + return new ArrayLiteral(expressions) { Segment = lb + rb }; + } +} \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Statement.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Statement.cs new file mode 100644 index 00000000..e870d72f --- /dev/null +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Statement.cs @@ -0,0 +1,162 @@ +using HydraScript.Domain.Constants; +using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes; +using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Expressions.PrimaryExpressions; +using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Statements; + +namespace HydraScript.Domain.FrontEnd.Parser.Impl; + +public partial class TopDownParser +{ + /// + /// Statement -> BlockStatement + /// ExpressionStatement + /// IfStatement + /// WhileStatement + /// ContinueStatement + /// BreakStatement + /// ReturnStatement + /// OutputStatement + /// InputStatement + /// + private Statement Statement() + { + if (CurrentIs("Ident") || CurrentIsLiteral() || + CurrentIs("LeftParen") || CurrentIsUnaryOperator()) + return ExpressionStatement(); + + if (CurrentIs("LeftCurl")) + return BlockStatement(); + + if (CurrentIsKeyword("return")) + return ReturnStatement(); + + if (CurrentIsKeyword("break")) + return new InsideStatementJump(InsideStatementJumpKeyword.Break) + { + Segment = Expect("Keyword", "break").Segment + }; + + if (CurrentIsKeyword("continue")) + return new InsideStatementJump(InsideStatementJumpKeyword.Continue) + { + Segment = Expect("Keyword", "continue").Segment + }; + + if (CurrentIsKeyword("if")) + return IfStatement(); + + if (CurrentIsKeyword("while")) + return WhileStatement(); + + if (CurrentIs("Output")) + return OutputStatement(); + + if (CurrentIs("Input")) + return InputStatement(); + + return null!; + } + + /// + /// BlockStatement -> '{' StatementList '}' + /// + private BlockStatement BlockStatement() + { + Expect("LeftCurl"); + var block = new BlockStatement(StatementList()); + Expect("RightCurl"); + + return block; + } + + /// + /// ExpressionStatement -> Expression + /// + private ExpressionStatement ExpressionStatement() + { + return new(Expression()); + } + + /// + /// ReturnStatement -> 'return' Expression? + /// + private ReturnStatement ReturnStatement() + { + var ret = Expect("Keyword", "return"); + if (CurrentIsExpression()) + { + return new ReturnStatement(Expression()) { Segment = ret.Segment }; + } + + return new ReturnStatement { Segment = ret.Segment }; + } + + /// + /// IfStatement -> 'if' '(' Expression ')' Statement ('else' Statement)? + /// + private IfStatement IfStatement() + { + var token = Expect("Keyword", "if"); + Expect("LeftParen"); + var expr = Expression(); + Expect("RightParen"); + var then = Statement(); + if (CurrentIsKeyword("else")) + { + Expect("Keyword", "else"); + var @else = Statement(); + return new IfStatement(expr, then, @else) { Segment = token.Segment }; + } + + return new IfStatement(expr, then) { Segment = token.Segment }; + } + + /// + /// WhileStatement -> 'while' '(' Expression ')' Statement + /// + private WhileStatement WhileStatement() + { + var token = Expect("Keyword", "while"); + Expect("LeftParen"); + var expr = Expression(); + Expect("RightParen"); + var stmt = Statement(); + return new WhileStatement(expr, stmt) { Segment = token.Segment }; + } + + /// + /// OutputStatement -> '>>>' Expression + /// + private OutputStatement OutputStatement() + { + Expect("Output"); + return new OutputStatement(Expression()); + } + + /// + /// InputStatement -> '<<<' (Ident | EnvVar) + /// + private InputStatement InputStatement() + { + var input = Expect("Input"); + if (CurrentIsOperator("$")) + { + var dollar = Expect("Operator"); + var envIdent = Expect("Ident"); + return new InputStatement( + new EnvVarReference(envIdent.Value) + { + Segment = dollar.Segment + envIdent.Segment + }) + { + Segment = input.Segment + envIdent.Segment + }; + } + + var ident = Expect("Ident"); + return new InputStatement(new IdentifierReference(ident.Value) { Segment = ident.Segment }) + { + Segment = input.Segment + ident.Segment + }; + } +} \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.cs index f1c00381..65e4262f 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.cs @@ -1,20 +1,11 @@ -using System.Globalization; -using System.Text.RegularExpressions; using HydraScript.Domain.Constants; using HydraScript.Domain.FrontEnd.Lexer; using HydraScript.Domain.FrontEnd.Parser.Impl.Ast; using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes; -using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Declarations; -using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Declarations.AfterTypesAreLoaded; -using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Expressions; -using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Expressions.AccessExpressions; -using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Expressions.ComplexLiterals; -using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Expressions.PrimaryExpressions; -using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Statements; namespace HydraScript.Domain.FrontEnd.Parser.Impl; -public class TopDownParser(ILexer lexer) : IParser +public partial class TopDownParser(ILexer lexer) : IParser { private IEnumerator _tokens = Enumerable.Empty().GetEnumerator(); @@ -106,794 +97,4 @@ private StatementListItem StatementListItem() return Statement(); } - - /// - /// Statement -> BlockStatement - /// ExpressionStatement - /// IfStatement - /// WhileStatement - /// ContinueStatement - /// BreakStatement - /// ReturnStatement - /// OutputStatement - /// InputStatement - /// - private Statement Statement() - { - if (CurrentIs("Ident") || CurrentIsLiteral() || - CurrentIs("LeftParen") || CurrentIsUnaryOperator()) - return ExpressionStatement(); - - if (CurrentIs("LeftCurl")) - return BlockStatement(); - - if (CurrentIsKeyword("return")) - return ReturnStatement(); - - if (CurrentIsKeyword("break")) - return new InsideStatementJump(InsideStatementJumpKeyword.Break) - { - Segment = Expect("Keyword", "break").Segment - }; - - if (CurrentIsKeyword("continue")) - return new InsideStatementJump(InsideStatementJumpKeyword.Continue) - { - Segment = Expect("Keyword", "continue").Segment - }; - - if (CurrentIsKeyword("if")) - return IfStatement(); - - if (CurrentIsKeyword("while")) - return WhileStatement(); - - if (CurrentIs("Output")) - return OutputStatement(); - - if (CurrentIs("Input")) - return InputStatement(); - - return null!; - } - - /// - /// BlockStatement -> '{' StatementList '}' - /// - private BlockStatement BlockStatement() - { - Expect("LeftCurl"); - var block = new BlockStatement(StatementList()); - Expect("RightCurl"); - - return block; - } - - /// - /// ExpressionStatement -> Expression - /// - private ExpressionStatement ExpressionStatement() - { - return new(Expression()); - } - - /// - /// ReturnStatement -> 'return' Expression? - /// - private ReturnStatement ReturnStatement() - { - var ret = Expect("Keyword", "return"); - if (CurrentIsExpression()) - { - return new ReturnStatement(Expression()) { Segment = ret.Segment }; - } - - return new ReturnStatement { Segment = ret.Segment }; - } - - /// - /// IfStatement -> 'if' '(' Expression ')' Statement ('else' Statement)? - /// - private IfStatement IfStatement() - { - var token = Expect("Keyword", "if"); - Expect("LeftParen"); - var expr = Expression(); - Expect("RightParen"); - var then = Statement(); - if (CurrentIsKeyword("else")) - { - Expect("Keyword", "else"); - var @else = Statement(); - return new IfStatement(expr, then, @else) {Segment = token.Segment}; - } - - return new IfStatement(expr, then) {Segment = token.Segment}; - } - - /// - /// WhileStatement -> 'while' '(' Expression ')' Statement - /// - private WhileStatement WhileStatement() - { - var token = Expect("Keyword", "while"); - Expect("LeftParen"); - var expr = Expression(); - Expect("RightParen"); - var stmt = Statement(); - return new WhileStatement(expr, stmt) {Segment = token.Segment}; - } - - /// - /// OutputStatement -> '>>>' Expression - /// - private OutputStatement OutputStatement() - { - Expect("Output"); - return new OutputStatement(Expression()); - } - - /// - /// InputStatement -> '<<<' (Ident | EnvVar) - /// - private InputStatement InputStatement() - { - var input = Expect("Input"); - if (CurrentIsOperator("$")) - { - var dollar = Expect("Operator"); - var envIdent = Expect("Ident"); - return new InputStatement(new EnvVarReference(envIdent.Value) - { - Segment = dollar.Segment + envIdent.Segment - }) - { - Segment = input.Segment + envIdent.Segment - }; - } - - var ident = Expect("Ident"); - return new InputStatement(new IdentifierReference(ident.Value) { Segment = ident.Segment }) - { - Segment = input.Segment + ident.Segment - }; - } - - /// - /// TypeDeclaration -> 'type' "Ident" = TypeValue - /// - private TypeDeclaration TypeDeclaration() - { - var typeWord = Expect("Keyword", "type"); - var ident = Expect("Ident"); - Expect("Assign"); - var type = TypeValue(); - - var typeId = new IdentifierReference(name: ident.Value) - { Segment = ident.Segment }; - - return new TypeDeclaration(typeId, type) { Segment = typeWord.Segment + ident.Segment }; - } - - /// - /// TypeValue -> TypeValueBase TypeValueSuffix* - /// - private TypeValue TypeValue() - { - if (CurrentIs("Ident")) - { - var ident = Expect("Ident"); - var identType = new TypeIdentValue( - TypeId: new IdentifierReference(name: ident.Value) - { Segment = ident.Segment }); - - return WithSuffix(identType); - } - - if (CurrentIs("LeftCurl")) - { - Expect("LeftCurl"); - var propertyTypes = new List(); - while (CurrentIs("Ident")) - { - var ident = Expect("Ident"); - Expect("Colon"); - var propType = TypeValue(); - propertyTypes.Add( - new PropertyTypeValue( - ident.Value, - propType)); - Expect("SemiColon"); - } - - Expect("RightCurl"); - - return WithSuffix(new ObjectTypeValue(propertyTypes)); - } - - return null!; - } - - /// - /// TypeValueSuffix -> '['']' | '?' - /// - private TypeValue WithSuffix(TypeValue baseType) - { - var type = baseType; - while (CurrentIs("LeftBracket") || CurrentIs("QuestionMark")) - { - if (CurrentIs("LeftBracket")) - { - Expect("LeftBracket"); - Expect("RightBracket"); - type = new ArrayTypeValue(type); - } - else if (CurrentIs("QuestionMark")) - { - Expect("QuestionMark"); - type = new NullableTypeValue(type); - } - } - - return type; - } - - /// - /// Declaration -> LexicalDeclaration | FunctionDeclaration | TypeDeclaration - /// - private Declaration Declaration() - { - if (CurrentIsKeyword("function")) - { - return FunctionDeclaration(); - } - - if (CurrentIsKeyword("let") || CurrentIsKeyword("const")) - { - return LexicalDeclaration(); - } - - if (CurrentIsKeyword("type")) - { - return TypeDeclaration(); - } - - return null!; - } - - /// - /// FunctionDeclaration -> 'function' "Ident" '(' FunctionParameters? ')' Type? BlockStatement - /// - private FunctionDeclaration FunctionDeclaration() - { - Expect("Keyword", "function"); - var ident = Expect("Ident"); - - Expect("LeftParen"); - var args = new List(); - var indexOfFirstDefaultArgument = int.MaxValue; - while (CurrentIs("Ident")) - { - var arg = Expect("Ident").Value; - if (CurrentIs("Colon")) - { - Expect("Colon"); - var type = TypeValue(); - args.Add(new NamedArgument(arg, type)); - } - else if (CurrentIs("Assign")) - { - Expect("Assign"); - var value = LiteralNode(); - indexOfFirstDefaultArgument = args.Count < indexOfFirstDefaultArgument - ? args.Count - : indexOfFirstDefaultArgument; - args.Add(new DefaultValueArgument(arg, value)); - } - - if (!CurrentIs("RightParen")) - Expect("Comma"); - } - - var rp = Expect("RightParen"); - - TypeValue returnType = new TypeIdentValue( - TypeId: new IdentifierReference(name: "undefined") - { Segment = rp.Segment }); - - if (CurrentIs("Colon")) - { - Expect("Colon"); - returnType = TypeValue(); - } - - var name = new IdentifierReference(ident.Value) { Segment = ident.Segment }; - return new FunctionDeclaration(name, returnType, args, BlockStatement(), indexOfFirstDefaultArgument) - { Segment = ident.Segment }; - } - - /// - /// LexicalDeclaration -> LetOrConst "Ident" Initialization (',' "Ident" Initialization)* - /// - private LexicalDeclaration LexicalDeclaration() - { - var readOnly = CurrentIsKeyword("const"); - Expect("Keyword", readOnly ? "const" : "let"); - var declaration = new LexicalDeclaration(readOnly); - - AddToDeclaration(declaration); - - while (CurrentIs("Comma")) - { - Expect("Comma"); - AddToDeclaration(declaration); - } - - return declaration; - } - - /// - /// Initialization -> Typed | Initializer - /// Typed -> Type Initializer? - /// Initializer -> '=' Expression - /// - private void AddToDeclaration(LexicalDeclaration declaration) - { - var ident = Expect("Ident"); - var identRef = new IdentifierReference(ident.Value) { Segment = ident.Segment }; - var assignment = new AssignmentExpression( - new MemberExpression(identRef), - new ImplicitLiteral(TypeIdentValue.Undefined)) - { Segment = ident.Segment }; - - if (CurrentIs("Assign")) - { - var assignSegment = Expect("Assign").Segment; - var expression = Expression(); - assignment = new AssignmentExpression( - new MemberExpression(identRef), expression - ) { Segment = assignSegment }; - } - else if (CurrentIs("Colon")) - { - Expect("Colon"); - var type = TypeValue(); - if (CurrentIs("Assign")) - { - var assignSegment = Expect("Assign").Segment; - var expression = Expression(); - assignment = new AssignmentExpression( - new MemberExpression(identRef), - expression, type - ) { Segment = assignSegment }; - } - else - { - var expression = new ImplicitLiteral(type); - assignment = new AssignmentExpression( - lhs: new MemberExpression(identRef), - expression, - type); - } - } - declaration.AddAssignment(assignment); - } - - /// - /// Expression -> CastExpression | AssignmentExpression - /// - private Expression Expression() - { - var expr = CastExpression(); - if (expr is LeftHandSideExpression lhs && CurrentIs("Assign")) - { - var assign = Expect("Assign"); - return new AssignmentExpression(lhs, Expression()) - {Segment = assign.Segment}; - } - return expr; - } - - /// - /// CallExpression -> MemberExpression Arguments (Arguments | '[' Expression ']' | '.' 'Ident')* - /// - private Expression CallExpression() - { - var member = MemberExpression(); - if (CurrentIs("LeftParen")) - { - Expect("LeftParen"); - var expressions = new List(); - if (CurrentIsExpression()) - { - expressions.Add(Expression()); - } - - while (CurrentIs("Comma")) - { - Expect("Comma"); - expressions.Add(Expression()); - } - - var rp = Expect("RightParen"); - return new CallExpression((member as MemberExpression)!, expressions) - { Segment = member.Segment + rp.Segment }; - } - - return member; - } - - /// - /// MemberExpression -> "Ident" ('[' Expression ']' | '.' 'Ident')* - /// - private Expression MemberExpression() - { - var primary = PrimaryExpression(); - - if (!CurrentIs("LeftBracket") && !CurrentIs("Dot") && - !CurrentIs("Assign") && !CurrentIs("LeftParen")) - return primary; - - var identRef = (primary as IdentifierReference)!; - var accessChain = new List(); - while (CurrentIs("LeftBracket") || CurrentIs("Dot")) - { - Token access; - if (CurrentIs("LeftBracket")) - { - access = Expect("LeftBracket"); - var lb = access.Segment; - var expr = Expression(); - var rb = Expect("RightBracket").Segment; - accessChain.Add( - new IndexAccess(expr, accessChain.LastOrDefault()) {Segment = lb + rb} - ); - } - else if (CurrentIs("Dot")) - { - access = Expect("Dot"); - var identToken = Expect("Ident"); - var idRef = new IdentifierReference(identToken.Value) - { Segment = identToken.Segment }; - accessChain.Add( - new DotAccess(idRef, accessChain.LastOrDefault()) {Segment = access.Segment} - ); - } - } - - return new MemberExpression( - identRef, - accessChain.FirstOrDefault(), - tail: accessChain.LastOrDefault()) - { - Segment = identRef.Segment - }; - } - - /// - /// CastExpression -> WithExpression 'as' 'string' - /// - private Expression CastExpression() - { - var withExpr = WithExpression(); - if (CurrentIsKeyword("as")) - { - var asKeyword = Expect("Keyword", "as"); - var type = TypeValue(); - return new CastAsExpression(withExpr, type) {Segment = asKeyword.Segment}; - } - - return withExpr; - } - - /// - /// WithExpression -> ConditionalExpression 'with' ObjectLiteral - /// - private Expression WithExpression() - { - var cond = ConditionalExpression(); - if (CurrentIsKeyword("with")) - { - var withKeyword = Expect("Keyword", "with"); - var objectLiteral = ObjectLiteral(); - return new WithExpression(cond, objectLiteral) {Segment = withKeyword.Segment}; - } - - return cond; - } - - /// - /// ConditionalExpression -> OrExpression ('?' Expression ':' Expression)? - /// - private Expression ConditionalExpression() - { - var test = OrExpression(); - if (CurrentIs("QuestionMark")) - { - Expect("QuestionMark"); - var consequent = Expression(); - Expect("Colon"); - var alternate = Expression(); - return new ConditionalExpression(test, consequent, alternate) - { - Segment = consequent.Segment + alternate.Segment - }; - } - - return test; - } - - /// - /// OrExpression -> AndExpression ('||' AndExpression)* - /// - private Expression OrExpression() - { - var left = AndExpression(); - while (CurrentIsOperator("||")) - { - var op = Expect("Operator"); - var right = AndExpression(); - left = new BinaryExpression(left, op.Value, right) - { - Segment = op.Segment - }; - } - - return left; - } - - /// - /// AndExpression -> EqExpression ('&&' EqExpression)* - /// - private Expression AndExpression() - { - var left = EqualityExpression(); - while (CurrentIsOperator("&&")) - { - var op = Expect("Operator"); - var right = EqualityExpression(); - left = new BinaryExpression(left, op.Value, right) - { - Segment = op.Segment - }; - } - - return left; - } - - /// - /// EqExpression -> RelExpression (('=='|'!=') RelExpression)* - /// - private Expression EqualityExpression() - { - var left = RelationExpression(); - while (CurrentIsOperator("==") || CurrentIsOperator("!=")) - { - var op = Expect("Operator"); - var right = RelationExpression(); - left = new BinaryExpression(left, op.Value, right) - { - Segment = op.Segment - }; - } - - return left; - } - - /// - /// RelExpression -> AddExpression (('<'|'>'|'≤'|'≥') AddExpression)* - /// - private Expression RelationExpression() - { - var left = AdditiveExpression(); - while (CurrentIsOperator(">") || CurrentIsOperator("<") || CurrentIsOperator(">=") || - CurrentIsOperator("<=")) - { - var op = Expect("Operator"); - var right = AdditiveExpression(); - left = new BinaryExpression(left, op.Value, right) - { - Segment = op.Segment - }; - } - - return left; - } - - /// - /// AddExpression -> MulExpression (('+'|'-') MulExpression)* - /// - private Expression AdditiveExpression() - { - var left = MultiplicativeExpression(); - while (CurrentIsOperator("+") || CurrentIsOperator("-")) - { - var op = Expect("Operator"); - var right = MultiplicativeExpression(); - left = new BinaryExpression(left, op.Value, right) - { - Segment = op.Segment - }; - } - - return left; - } - - /// - /// MulExpression -> UnaryExpression (('*'|'/'|'%'|'++'|'::') UnaryExpression)* - /// - private Expression MultiplicativeExpression() - { - var left = UnaryExpression(); - while (CurrentIsOperator("*") || CurrentIsOperator("/") || CurrentIsOperator("%") - || CurrentIsOperator("++") || CurrentIsOperator("::")) - { - var op = Expect("Operator"); - var right = UnaryExpression(); - left = new BinaryExpression(left, op.Value, right) - { - Segment = op.Segment - }; - } - - return left; - } - - /// - /// UnaryExpression -> LeftHandSideExpression | ('-'|'!'|'~') UnaryExpression - /// - private Expression UnaryExpression() - { - if (CurrentIsUnaryOperator(expectEnv: false)) - { - var op = Expect("Operator"); - return new UnaryExpression(op.Value, UnaryExpression()) - { - Segment = op.Segment - }; - } - - return LeftHandSideExpression(); - } - - /// - /// LeftHandSideExpression -> MemberExpression | CallExpression - /// - private Expression LeftHandSideExpression() - { - return CallExpression(); - } - - /// - /// PrimaryExpression -> "Ident" | EnvVar | Literal | '(' Expression ')' | ObjectLiteral | ArrayLiteral - /// EnvVar -> '$' "Ident" - /// - private Expression PrimaryExpression() - { - if (CurrentIs("LeftParen")) - { - Expect("LeftParen"); - var expr = Expression(); - Expect("RightParen"); - return expr; - } - - if (CurrentIs("Ident")) - { - var ident = Expect("Ident"); - return new IdentifierReference(ident.Value) - { - Segment = ident.Segment - }; - } - - if (CurrentIsOperator("$")) - { - var dollar = Expect("Operator"); - var ident = Expect("Ident"); - return new EnvVarReference(ident.Value) - { - Segment = dollar.Segment + ident.Segment - }; - } - - if (CurrentIsLiteral()) - { - return LiteralNode(); - } - - if (CurrentIs("LeftCurl")) - { - return ObjectLiteral(); - } - - if (CurrentIs("LeftBracket")) - { - return ArrayLiteral(); - } - - return null!; - } - - /// - /// Literal -> "NullLiteral" - /// "IntegerLiteral" - /// "FloatLiteral" - /// "StringLiteral" - /// "BooleanLiteral" - /// - private Literal LiteralNode() - { - var segment = _tokens.Current.Segment; - if (CurrentIs("StringLiteral")) - { - var str = Expect("StringLiteral"); - return Literal.String( - value: Regex.Unescape(str.Value.Trim('"')), - segment, - label: str.Value - .Replace(@"\", @"\\") - .Replace(@"""", @"\""")); - } - - if (CurrentIs("NullLiteral")) - { - Expect("NullLiteral"); - return Literal.Null(segment); - } - - return _tokens.Current.Type.Tag switch - { - "IntegerLiteral" => Literal.Number(value: double.Parse(Expect("IntegerLiteral").Value), segment), - "FloatLiteral" => Literal.Number( - value: double.Parse( - Expect("FloatLiteral").Value, - CultureInfo.InvariantCulture), - segment), - "BooleanLiteral" => Literal.Boolean(value: bool.Parse(Expect("BooleanLiteral").Value), segment), - _ => throw new ParserException("There are no more supported literals") - }; - } - - /// - /// ObjectLiteral -> '{' PropertyDefinitionList '}' - /// - private ObjectLiteral ObjectLiteral() - { - Expect("LeftCurl"); - var properties = new List(); - while (CurrentIs("Ident")) - { - var idToken = Expect("Ident"); - var id = new IdentifierReference(idToken.Value) - { Segment = idToken.Segment }; - - Expect("Colon"); - var expr = Expression(); - properties.Add(new Property(id, expr) { Segment = idToken.Segment }); - - Expect("SemiColon"); - } - Expect("RightCurl"); - return new ObjectLiteral(properties); - } - - /// - /// ArrayLiteral -> '[' ElementList ']' - /// - private ArrayLiteral ArrayLiteral() - { - var lb = Expect("LeftBracket").Segment; - var expressions = new List(); - while (CurrentIsExpression()) - { - expressions.Add(Expression()); - if (!CurrentIs("RightBracket")) - { - Expect("Comma"); - } - } - var rb = Expect("RightBracket").Segment; - return new ArrayLiteral(expressions) {Segment = lb + rb}; - } } \ No newline at end of file From 437499341584d6382c73e52d9db409337a3e64b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D0=BD=D0=B8=D0=BD=20=D0=A1=D1=82=D0=B5=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B8=D1=87?= Date: Wed, 20 May 2026 19:19:45 +0300 Subject: [PATCH 02/20] docs(grammar): rules update grammar rules for assignment and cast expressions Refined `AssignmentExpression` grammar to include optional "Operator" and updated `CastExpression` to support dynamic `TypeValue`. --- .../Parser/Impl/TopDownParser.Expression.cs | 3 ++- src/Domain/HydraScript.Domain.FrontEnd/Parser/grammar.txt | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs index eab10f9d..553ea03b 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs @@ -12,6 +12,7 @@ public partial class TopDownParser { /// /// Expression -> CastExpression | AssignmentExpression + /// AssignmentExpression -> LeftHandSideExpression "Operator"? '=' Expression /// private Expression Expression() { @@ -101,7 +102,7 @@ private Expression MemberExpression() } /// - /// CastExpression -> WithExpression 'as' 'string' + /// CastExpression -> WithExpression 'as' TypeValue /// private Expression CastExpression() { diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/grammar.txt b/src/Domain/HydraScript.Domain.FrontEnd/Parser/grammar.txt index 91978cbf..331d36eb 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/grammar.txt +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/grammar.txt @@ -23,9 +23,9 @@ ExpressionStatement -> Expression Expression -> CastExpression AssignmentExpression -CastExpression -> WithExpression 'as' 'string' +CastExpression -> WithExpression 'as' TypeValue -AssignmentExpression -> LeftHandSideExpression '=' Expression +AssignmentExpression -> LeftHandSideExpression "Operator"? '=' Expression LeftHandSideExpression -> MemberExpression CallExpression From 08d2ea4f73350e53c98751d393e94a61e81a6a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D0=BD=D0=B8=D0=BD=20=D0=A1=D1=82=D0=B5=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B8=D1=87?= Date: Thu, 21 May 2026 21:53:11 +0300 Subject: [PATCH 03/20] fix(TopDownParser): FunctionDeclaration Added validation to throw `ParserException` when no `:` or `=` follows an argument name --- .../Parser/Impl/TopDownParser.Declaration.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Declaration.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Declaration.cs index 792641a5..ee4ab250 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Declaration.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Declaration.cs @@ -46,12 +46,12 @@ private FunctionDeclaration FunctionDeclaration() var indexOfFirstDefaultArgument = int.MaxValue; while (CurrentIs("Ident")) { - var arg = Expect("Ident").Value; + var arg = Expect("Ident"); if (CurrentIs("Colon")) { Expect("Colon"); var type = TypeValue(); - args.Add(new NamedArgument(arg, type)); + args.Add(new NamedArgument(arg.Value, type)); } else if (CurrentIs("Assign")) { @@ -60,8 +60,9 @@ private FunctionDeclaration FunctionDeclaration() indexOfFirstDefaultArgument = args.Count < indexOfFirstDefaultArgument ? args.Count : indexOfFirstDefaultArgument; - args.Add(new DefaultValueArgument(arg, value)); + args.Add(new DefaultValueArgument(arg.Value, value)); } + else throw new ParserException($"Expected ':' or '=' after argument name <{arg}>"); if (!CurrentIs("RightParen")) Expect("Comma"); From d301c7a3b89c8a94d6878498c12960ceff783160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D0=BD=D0=B8=D0=BD=20=D0=A1=D1=82=D0=B5=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B8=D1=87?= Date: Thu, 21 May 2026 21:53:35 +0300 Subject: [PATCH 04/20] refactor(ParserException): simplify constructor by removing `Segment` parameter --- .../HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.cs | 4 ++-- .../HydraScript.Domain.FrontEnd/Parser/ParserException.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.cs index 65e4262f..ec4ef4d2 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.cs @@ -24,9 +24,9 @@ private Token Expect(string expectedTag, string? expectedValue = null) var current = _tokens.Current; if (!CurrentIs(expectedTag)) - throw new ParserException(_tokens.Current.Segment, expectedTag, _tokens.Current); + throw new ParserException(expectedTag, _tokens.Current); if (_tokens.Current.Value != (expectedValue ?? _tokens.Current.Value)) - throw new ParserException(_tokens.Current.Segment, expectedValue, _tokens.Current); + throw new ParserException(expectedValue, _tokens.Current); _tokens.MoveNext(); return current; diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/ParserException.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/ParserException.cs index 995dbf2d..ac473f48 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/ParserException.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/ParserException.cs @@ -12,8 +12,8 @@ public ParserException(string message) : base(message) { } protected ParserException(string message, Exception inner) : base(message, inner) { } - public ParserException(Segment segment, string? expected, Token actual) : - base($"Wrong syntax: {segment} expected {expected}; actual = ({actual.Type.Tag}, {actual.Value})") + public ParserException(string? expected, Token actual) : + base($"Wrong syntax: {actual.Segment} expected {expected}; actual = ({actual.Type.Tag}, {actual.Value})") { } } \ No newline at end of file From a0a237a8035a9cd135b17f3bb2cb9ea411a196fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D0=BD=D0=B8=D0=BD=20=D0=A1=D1=82=D0=B5=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B8=D1=87?= Date: Thu, 21 May 2026 21:54:20 +0300 Subject: [PATCH 05/20] refactor(TopDownParser): CallExpression simplify expression parsing logic Allow end call expr list with comma --- .../Parser/Impl/TopDownParser.Expression.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs index 553ea03b..58cd9115 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs @@ -37,15 +37,12 @@ private Expression CallExpression() { Expect("LeftParen"); var expressions = new List(); - if (CurrentIsExpression()) - { - expressions.Add(Expression()); - } - while (CurrentIs("Comma")) + while (CurrentIsExpression()) { - Expect("Comma"); expressions.Add(Expression()); + if (!CurrentIs("RightParen")) + Expect("Comma"); } var rp = Expect("RightParen"); From 2dfb4c79d15037b354079e60618eaf548caf6ef7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D0=BD=D0=B8=D0=BD=20=D0=A1=D1=82=D0=B5=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B8=D1=87?= Date: Thu, 21 May 2026 22:37:54 +0300 Subject: [PATCH 06/20] refactor(AST): AssignmentExpression replace `LeftHandSideExpression` with `MemberExpression` in `AssignmentExpression` and related grammar adjustments --- .../Exceptions/WrongAssignmentTarget.cs | 10 ---------- .../Visitors/SemanticChecker.cs | 3 --- .../Impl/Ast/Nodes/Expressions/AssignmentExpression.cs | 4 ++-- .../Parser/Impl/TopDownParser.Expression.cs | 4 ++-- .../HydraScript.Domain.FrontEnd/Parser/grammar.txt | 2 +- 5 files changed, 5 insertions(+), 18 deletions(-) delete mode 100644 src/Application/HydraScript.Application.StaticAnalysis/Exceptions/WrongAssignmentTarget.cs diff --git a/src/Application/HydraScript.Application.StaticAnalysis/Exceptions/WrongAssignmentTarget.cs b/src/Application/HydraScript.Application.StaticAnalysis/Exceptions/WrongAssignmentTarget.cs deleted file mode 100644 index 2f781f57..00000000 --- a/src/Application/HydraScript.Application.StaticAnalysis/Exceptions/WrongAssignmentTarget.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Expressions; - -namespace HydraScript.Application.StaticAnalysis.Exceptions; - -[ExcludeFromCodeCoverage] -public class WrongAssignmentTarget(LeftHandSideExpression lhs) : - SemanticException( - lhs.Segment, - $"Assignment target must be variable, property or indexer. '{lhs.Id.Name}' is {lhs.GetType().Name}"); \ No newline at end of file diff --git a/src/Application/HydraScript.Application.StaticAnalysis/Visitors/SemanticChecker.cs b/src/Application/HydraScript.Application.StaticAnalysis/Visitors/SemanticChecker.cs index fa217df2..7ae491e5 100644 --- a/src/Application/HydraScript.Application.StaticAnalysis/Visitors/SemanticChecker.cs +++ b/src/Application/HydraScript.Application.StaticAnalysis/Visitors/SemanticChecker.cs @@ -308,9 +308,6 @@ public Type Visit(AssignmentExpression visitable) { var typeComparer = default(CommutativeTypeEqualityComparer); - if (visitable.Destination is CallExpression) - throw new WrongAssignmentTarget(visitable.Destination); - var sourceType = visitable.Source.Accept(This); if (!visitable.Destination.Empty()) { diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/AssignmentExpression.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/AssignmentExpression.cs index 9408c52d..4717e085 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/AssignmentExpression.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/AssignmentExpression.cs @@ -7,12 +7,12 @@ public partial class AssignmentExpression : Expression { protected override IReadOnlyList Children { get; } - public LeftHandSideExpression Destination { get; } + public MemberExpression Destination { get; } public Expression Source { get; } public TypeValue? DestinationType { get; } public AssignmentExpression( - LeftHandSideExpression lhs, + MemberExpression lhs, Expression source, TypeValue? destinationType = null) { diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs index 58cd9115..c9b415f6 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs @@ -12,12 +12,12 @@ public partial class TopDownParser { /// /// Expression -> CastExpression | AssignmentExpression - /// AssignmentExpression -> LeftHandSideExpression "Operator"? '=' Expression + /// AssignmentExpression -> MemberExpression "Operator"? '=' Expression /// private Expression Expression() { var expr = CastExpression(); - if (expr is LeftHandSideExpression lhs && CurrentIs("Assign")) + if (expr is MemberExpression lhs && CurrentIs("Assign")) { var assign = Expect("Assign"); return new AssignmentExpression(lhs, Expression()) diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/grammar.txt b/src/Domain/HydraScript.Domain.FrontEnd/Parser/grammar.txt index 331d36eb..32562d74 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/grammar.txt +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/grammar.txt @@ -25,7 +25,7 @@ Expression -> CastExpression CastExpression -> WithExpression 'as' TypeValue -AssignmentExpression -> LeftHandSideExpression "Operator"? '=' Expression +AssignmentExpression -> MemberExpression "Operator"? '=' Expression LeftHandSideExpression -> MemberExpression CallExpression From b8e4d6b653859cfece78210c3d4cc1f71a23371e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D0=BD=D0=B8=D0=BD=20=D0=A1=D1=82=D0=B5=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B8=D1=87?= Date: Thu, 21 May 2026 23:26:57 +0300 Subject: [PATCH 07/20] refactor(TopDownParser): LexicalDeclaration simplify declaration parsing logic Replaced `AddToDeclaration` with `DeclarationAssignmentExpression` Prohibited undefined vars like `let x` grammar-wise --- .../Visitors/DeclarationVisitor.cs | 3 +- .../Parser/Impl/TopDownParser.Declaration.cs | 49 ++++++------------- 2 files changed, 17 insertions(+), 35 deletions(-) diff --git a/src/Application/HydraScript.Application.StaticAnalysis/Visitors/DeclarationVisitor.cs b/src/Application/HydraScript.Application.StaticAnalysis/Visitors/DeclarationVisitor.cs index 7bb0f789..e929bc79 100644 --- a/src/Application/HydraScript.Application.StaticAnalysis/Visitors/DeclarationVisitor.cs +++ b/src/Application/HydraScript.Application.StaticAnalysis/Visitors/DeclarationVisitor.cs @@ -56,8 +56,7 @@ public VisitUnit Visit(LexicalDeclaration visitable) if (_symbolTables[visitable.Scope].ContainsSymbol(new VariableSymbolId(assignment.Destination.Id))) throw new DeclarationAlreadyExists(assignment.Destination.Id); - var destinationType = assignment.DestinationType?.Accept( - _typeBuilder) ?? _typesService.Undefined; + var destinationType = assignment.DestinationType?.Accept(_typeBuilder) ?? _typesService.Undefined; if (destinationType == _typesService.Undefined && assignment.Source is ImplicitLiteral or ArrayLiteral { Expressions.Count: 0 }) diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Declaration.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Declaration.cs index ee4ab250..6ce1d705 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Declaration.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Declaration.cs @@ -68,11 +68,8 @@ private FunctionDeclaration FunctionDeclaration() Expect("Comma"); } - var rp = Expect("RightParen"); - - TypeValue returnType = new TypeIdentValue( - TypeId: new IdentifierReference(name: "undefined") - { Segment = rp.Segment }); + Expect("RightParen"); + TypeValue returnType = TypeIdentValue.Undefined; if (CurrentIs("Colon")) { @@ -94,12 +91,12 @@ private LexicalDeclaration LexicalDeclaration() Expect("Keyword", readOnly ? "const" : "let"); var declaration = new LexicalDeclaration(readOnly); - AddToDeclaration(declaration); + declaration.AddAssignment(DeclarationAssignmentExpression()); while (CurrentIs("Comma")) { Expect("Comma"); - AddToDeclaration(declaration); + declaration.AddAssignment(DeclarationAssignmentExpression()); } return declaration; @@ -110,45 +107,31 @@ private LexicalDeclaration LexicalDeclaration() /// Typed -> Type Initializer? /// Initializer -> '=' Expression /// - private void AddToDeclaration(LexicalDeclaration declaration) + private AssignmentExpression DeclarationAssignmentExpression() { var ident = Expect("Ident"); var identRef = new IdentifierReference(ident.Value) { Segment = ident.Segment }; - var assignment = new AssignmentExpression( - new MemberExpression(identRef), - new ImplicitLiteral(TypeIdentValue.Undefined)) - { Segment = ident.Segment }; if (CurrentIs("Assign")) { var assignSegment = Expect("Assign").Segment; - var expression = Expression(); - assignment = new AssignmentExpression( - new MemberExpression(identRef), expression) { Segment = assignSegment }; + return new AssignmentExpression( + new MemberExpression(identRef), Expression()) + { Segment = assignSegment }; } - else if (CurrentIs("Colon")) + + if (CurrentIs("Colon")) { Expect("Colon"); var type = TypeValue(); - if (CurrentIs("Assign")) - { - var assignSegment = Expect("Assign").Segment; - var expression = Expression(); - assignment = new AssignmentExpression( - new MemberExpression(identRef), - expression, type) { Segment = assignSegment }; - } - else - { - var expression = new ImplicitLiteral(type); - assignment = new AssignmentExpression( - lhs: new MemberExpression(identRef), - expression, - type); - } + var assignSegment = CurrentIs("Assign") ? Expect("Assign").Segment : string.Empty; + var expression = assignSegment is not "" ? Expression() : new ImplicitLiteral(type); + return new AssignmentExpression( + new MemberExpression(identRef), expression, type) + { Segment = assignSegment }; } - declaration.AddAssignment(assignment); + throw new ParserException($"Expected ':' or '=' after var name <{ident}>"); } /// From f8d8cd8a6b282a2874fe5dee7e77c8fd99fcf39d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D0=BD=D0=B8=D0=BD=20=D0=A1=D1=82=D0=B5=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B8=D1=87?= Date: Fri, 22 May 2026 01:15:09 +0300 Subject: [PATCH 08/20] refactor(TopDownParser): Grammar - Replaced null returns with `ParserException` for better error handling - Extracted `CurrentIsStatement` for cleaner parsing logic - Updated grammar to improve clarity on `MemberExpression` Closes #233 --- .../Parser/Impl/TopDownParser.Declaration.cs | 4 +- .../Parser/Impl/TopDownParser.Expression.cs | 143 ++++++++++-------- .../Parser/Impl/TopDownParser.Statement.cs | 2 +- .../Parser/Impl/TopDownParser.cs | 16 +- .../Parser/grammar.txt | 39 ++--- 5 files changed, 111 insertions(+), 93 deletions(-) diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Declaration.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Declaration.cs index 6ce1d705..f9b2a538 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Declaration.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Declaration.cs @@ -30,7 +30,7 @@ private Declaration Declaration() return TypeDeclaration(); } - return null!; + throw new ParserException(nameof(Declaration), _tokens.Current); } /// @@ -186,7 +186,7 @@ private TypeValue TypeValue() return WithSuffix(new ObjectTypeValue(propertyTypes)); } - return null!; + throw new ParserException(nameof(TypeValue), _tokens.Current); } /// diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs index c9b415f6..ca5dc3e9 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs @@ -1,6 +1,5 @@ using System.Globalization; using System.Text.RegularExpressions; -using HydraScript.Domain.FrontEnd.Lexer; using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Expressions; using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Expressions.AccessExpressions; using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Expressions.ComplexLiterals; @@ -12,7 +11,7 @@ public partial class TopDownParser { /// /// Expression -> CastExpression | AssignmentExpression - /// AssignmentExpression -> MemberExpression "Operator"? '=' Expression + /// AssignmentExpression -> MemberExpression '=' Expression /// private Expression Expression() { @@ -28,7 +27,8 @@ private Expression Expression() } /// - /// CallExpression -> MemberExpression Arguments (Arguments | '[' Expression ']' | '.' 'Ident')* + /// CallExpression -> MemberExpression Arguments + /// Arguments -> '(' (Expression ',')* ')' /// private Expression CallExpression() { @@ -46,55 +46,49 @@ private Expression CallExpression() } var rp = Expect("RightParen"); - return new CallExpression((member as MemberExpression)!, expressions) + return new CallExpression(member, expressions) { Segment = member.Segment + rp.Segment }; } - return member; + return member.Empty() && !CurrentIs("Assign") ? member.Id : member; } /// - /// MemberExpression -> "Ident" ('[' Expression ']' | '.' 'Ident')* + /// MemberExpression -> Var ('[' Expression ']' | '.' 'Ident')* /// - private Expression MemberExpression() + private MemberExpression MemberExpression() { - var primary = PrimaryExpression(); - - if (!CurrentIs("LeftBracket") && !CurrentIs("Dot") && - !CurrentIs("Assign") && !CurrentIs("LeftParen")) - return primary; - - var identRef = (primary as IdentifierReference)!; - var accessChain = new List(); + var memberRoot = Var(); + var accessChain = new LinkedList(); while (CurrentIs("LeftBracket") || CurrentIs("Dot")) { - Token access; if (CurrentIs("LeftBracket")) { - access = Expect("LeftBracket"); - var lb = access.Segment; + var lb = Expect("LeftBracket").Segment; var expr = Expression(); var rb = Expect("RightBracket").Segment; - accessChain.Add( - new IndexAccess(expr, accessChain.LastOrDefault()) { Segment = lb + rb }); + accessChain.AddLast( + new IndexAccess(expr, accessChain.Last?.Value) + { Segment = lb + rb }); } else if (CurrentIs("Dot")) { - access = Expect("Dot"); - var identToken = Expect("Ident"); - var idRef = new IdentifierReference(identToken.Value) - { Segment = identToken.Segment }; - accessChain.Add( - new DotAccess(idRef, accessChain.LastOrDefault()) { Segment = access.Segment }); + var access = Expect("Dot"); + var propToken = Expect("Ident"); + var propIdent = new IdentifierReference(propToken.Value) + { Segment = propToken.Segment }; + accessChain.AddLast( + new DotAccess(propIdent, accessChain.Last?.Value) + { Segment = access.Segment }); } } return new MemberExpression( - identRef, - accessChain.FirstOrDefault(), - tail: accessChain.LastOrDefault()) + memberRoot, + accessChain.First?.Value, + tail: accessChain.Last?.Value) { - Segment = identRef.Segment + Segment = memberRoot.Segment }; } @@ -285,18 +279,14 @@ private Expression UnaryExpression() } /// - /// LeftHandSideExpression -> MemberExpression | CallExpression + /// LeftHandSideExpression -> PrimaryExpression + /// ParenthesizedExpression + /// ComplexLiteral + /// MemberExpression + /// CallExpression + /// ParenthesizedExpression -> '(' Expression ')' /// private Expression LeftHandSideExpression() - { - return CallExpression(); - } - - /// - /// PrimaryExpression -> "Ident" | EnvVar | Literal | '(' Expression ')' | ObjectLiteral | ArrayLiteral - /// EnvVar -> '$' "Ident" - /// - private Expression PrimaryExpression() { if (CurrentIs("LeftParen")) { @@ -306,41 +296,48 @@ private Expression PrimaryExpression() return expr; } - if (CurrentIs("Ident")) + if (CurrentIs("LeftCurl") || CurrentIs("LeftBracket")) { - var ident = Expect("Ident"); - return new IdentifierReference(ident.Value) - { - Segment = ident.Segment - }; + return ComplexLiteral(); } - if (CurrentIsOperator("$")) + if (CurrentIs("Ident") || CurrentIsOperator("$")) { - var dollar = Expect("Operator"); - var ident = Expect("Ident"); - return new EnvVarReference(ident.Value) - { - Segment = dollar.Segment + ident.Segment - }; + return CallExpression(); } - if (CurrentIsLiteral()) - { - return LiteralNode(); - } + return PrimaryExpression(); + } - if (CurrentIs("LeftCurl")) - { - return ObjectLiteral(); - } + /// + /// PrimaryExpression -> Var | Literal + /// + private PrimaryExpression PrimaryExpression() + { + return LiteralNode(); + } - if (CurrentIs("LeftBracket")) + /// + /// Var -> "Ident" | EnvVar + /// EnvVar -> '$' "Ident" + /// + private IdentifierReference Var() + { + if (CurrentIs("Ident")) { - return ArrayLiteral(); + var ident = Expect("Ident"); + return new IdentifierReference(ident.Value) + { + Segment = ident.Segment + }; } - return null!; + var dollar = Expect("Operator"); + var envIdent = Expect("Ident"); + return new EnvVarReference(envIdent.Value) + { + Segment = dollar.Segment + envIdent.Segment + }; } /// @@ -379,12 +376,27 @@ private Literal LiteralNode() CultureInfo.InvariantCulture), segment), "BooleanLiteral" => Literal.Boolean(value: bool.Parse(Expect("BooleanLiteral").Value), segment), - _ => throw new ParserException("There are no more supported literals") + _ => throw new ParserException("Literal", _tokens.Current) }; } + /// + /// ComplexLiteral -> ObjectLiteral | ArrayLiteral + /// + private ComplexLiteral ComplexLiteral() + { + if (CurrentIs("LeftCurl")) + { + return ObjectLiteral(); + } + + return ArrayLiteral(); + } + /// /// ObjectLiteral -> '{' PropertyDefinitionList '}' + /// PropertyDefinitionList -> (FieldProperty ';')* + /// FieldProperty -> "Ident" ':' Expression /// private ObjectLiteral ObjectLiteral() { @@ -409,6 +421,7 @@ private ObjectLiteral ObjectLiteral() /// /// ArrayLiteral -> '[' ElementList ']' + /// ElementList -> (Expression ',')* /// private ArrayLiteral ArrayLiteral() { diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Statement.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Statement.cs index e870d72f..fb846e4c 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Statement.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Statement.cs @@ -54,7 +54,7 @@ private Statement Statement() if (CurrentIs("Input")) return InputStatement(); - return null!; + throw new ParserException(nameof(Statement), _tokens.Current); } /// diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.cs index ec4ef4d2..8cd35303 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.cs @@ -62,11 +62,16 @@ private bool CurrentIsExpression() => CurrentIs("Ident") || CurrentIsLiteral() || CurrentIsUnaryOperator() || CurrentIs("LeftParen") || CurrentIs("LeftCurl") || CurrentIs("LeftBracket"); + private bool CurrentIsStatement() => + CurrentIsExpression() || + CurrentIs("Output") || CurrentIs("Input") || + CurrentIsKeyword("return") || CurrentIsKeyword("break") || CurrentIsKeyword("continue") || + CurrentIsKeyword("if") || CurrentIsKeyword("while"); + /// /// Script -> StatementList /// - private ScriptBody Script() => - new(StatementList()); + private ScriptBody Script() => new(StatementList()); /// /// StatementList -> StatementListItem* @@ -74,10 +79,7 @@ private ScriptBody Script() => private List StatementList() { var statementList = new List(); - while (CurrentIsDeclaration() || CurrentIsExpression() || - CurrentIs("Output") || CurrentIs("Input") || - CurrentIsKeyword("return") || CurrentIsKeyword("break") || CurrentIsKeyword("continue") || - CurrentIsKeyword("if") || CurrentIsKeyword("while")) + while (CurrentIsDeclaration() || CurrentIsStatement()) { statementList.Add(StatementListItem()); } @@ -91,9 +93,7 @@ private List StatementList() private StatementListItem StatementListItem() { if (CurrentIsDeclaration()) - { return Declaration(); - } return Statement(); } diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/grammar.txt b/src/Domain/HydraScript.Domain.FrontEnd/Parser/grammar.txt index 32562d74..49798048 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/grammar.txt +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/grammar.txt @@ -1,7 +1,6 @@ Script -> StatementList StatementList -> StatementListItem* -StatementListItem -> Statement - Declaration +StatementListItem -> Statement | Declaration Statement -> BlockStatement ExpressionStatement @@ -20,18 +19,29 @@ Declaration -> LexicalDeclaration BlockStatement -> '{' StatementList '}' ExpressionStatement -> Expression -Expression -> CastExpression - AssignmentExpression - +Expression -> CastExpression | AssignmentExpression CastExpression -> WithExpression 'as' TypeValue +AssignmentExpression -> MemberExpression '=' Expression -AssignmentExpression -> MemberExpression "Operator"? '=' Expression - -LeftHandSideExpression -> MemberExpression +LeftHandSideExpression -> PrimaryExpression + ParenthesizedExpression + ComplexLiteral + MemberExpression CallExpression -CallExpression -> MemberExpression Arguments (Arguments | '[' Expression ']' | '.' 'Ident')* -MemberExpression -> "Ident" ('[' Expression ']' | '.' 'Ident')* +ParenthesizedExpression -> '(' Expression ')' + +ComplexLiteral -> ObjectLiteral | ArrayLiteral + +ObjectLiteral -> '{' PropertyDefinitionList '}' +PropertyDefinitionList -> (FieldProperty ';')* +FieldProperty -> "Ident" ':' Expression + +ArrayLiteral -> '[' ElementList ']' +ElementList -> (Expression ',')* + +CallExpression -> MemberExpression Arguments +MemberExpression -> Var ('[' Expression ']' | '.' 'Ident')* Arguments -> '(' (Expression ',')* ')' WithExpression -> ConditionalExpression 'with' ObjectLiteral @@ -44,19 +54,14 @@ AddExpression -> MulExpression (('+'|'-') MulExpression)* MulExpression -> UnaryExpression (('*'|'/'|'%'|'++'|'::') UnaryExpression)* UnaryExpression -> LeftHandSideExpression | ('-'|'!'|'~') UnaryExpression -PrimaryExpression -> "Ident" | EnvVar | Literal | '(' Expression ')' | ObjectLiteral | ArrayLiteral +PrimaryExpression -> Var | Literal +Var -> "Ident" | EnvVar EnvVar -> '$' "Ident" Literal -> "NullLiteral" "IntegerLiteral" "FloatLiteral" "StringLiteral" "BooleanLiteral" -ObjectLiteral -> '{' PropertyDefinitionList '}' -PropertyDefinitionList -> (FieldProperty ';')* -FieldProperty -> "Ident" ':' Expression - -ArrayLiteral -> '[' ElementList ']' -ElementList -> (Expression ',')* IfStatement -> 'if' '(' Expression ')' Statement ('else' Statement)? From 9515f333833d20e87e0850b6d30327d6e7decc91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D0=BD=D0=B8=D0=BD=20=D0=A1=D1=82=D0=B5=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B8=D1=87?= Date: Fri, 22 May 2026 01:19:13 +0300 Subject: [PATCH 09/20] refactor(TopDownParser): Expect calls Specify value of expected tokens for certain types --- .../Parser/Impl/TopDownParser.Declaration.cs | 8 ++++---- .../Parser/Impl/TopDownParser.Expression.cs | 2 +- .../Parser/Impl/TopDownParser.Statement.cs | 2 +- src/Domain/HydraScript.Domain.FrontEnd/Parser/grammar.txt | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Declaration.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Declaration.cs index f9b2a538..6ea45923 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Declaration.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Declaration.cs @@ -55,7 +55,7 @@ private FunctionDeclaration FunctionDeclaration() } else if (CurrentIs("Assign")) { - Expect("Assign"); + Expect("Assign", "="); var value = LiteralNode(); indexOfFirstDefaultArgument = args.Count < indexOfFirstDefaultArgument ? args.Count @@ -114,7 +114,7 @@ private AssignmentExpression DeclarationAssignmentExpression() if (CurrentIs("Assign")) { - var assignSegment = Expect("Assign").Segment; + var assignSegment = Expect("Assign", "=").Segment; return new AssignmentExpression( new MemberExpression(identRef), Expression()) { Segment = assignSegment }; @@ -124,7 +124,7 @@ private AssignmentExpression DeclarationAssignmentExpression() { Expect("Colon"); var type = TypeValue(); - var assignSegment = CurrentIs("Assign") ? Expect("Assign").Segment : string.Empty; + var assignSegment = CurrentIs("Assign") ? Expect("Assign", "=").Segment : string.Empty; var expression = assignSegment is not "" ? Expression() : new ImplicitLiteral(type); return new AssignmentExpression( new MemberExpression(identRef), expression, type) @@ -141,7 +141,7 @@ private TypeDeclaration TypeDeclaration() { var typeWord = Expect("Keyword", "type"); var ident = Expect("Ident"); - Expect("Assign"); + Expect("Assign", "="); var type = TypeValue(); var typeId = new IdentifierReference(name: ident.Value) diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs index ca5dc3e9..e8c26124 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs @@ -11,7 +11,7 @@ public partial class TopDownParser { /// /// Expression -> CastExpression | AssignmentExpression - /// AssignmentExpression -> MemberExpression '=' Expression + /// AssignmentExpression -> MemberExpression "Assign" Expression /// private Expression Expression() { diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Statement.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Statement.cs index fb846e4c..d68ca9a9 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Statement.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Statement.cs @@ -141,7 +141,7 @@ private InputStatement InputStatement() var input = Expect("Input"); if (CurrentIsOperator("$")) { - var dollar = Expect("Operator"); + var dollar = Expect("Operator", "$"); var envIdent = Expect("Ident"); return new InputStatement( new EnvVarReference(envIdent.Value) diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/grammar.txt b/src/Domain/HydraScript.Domain.FrontEnd/Parser/grammar.txt index 49798048..cc36cb71 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/grammar.txt +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/grammar.txt @@ -21,7 +21,7 @@ BlockStatement -> '{' StatementList '}' ExpressionStatement -> Expression Expression -> CastExpression | AssignmentExpression CastExpression -> WithExpression 'as' TypeValue -AssignmentExpression -> MemberExpression '=' Expression +AssignmentExpression -> MemberExpression "Assign" Expression LeftHandSideExpression -> PrimaryExpression ParenthesizedExpression From 51a0672f96af64accffafa99398b2eeac1447d6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D0=BD=D0=B8=D0=BD=20=D0=A1=D1=82=D0=B5=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B8=D1=87?= Date: Fri, 22 May 2026 13:34:07 +0300 Subject: [PATCH 10/20] feat(FrontEnd): RegexLexer - Updated `TokenTypes` to separate `Assign` and `Operator` patterns, improving token categorization. - Added `Distinct()` to `TokenTypesProvider` for unique token handling. - Adjusted `GeneratedRegexContainer` to include `ExplicitCapture` for better regex performance. Closes #233 --- .../TokenTypes.cs | 24 +++++++++++++------ .../Lexer/Impl/TokenTypesProvider.cs | 1 + .../GeneratedRegexContainer.cs | 4 +++- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/Domain/HydraScript.Domain.Constants/TokenTypes.cs b/src/Domain/HydraScript.Domain.Constants/TokenTypes.cs index 001a91c3..2afab1b6 100644 --- a/src/Domain/HydraScript.Domain.Constants/TokenTypes.cs +++ b/src/Domain/HydraScript.Domain.Constants/TokenTypes.cs @@ -71,11 +71,26 @@ public static IEnumerable Stream Tag: "Input", Pattern: "[<]{3}", Priority: 13); + + yield return new( + Tag: "Assign", + Pattern: "[+]{1,2}[=]|[-][=]|[*][=]|[/][=]|[%][=]|[|][|][=]|[&][&][=]", + Priority: 14); yield return new( Tag: "Operator", - Pattern: "[+]{1,2}|[-]|[*]|[/]|[%]|([!]|[=])[=]|([<]|[>])[=]?|[!]|[|]{2}|[&]{2}|[~]|[:]{2}|[$]", - Priority: 14); + Pattern: "[=][=]|[!][=]|[<][=]|[>][=]", + Priority: 15); + + yield return new( + Tag: "Assign", + Pattern: "[=]", + Priority: 16); + + yield return new( + Tag: "Operator", + Pattern: "[+][+]|[-]|[+*/%<>!~$]|[|][|]|[&][&]|[:][:]", + Priority: 17); yield return new( Tag: "Comma", @@ -117,11 +132,6 @@ public static IEnumerable Stream Pattern: "[]]", Priority: 109); - yield return new( - Tag: "Assign", - Pattern: "[=]", - Priority: 99); - yield return new( Tag: "QuestionMark", Pattern: "[?]", diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Lexer/Impl/TokenTypesProvider.cs b/src/Domain/HydraScript.Domain.FrontEnd/Lexer/Impl/TokenTypesProvider.cs index 4e15a503..0671671f 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Lexer/Impl/TokenTypesProvider.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Lexer/Impl/TokenTypesProvider.cs @@ -12,6 +12,7 @@ public FrozenDictionary GetTokenTypes() => .Select(x => x.CanIgnore ? new IgnorableType(x.Tag) : new TokenType(x.Tag)) + .Distinct() .Concat([new EndOfProgramType(), new ErrorType()]) .ToFrozenDictionary(x => x.Tag); } \ No newline at end of file diff --git a/src/Infrastructure/HydraScript.Infrastructure/GeneratedRegexContainer.cs b/src/Infrastructure/HydraScript.Infrastructure/GeneratedRegexContainer.cs index 0446b5e7..93f2f51d 100644 --- a/src/Infrastructure/HydraScript.Infrastructure/GeneratedRegexContainer.cs +++ b/src/Infrastructure/HydraScript.Infrastructure/GeneratedRegexContainer.cs @@ -5,6 +5,8 @@ namespace HydraScript.Infrastructure; public sealed partial class GeneratedRegexContainer : IGeneratedRegexContainer { - [GeneratedRegex(PatternContainer.Value, RegexOptions.Compiled)] + [GeneratedRegex( + PatternContainer.Value, + options: RegexOptions.Compiled | RegexOptions.ExplicitCapture)] public static partial Regex Regex { get; } } \ No newline at end of file From 876d00e6d0afe2096fab6fca4c5a497ed5124a64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D0=BD=D0=B8=D0=BD=20=D0=A1=D1=82=D0=B5=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B8=D1=87?= Date: Fri, 22 May 2026 15:35:58 +0300 Subject: [PATCH 11/20] refactor: fix --- .../Lexer/Impl/TextCoordinateSystemComputer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Lexer/Impl/TextCoordinateSystemComputer.cs b/src/Domain/HydraScript.Domain.FrontEnd/Lexer/Impl/TextCoordinateSystemComputer.cs index 07cde58b..90660c4e 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Lexer/Impl/TextCoordinateSystemComputer.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Lexer/Impl/TextCoordinateSystemComputer.cs @@ -4,7 +4,7 @@ namespace HydraScript.Domain.FrontEnd.Lexer.Impl; public class TextCoordinateSystemComputer : ITextCoordinateSystemComputer { - private readonly SearchValues _sv = SearchValues.Create(['\n']); + private readonly SearchValues _sv = SearchValues.Create('\n'); /// public IReadOnlyList GetLines(string text) From 2d865680e7d67c61024e14b00ffddbb9016f4bc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D0=BD=D0=B8=D0=BD=20=D0=A1=D1=82=D0=B5=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B8=D1=87?= Date: Fri, 22 May 2026 15:38:20 +0300 Subject: [PATCH 12/20] refactor: fix --- .../Impl/Ast/Nodes/Expressions/PrimaryExpressions/ValueDto.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/PrimaryExpressions/ValueDto.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/PrimaryExpressions/ValueDto.cs index 655e09c9..e1abc377 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/PrimaryExpressions/ValueDto.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/PrimaryExpressions/ValueDto.cs @@ -14,7 +14,7 @@ public static ValueDto NameDto(string name) => public static ValueDto EnvDto(string name) => new(ValueDtoType.Env, name, Value: null, Label: null); -}; +} public enum ValueDtoType { From 90df2931bf8973ff962638daf4814ba45ed7ac95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D0=BD=D0=B8=D0=BD=20=D0=A1=D1=82=D0=B5=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B8=D1=87?= Date: Fri, 22 May 2026 15:40:16 +0300 Subject: [PATCH 13/20] refactor: lhs empty --- .../Visitors/ExpressionInstructionProvider.cs | 2 +- .../Parser/Impl/Ast/Nodes/Expressions/CallExpression.cs | 2 -- .../Parser/Impl/Ast/Nodes/Expressions/LeftHandSideExpression.cs | 2 -- .../Parser/Impl/Ast/Nodes/Expressions/MemberExpression.cs | 2 +- 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Application/HydraScript.Application.CodeGeneration/Visitors/ExpressionInstructionProvider.cs b/src/Application/HydraScript.Application.CodeGeneration/Visitors/ExpressionInstructionProvider.cs index a34b5ea0..fce4fcec 100644 --- a/src/Application/HydraScript.Application.CodeGeneration/Visitors/ExpressionInstructionProvider.cs +++ b/src/Application/HydraScript.Application.CodeGeneration/Visitors/ExpressionInstructionProvider.cs @@ -317,7 +317,7 @@ public AddressedInstructions Visit(CallExpression visitable) if (visitable.IsEmptyCall) return []; - var methodCall = !visitable.Empty(); + var methodCall = !visitable.Member.Empty(); AddressedInstructions result = []; if (methodCall) diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/CallExpression.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/CallExpression.cs index f92e3081..9e6e6b52 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/CallExpression.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/CallExpression.cs @@ -30,7 +30,5 @@ public CallExpression(MemberExpression member, List expressions) public override IdentifierReference Id => Member.Id; - public override bool Empty() => Member.Empty(); - protected override string NodeRepresentation() => "()"; } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/LeftHandSideExpression.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/LeftHandSideExpression.cs index f15d9497..63dd3be1 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/LeftHandSideExpression.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/LeftHandSideExpression.cs @@ -5,6 +5,4 @@ namespace HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Expressions; public abstract class LeftHandSideExpression : Expression { public abstract IdentifierReference Id { get; } - - public abstract bool Empty(); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/MemberExpression.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/MemberExpression.cs index b298cdf6..07decfb6 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/MemberExpression.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/MemberExpression.cs @@ -35,7 +35,7 @@ public MemberExpression( public override IdentifierReference Id => _identifierReference; - public override bool Empty() => AccessChain is null; + public bool Empty() => AccessChain.Count == 0; protected override string NodeRepresentation() => nameof(MemberExpression); } \ No newline at end of file From a45a29f51b02e5c325dca1a32010f9b2e782adca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D0=BD=D0=B8=D0=BD=20=D0=A1=D1=82=D0=B5=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B8=D1=87?= Date: Fri, 22 May 2026 15:51:02 +0300 Subject: [PATCH 14/20] refactor(AST): Node cloning --- .../Parser/Impl/Ast/AbstractSyntaxTreeNode.cs | 8 ++++---- .../AfterTypesAreLoaded/FunctionDeclaration.cs | 8 ++++++++ .../AfterTypesAreLoaded/IFunctionArgument.cs | 12 ++++++++++++ .../AfterTypesAreLoaded/LexicalDeclaration.cs | 11 +++++++++++ .../Ast/Nodes/Declarations/TypeDeclaration.cs | 2 ++ .../Impl/Ast/Nodes/Declarations/TypeValue.cs | 17 +++++++++++++++++ .../Nodes/Expressions/AssignmentExpression.cs | 3 +++ .../Ast/Nodes/Expressions/BinaryExpression.cs | 2 ++ .../Ast/Nodes/Expressions/CallExpression.cs | 3 +++ .../Ast/Nodes/Expressions/CastAsExpression.cs | 2 ++ .../Expressions/ComplexLiterals/ArrayLiteral.cs | 3 +++ .../ComplexLiterals/ObjectLiteral.cs | 7 +++++-- .../Expressions/ComplexLiterals/Property.cs | 2 ++ .../Nodes/Expressions/ConditionalExpression.cs | 3 +++ .../Impl/Ast/Nodes/Expressions/Expression.cs | 2 ++ .../PrimaryExpressions/EnvVarReference.cs | 2 ++ .../PrimaryExpressions/IdentifierReference.cs | 2 ++ .../PrimaryExpressions/ImplicitLiteral.cs | 6 ++++-- .../Expressions/PrimaryExpressions/Literal.cs | 4 +++- .../Ast/Nodes/Expressions/UnaryExpression.cs | 2 ++ .../Ast/Nodes/Expressions/WithExpression.cs | 3 +++ .../Parser/Impl/Ast/Nodes/ScriptBody.cs | 3 +++ .../Parser/Impl/Ast/Nodes/StatementListItem.cs | 15 ++++++++++++--- .../Impl/Ast/Nodes/Statements/BlockStatement.cs | 3 +++ .../Ast/Nodes/Statements/ExpressionStatement.cs | 2 ++ .../Impl/Ast/Nodes/Statements/IfStatement.cs | 3 +++ .../Impl/Ast/Nodes/Statements/InputStatement.cs | 2 ++ .../Ast/Nodes/Statements/InsideStatementJump.cs | 2 ++ .../Ast/Nodes/Statements/OutputStatement.cs | 2 ++ .../Ast/Nodes/Statements/ReturnStatement.cs | 2 ++ .../Impl/Ast/Nodes/Statements/WhileStatement.cs | 2 ++ tests/coverage-exclude.xml | 2 ++ 32 files changed, 130 insertions(+), 12 deletions(-) diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/AbstractSyntaxTreeNode.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/AbstractSyntaxTreeNode.cs index be303205..9dc8660a 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/AbstractSyntaxTreeNode.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/AbstractSyntaxTreeNode.cs @@ -36,6 +36,8 @@ IEnumerator IEnumerable.GetEnumerator() => public IReadOnlyList GetAllNodes() => new TraverseEnumerator(this).AsValueEnumerable().ToArray(); + public abstract IAbstractSyntaxTreeNode Clone(); + /// /// Метод возвращает true, если узел - потомок заданного типа и выполняется заданное условие.
/// В случае, когда условие не задано, проверяется просто соответствие типов. @@ -45,13 +47,11 @@ public IReadOnlyList GetAllNodes() => public bool ChildOf(Predicate? condition = null) where T : IAbstractSyntaxTreeNode { var parent = Parent; - while (parent != default!) + while (parent != null!) { if (parent is T node) { - return condition is not null - ? condition(node) - : true; + return condition?.Invoke(node) ?? true; } parent = parent.Parent; diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/FunctionDeclaration.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/FunctionDeclaration.cs index aff96ccb..7422afc7 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/FunctionDeclaration.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/FunctionDeclaration.cs @@ -56,4 +56,12 @@ public override void InitScope(Scope? scope = null) protected override string NodeRepresentation() => ZString.Concat("function", ' ', Name); + + public override FunctionDeclaration Clone() => + new( + Name.Clone(), + ReturnTypeValue.DeepClone(), + _arguments.Select(x => x.DeepClone()).ToList(), + Statements.Clone(), + IndexOfFirstDefaultArgument); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/IFunctionArgument.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/IFunctionArgument.cs index 5a37e3fd..c54503fa 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/IFunctionArgument.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/IFunctionArgument.cs @@ -9,6 +9,8 @@ public interface IFunctionArgument public TypeValue TypeValue { get; } public ValueDto Info { get; } + + public IFunctionArgument DeepClone(); } public record NamedArgument( @@ -19,6 +21,11 @@ public override string ToString() => $"{Name}: {TypeValue}"; public ValueDto Info { get; } = ValueDto.NameDto(Name); + + public IFunctionArgument DeepClone() => this with + { + TypeValue = TypeValue.DeepClone() + }; } public record DefaultValueArgument : IFunctionArgument @@ -36,6 +43,11 @@ public DefaultValueArgument(string name, Literal literal) public ValueDto Info { get; } + public IFunctionArgument DeepClone() => + new DefaultValueArgument( + Name, + new Literal(TypeValue.DeepClone(), Info.Value, label: Info.Label)); + public override string ToString() => $"{Name} = {Info.Label}"; } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/LexicalDeclaration.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/LexicalDeclaration.cs index bd0c65d0..cdeb8f55 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/LexicalDeclaration.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/LexicalDeclaration.cs @@ -21,4 +21,15 @@ public void AddAssignment(AssignmentExpression assignment) protected override string NodeRepresentation() => ReadOnly ? "const" : "let"; + + public override LexicalDeclaration Clone() + { + var clone = new LexicalDeclaration(ReadOnly); + for (var i = 0; i < _assignments.Count; i++) + { + clone.AddAssignment(_assignments[i].Clone()); + } + + return clone; + } } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/TypeDeclaration.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/TypeDeclaration.cs index e4c7cbfb..a226e7cc 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/TypeDeclaration.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/TypeDeclaration.cs @@ -17,4 +17,6 @@ public override void InitScope(Scope? scope = null) protected override string NodeRepresentation() => $"type {TypeId.Name} = {TypeValue}"; + + public override TypeDeclaration Clone() => new(TypeId.Clone(), TypeValue.DeepClone()); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/TypeValue.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/TypeValue.cs index 781bd7ce..d15260db 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/TypeValue.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/TypeValue.cs @@ -6,6 +6,7 @@ public abstract record TypeValue : IVisitable { public Scope Scope { get; set; } = null!; public abstract TReturn Accept(IVisitor visitor); + public abstract TypeValue DeepClone(); } [AutoVisitable] @@ -18,18 +19,24 @@ public partial record TypeIdentValue(IdentifierReference TypeId) : TypeValue public static TypeIdentValue Undefined => new(new IdentifierReference("undefined")); public override string ToString() => TypeId; + + public override TypeValue DeepClone() => new TypeIdentValue(TypeId.Clone()); } [AutoVisitable] public partial record ArrayTypeValue(TypeValue TypeValue) : TypeValue { public override string ToString() => $"{TypeValue}[]"; + + public override TypeValue DeepClone() => new ArrayTypeValue(TypeValue.DeepClone()); } [AutoVisitable] public partial record NullableTypeValue(TypeValue TypeValue) : TypeValue { public override string ToString() => $"{TypeValue}?"; + + public override TypeValue DeepClone() => new NullableTypeValue(TypeValue.DeepClone()); } public record PropertyTypeValue( @@ -38,10 +45,20 @@ public record PropertyTypeValue( { public override string ToString() => $"{Key}: {TypeValue}"; + + public PropertyTypeValue DeepClone() => this with + { + TypeValue = TypeValue.DeepClone() + }; } [AutoVisitable] public partial record ObjectTypeValue(List Properties) : TypeValue { public override string ToString() => $"{{{string.Join(';', Properties)}}}"; + + public override TypeValue DeepClone() => new ObjectTypeValue( + Properties + .Select(p => p.DeepClone()) + .ToList()); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/AssignmentExpression.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/AssignmentExpression.cs index 4717e085..d1d9e2f5 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/AssignmentExpression.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/AssignmentExpression.cs @@ -35,4 +35,7 @@ public override void InitScope(Scope? scope = null) } protected override string NodeRepresentation() => "="; + + public override AssignmentExpression Clone() => + new (Destination.Clone(), Source.Clone(), DestinationType); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/BinaryExpression.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/BinaryExpression.cs index 541cde20..5b3b7c6d 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/BinaryExpression.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/BinaryExpression.cs @@ -23,4 +23,6 @@ public BinaryExpression(Expression left, string @operator, Expression right) } protected override string NodeRepresentation() => Operator; + + public override BinaryExpression Clone() => new(Left.Clone(), Operator, Right.Clone()); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/CallExpression.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/CallExpression.cs index 9e6e6b52..db1e7bc1 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/CallExpression.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/CallExpression.cs @@ -31,4 +31,7 @@ public CallExpression(MemberExpression member, List expressions) public override IdentifierReference Id => Member.Id; protected override string NodeRepresentation() => "()"; + + public override CallExpression Clone() => + new(Member.Clone(), _parameters.Select(x => x.Clone()).ToList()); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/CastAsExpression.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/CastAsExpression.cs index 8aca5171..366daf93 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/CastAsExpression.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/CastAsExpression.cs @@ -30,6 +30,8 @@ public override void InitScope(Scope? scope = null) protected override string NodeRepresentation() => $"as {Cast}"; + public override CastAsExpression Clone() => new(Expression.Clone(), Cast); + public enum DestinationType { Undefined, diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/ComplexLiterals/ArrayLiteral.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/ComplexLiterals/ArrayLiteral.cs index 2aeb7f64..cbb5b132 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/ComplexLiterals/ArrayLiteral.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/ComplexLiterals/ArrayLiteral.cs @@ -25,4 +25,7 @@ public ArrayLiteral(List expressions) } protected override string NodeRepresentation() => "[]"; + + public override ArrayLiteral Clone() => + new(_expressions.Select(x => x.Clone()).ToList()); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/ComplexLiterals/ObjectLiteral.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/ComplexLiterals/ObjectLiteral.cs index fd43ece9..0de9c8e4 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/ComplexLiterals/ObjectLiteral.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/ComplexLiterals/ObjectLiteral.cs @@ -18,10 +18,10 @@ public override IdentifierReference Id { get { - if (Parent is AssignmentExpression assignment) + if (Parent is AssignmentExpression assignment) return assignment.Destination.Id; - if (Parent is WithExpression{Parent:AssignmentExpression withAssignment}) + if (Parent is WithExpression { Parent: AssignmentExpression withAssignment }) return withAssignment.Destination.Id; return new(NullId); @@ -44,4 +44,7 @@ public override void InitScope(Scope? scope = null) } protected override string NodeRepresentation() => "{}"; + + public override ObjectLiteral Clone() => + new(_properties.Select(x => x.Clone()).ToList()); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/ComplexLiterals/Property.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/ComplexLiterals/Property.cs index 5b85ce3d..76c5d282 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/ComplexLiterals/Property.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/ComplexLiterals/Property.cs @@ -31,4 +31,6 @@ public void Deconstruct(out string id, out Expression expr) } protected override string NodeRepresentation() => ":"; + + public override Property Clone() => new(Id.Clone(), Expression.Clone()); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/ConditionalExpression.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/ConditionalExpression.cs index fc5202e1..4013176f 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/ConditionalExpression.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/ConditionalExpression.cs @@ -23,4 +23,7 @@ public ConditionalExpression(Expression test, Expression consequent, Expression } protected override string NodeRepresentation() => "?:"; + + public override ConditionalExpression Clone() => + new(Test.Clone(), Consequent.Clone(), Alternate.Clone()); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/Expression.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/Expression.cs index a3766257..9ab3884e 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/Expression.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/Expression.cs @@ -2,6 +2,8 @@ namespace HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Expressions; public abstract class Expression : AbstractSyntaxTreeNode { + public abstract override Expression Clone(); + public abstract override TReturn Accept( IVisitor visitor); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/PrimaryExpressions/EnvVarReference.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/PrimaryExpressions/EnvVarReference.cs index 91ed31a2..16a1eee3 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/PrimaryExpressions/EnvVarReference.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/PrimaryExpressions/EnvVarReference.cs @@ -7,6 +7,8 @@ public partial class EnvVarReference(string name) : IdentifierReference(name) { protected override string NodeRepresentation() => ZString.Concat('$', Name); + public override EnvVarReference Clone() => new(Name); + public override ValueDto ToValueDto() => ValueDto.EnvDto(Name); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/PrimaryExpressions/IdentifierReference.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/PrimaryExpressions/IdentifierReference.cs index 407577f6..5c2ff913 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/PrimaryExpressions/IdentifierReference.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/PrimaryExpressions/IdentifierReference.cs @@ -7,6 +7,8 @@ public partial class IdentifierReference(string name) : PrimaryExpression protected override string NodeRepresentation() => Name; + public override IdentifierReference Clone() => new(Name); + public override ValueDto ToValueDto() => ValueDto.NameDto(Name); diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/PrimaryExpressions/ImplicitLiteral.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/PrimaryExpressions/ImplicitLiteral.cs index 3582930f..8ff1f60a 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/PrimaryExpressions/ImplicitLiteral.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/PrimaryExpressions/ImplicitLiteral.cs @@ -12,12 +12,14 @@ public partial class ImplicitLiteral(TypeValue type) : AbstractLiteral(type) TypeIdentValue { TypeId.Name: "boolean" } => false, TypeIdentValue { TypeId.Name: "null" } or NullableTypeValue => null, ArrayTypeValue => new List(), - _ => new Undefined() + _ => default(Undefined) }; protected override string NodeRepresentation() => $"Implicit {Type}"; + public override ImplicitLiteral Clone() => new(Type.DeepClone()); + public void SetValue(object? value) => _defaultValue = value; public bool IsDefined => _defaultValue is not Undefined; @@ -29,5 +31,5 @@ _defaultValue is null ? "null" : _defaultValue.ToString()!); - private sealed class Undefined; + private readonly struct Undefined; } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/PrimaryExpressions/Literal.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/PrimaryExpressions/Literal.cs index 68d169a0..4b703f59 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/PrimaryExpressions/Literal.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/PrimaryExpressions/Literal.cs @@ -11,7 +11,7 @@ public partial class Literal : AbstractLiteral public Literal( TypeValue type, object? value, - string segment, + string segment = "", string? label = null) : base(type) { _label = (label ?? value?.ToString())!; @@ -21,6 +21,8 @@ public Literal( protected override string NodeRepresentation() => _label; + public override Literal Clone() => new(Type.DeepClone(), _value, Segment, _label); + public override ValueDto ToValueDto() => ValueDto.ConstantDto(_value, _label); diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/UnaryExpression.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/UnaryExpression.cs index 76ed1059..66dd6f9b 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/UnaryExpression.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/UnaryExpression.cs @@ -19,4 +19,6 @@ public UnaryExpression(string @operator, Expression expression) } protected override string NodeRepresentation() => Operator; + + public override UnaryExpression Clone() => new(Operator, Expression.Clone()); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/WithExpression.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/WithExpression.cs index 5fd50c11..6d10e9b0 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/WithExpression.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/WithExpression.cs @@ -24,4 +24,7 @@ public WithExpression(Expression expression, ObjectLiteral objectLiteral) } protected override string NodeRepresentation() => "with"; + + public override WithExpression Clone() => + new(Expression.Clone(), ObjectLiteral.Clone()); } diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/ScriptBody.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/ScriptBody.cs index 2c179707..49c8a760 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/ScriptBody.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/ScriptBody.cs @@ -23,4 +23,7 @@ public override void InitScope(Scope? scope = null) } protected override string NodeRepresentation() => "Script"; + + public override ScriptBody Clone() => + new(_statementList.Select(x => x.Clone()).ToList()); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/StatementListItem.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/StatementListItem.cs index e48633da..e58ae446 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/StatementListItem.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/StatementListItem.cs @@ -1,10 +1,19 @@ namespace HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes; public abstract class StatementListItem : - AbstractSyntaxTreeNode; + AbstractSyntaxTreeNode +{ + public abstract override StatementListItem Clone(); +} public abstract class Statement : - StatementListItem; + StatementListItem +{ + public abstract override Statement Clone(); +} public abstract class Declaration : - StatementListItem; \ No newline at end of file + StatementListItem +{ + public abstract override Declaration Clone(); +} \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/BlockStatement.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/BlockStatement.cs index 36bf5975..91e4e4fb 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/BlockStatement.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/BlockStatement.cs @@ -24,4 +24,7 @@ public override void InitScope(Scope? scope = null) } protected override string NodeRepresentation() => "{}"; + + public override BlockStatement Clone() => + new(_statementList.Select(x => x.Clone()).ToList()); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/ExpressionStatement.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/ExpressionStatement.cs index 0d112cdf..86c1c18b 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/ExpressionStatement.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/ExpressionStatement.cs @@ -16,4 +16,6 @@ public ExpressionStatement(Expression expression) } protected override string NodeRepresentation() => nameof(ExpressionStatement); + + public override ExpressionStatement Clone() => new(Expression.Clone()); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/IfStatement.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/IfStatement.cs index 800415f6..3a624ae3 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/IfStatement.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/IfStatement.cs @@ -32,4 +32,7 @@ public IfStatement(Expression test, Statement then, Statement? @else = null) public bool HasElseBlock() => Else is { Count: > 0 }; protected override string NodeRepresentation() => "if"; + + public override IfStatement Clone() => + new(Test.Clone(), Then.Clone(), Else?.Clone()); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/InputStatement.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/InputStatement.cs index 178e7b0f..a37b2732 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/InputStatement.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/InputStatement.cs @@ -18,4 +18,6 @@ public InputStatement(IdentifierReference destination) } protected override string NodeRepresentation() => "input"; + + public override InputStatement Clone() => new(Destination.Clone()); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/InsideStatementJump.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/InsideStatementJump.cs index bf7ebce0..87e0a878 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/InsideStatementJump.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/InsideStatementJump.cs @@ -6,4 +6,6 @@ public partial class InsideStatementJump(string keyword) : Statement public string Keyword { get; } = keyword; protected override string NodeRepresentation() => Keyword; + + public override InsideStatementJump Clone() => new(Keyword); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/OutputStatement.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/OutputStatement.cs index 4a38d162..6cd8bc79 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/OutputStatement.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/OutputStatement.cs @@ -16,4 +16,6 @@ public OutputStatement(Expression expression) } protected override string NodeRepresentation() => "output"; + + public override OutputStatement Clone() => new(Expression.Clone()); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/ReturnStatement.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/ReturnStatement.cs index b8764458..e389993c 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/ReturnStatement.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/ReturnStatement.cs @@ -16,4 +16,6 @@ public ReturnStatement(Expression? expression = null) } protected override string NodeRepresentation() => "return"; + + public override ReturnStatement Clone() => new(Expression?.Clone()); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/WhileStatement.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/WhileStatement.cs index cffb4a4d..f730a5a1 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/WhileStatement.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/WhileStatement.cs @@ -20,4 +20,6 @@ public WhileStatement(Expression condition, Statement statement) } protected override string NodeRepresentation() => "while"; + + public override WhileStatement Clone() => new(Condition.Clone(), Statement.Clone()); } \ No newline at end of file diff --git a/tests/coverage-exclude.xml b/tests/coverage-exclude.xml index d5160dda..9af13767 100644 --- a/tests/coverage-exclude.xml +++ b/tests/coverage-exclude.xml @@ -7,6 +7,8 @@ .*ToString.* .*GetHashCode.* .*GetEnumerator.* + .*Clone.* + .*DeepClone.* From e86690d5f962efe3dc474abec420cbfaf4e3bb11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D0=BD=D0=B8=D0=BD=20=D0=A1=D1=82=D0=B5=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B8=D1=87?= Date: Fri, 22 May 2026 17:59:05 +0300 Subject: [PATCH 15/20] refactor(AST): handle null parent scopes gracefully Standardized null safety for parent scopes across AST nodes, replacing direct access with conditional checks and fallback to `Scope.Empty`. --- .../Visitors/DeclarationVisitor.cs | 2 +- .../Parser/IAbstractSyntaxTreeNode.cs | 2 +- .../Parser/Impl/Ast/AbstractSyntaxTreeNode.cs | 8 ++++---- .../AfterTypesAreLoaded/FunctionDeclaration.cs | 6 +++--- .../Expressions/ComplexLiterals/ObjectLiteral.cs | 2 +- .../PrimaryExpressions/AbstractLiteral.cs | 2 +- .../Impl/Ast/Nodes/Statements/BlockStatement.cs | 2 +- .../HydraScript.Domain.FrontEnd/Parser/Scope.cs | 16 ++++++++++++++-- 8 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/Application/HydraScript.Application.StaticAnalysis/Visitors/DeclarationVisitor.cs b/src/Application/HydraScript.Application.StaticAnalysis/Visitors/DeclarationVisitor.cs index e929bc79..d9f40127 100644 --- a/src/Application/HydraScript.Application.StaticAnalysis/Visitors/DeclarationVisitor.cs +++ b/src/Application/HydraScript.Application.StaticAnalysis/Visitors/DeclarationVisitor.cs @@ -79,7 +79,7 @@ public VisitUnit Visit(FunctionDeclaration visitable) visitable.ReturnStatements = returnAnalyzerResult.ReturnStatements; visitable.AllCodePathsEndedWithReturn = returnAnalyzerResult.CodePathEndedWithReturn; - var parentTable = _symbolTables[visitable.Parent.Scope]; + var parentTable = _symbolTables[visitable.Parent?.Scope ?? Scope.Empty]; var parameters = new List(); for (var i = 0; i < visitable.Arguments.Count; i++) diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/IAbstractSyntaxTreeNode.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/IAbstractSyntaxTreeNode.cs index ee59c67a..c303b1de 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/IAbstractSyntaxTreeNode.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/IAbstractSyntaxTreeNode.cs @@ -4,7 +4,7 @@ public interface IAbstractSyntaxTreeNode : IReadOnlyList, IVisitable { - public IAbstractSyntaxTreeNode Parent { get; } + public IAbstractSyntaxTreeNode? Parent { get; } public Scope Scope { get; } public void InitScope(Scope? scope = null); public IReadOnlyList GetAllNodes(); diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/AbstractSyntaxTreeNode.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/AbstractSyntaxTreeNode.cs index 9dc8660a..b14e6686 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/AbstractSyntaxTreeNode.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/AbstractSyntaxTreeNode.cs @@ -5,9 +5,9 @@ namespace HydraScript.Domain.FrontEnd.Parser.Impl.Ast; public abstract class AbstractSyntaxTreeNode : IAbstractSyntaxTreeNode { - public IAbstractSyntaxTreeNode Parent { get; set; } = null!; + public IAbstractSyntaxTreeNode? Parent { get; internal set; } - public Scope Scope { get; protected set; } = null!; + public Scope Scope { get; protected set; } = Scope.Empty; /// Базовая стратегия - инициализация через родительский узел /// Обязательно null @@ -15,7 +15,7 @@ public virtual void InitScope(Scope? scope = null) { if (scope is not null) throw new ArgumentException("'scope' must be null"); - Scope = Parent.Scope; + Scope = Parent?.Scope ?? Scope.Empty; } public string Segment { get; init; } = string.Empty; @@ -47,7 +47,7 @@ public IReadOnlyList GetAllNodes() => public bool ChildOf(Predicate? condition = null) where T : IAbstractSyntaxTreeNode { var parent = Parent; - while (parent != null!) + while (parent != null) { if (parent is T node) { diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/FunctionDeclaration.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/FunctionDeclaration.cs index 7422afc7..8e01be68 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/FunctionDeclaration.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/FunctionDeclaration.cs @@ -48,10 +48,10 @@ public override void InitScope(Scope? scope = null) { ArgumentNullException.ThrowIfNull(scope); Scope = scope; - Scope.AddOpenScope(Parent.Scope); + Scope.AddOpenScope(Parent?.Scope ?? Scope.Empty); - _arguments.ForEach(x => x.TypeValue.Scope = Parent.Scope); - ReturnTypeValue.Scope = Parent.Scope; + _arguments.ForEach(x => x.TypeValue.Scope = Parent?.Scope ?? Scope.Empty); + ReturnTypeValue.Scope = Parent?.Scope ?? Scope.Empty; } protected override string NodeRepresentation() => diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/ComplexLiterals/ObjectLiteral.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/ComplexLiterals/ObjectLiteral.cs index 0de9c8e4..c93defb4 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/ComplexLiterals/ObjectLiteral.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/ComplexLiterals/ObjectLiteral.cs @@ -40,7 +40,7 @@ public override void InitScope(Scope? scope = null) { ArgumentNullException.ThrowIfNull(scope); Scope = scope; - Scope.AddOpenScope(Parent.Scope); + Scope.AddOpenScope(Parent?.Scope ?? Scope.Empty); } protected override string NodeRepresentation() => "{}"; diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/PrimaryExpressions/AbstractLiteral.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/PrimaryExpressions/AbstractLiteral.cs index 198214d1..22c05716 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/PrimaryExpressions/AbstractLiteral.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/PrimaryExpressions/AbstractLiteral.cs @@ -11,6 +11,6 @@ public abstract partial class AbstractLiteral(TypeValue type) : PrimaryExpressio public override void InitScope(Scope? scope = null) { base.InitScope(scope); - Type.Scope = Parent.Scope; + Type.Scope = Parent?.Scope ?? Scope.Empty; } } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/BlockStatement.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/BlockStatement.cs index 91e4e4fb..341220d4 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/BlockStatement.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/BlockStatement.cs @@ -20,7 +20,7 @@ public override void InitScope(Scope? scope = null) { ArgumentNullException.ThrowIfNull(scope); Scope = scope; - Scope.AddOpenScope(Parent.Scope); + Scope.AddOpenScope(Parent?.Scope ?? Scope.Empty); } protected override string NodeRepresentation() => "{}"; diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Scope.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Scope.cs index 86e00d17..c0c80dce 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Scope.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Scope.cs @@ -1,12 +1,24 @@ namespace HydraScript.Domain.FrontEnd.Parser; -public record Scope +public sealed record Scope { - public Guid Id { get; } = Guid.NewGuid(); + private Scope(Guid id) + { + Id = id; + } + + public Scope() : this(Guid.NewGuid()) + { + } + + public Guid Id { get; } + public Scope? OpenScope { get; private set; } public void AddOpenScope(Scope scope) => OpenScope = scope; public override string ToString() => Id.ToString(); + + public static readonly Scope Empty = new(Guid.Empty); } \ No newline at end of file From db9886b9df75820f43fb6e3e094c6403ce6fef9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D0=BD=D0=B8=D0=BD=20=D0=A1=D1=82=D0=B5=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B8=D1=87?= Date: Fri, 22 May 2026 18:45:10 +0300 Subject: [PATCH 16/20] refactor(tests): simplify code assertion --- .../ErrorPrograms/DefaultParameterTests.cs | 6 +-- .../FunctionWithoutReturnStatementTests.cs | 3 +- .../NullAssignmentWhenUndefinedTests.cs | 3 +- .../VariableInitializationTests.cs | 3 +- .../ErrorPrograms/VoidAssignmentTests.cs | 3 +- .../SuccessPrograms/ArithmeticTests.cs | 3 +- .../SuccessPrograms/DumpOptionTests.cs | 3 +- .../SuccessPrograms/InputTests.cs | 3 +- .../SuccessfulProgramsTests.cs | 3 +- .../Domain/FrontEnd/AstNodeTests.cs | 45 ++++++++++++++++++- 10 files changed, 55 insertions(+), 20 deletions(-) diff --git a/tests/HydraScript.IntegrationTests/ErrorPrograms/DefaultParameterTests.cs b/tests/HydraScript.IntegrationTests/ErrorPrograms/DefaultParameterTests.cs index debbb34e..1cd03c18 100644 --- a/tests/HydraScript.IntegrationTests/ErrorPrograms/DefaultParameterTests.cs +++ b/tests/HydraScript.IntegrationTests/ErrorPrograms/DefaultParameterTests.cs @@ -9,8 +9,7 @@ public void DefaultParameter_PlacedBeforeNamed_HydraScriptError() { const string script = "function func(a = 1, b: boolean) { }"; using var runner = fixture.GetRunner(new TestHostFixture.Options(InMemoryScript: script)); - var code = runner.Invoke(); - code.Should().Be(Executor.ExitCodes.HydraScriptError); + runner.Invoke().Should().Be(Executor.ExitCodes.HydraScriptError); fixture.LogMessages.Should() .Contain(x => x.Contains("The argument b: boolean of function func is placed after default value argument")); @@ -29,8 +28,7 @@ function f(a = 0, b = 1, c = 2) {} """ ; using var runner = fixture.GetRunner(new TestHostFixture.Options(InMemoryScript: script)); - var code = runner.Invoke(); - code.Should().Be(Executor.ExitCodes.HydraScriptError); + runner.Invoke().Should().Be(Executor.ExitCodes.HydraScriptError); const string expectedMessage = "Candidates are:\n" + "function f(number)\n" + diff --git a/tests/HydraScript.IntegrationTests/ErrorPrograms/FunctionWithoutReturnStatementTests.cs b/tests/HydraScript.IntegrationTests/ErrorPrograms/FunctionWithoutReturnStatementTests.cs index 935e167d..0d983051 100644 --- a/tests/HydraScript.IntegrationTests/ErrorPrograms/FunctionWithoutReturnStatementTests.cs +++ b/tests/HydraScript.IntegrationTests/ErrorPrograms/FunctionWithoutReturnStatementTests.cs @@ -15,8 +15,7 @@ function f(b: boolean) { } """; using var runner = fixture.GetRunner(new TestHostFixture.Options(InMemoryScript: script)); - var code = runner.Invoke(); - code.Should().Be(Executor.ExitCodes.HydraScriptError); + runner.Invoke().Should().Be(Executor.ExitCodes.HydraScriptError); fixture.LogMessages.Should() .Contain(x => x.Contains("function with non-void return type must have a return statement")); diff --git a/tests/HydraScript.IntegrationTests/ErrorPrograms/NullAssignmentWhenUndefinedTests.cs b/tests/HydraScript.IntegrationTests/ErrorPrograms/NullAssignmentWhenUndefinedTests.cs index 79bbf6c6..b863500c 100644 --- a/tests/HydraScript.IntegrationTests/ErrorPrograms/NullAssignmentWhenUndefinedTests.cs +++ b/tests/HydraScript.IntegrationTests/ErrorPrograms/NullAssignmentWhenUndefinedTests.cs @@ -8,8 +8,7 @@ public class NullAssignmentWhenUndefinedTests(TestHostFixture fixture) : IClassF public void NullAssignment_UndefinedDestinationOrReturnType_HydraScriptError(string script) { using var runner = fixture.GetRunner(new TestHostFixture.Options(InMemoryScript: script)); - var code = runner.Invoke(); - code.Should().Be(Executor.ExitCodes.HydraScriptError); + runner.Invoke().Should().Be(Executor.ExitCodes.HydraScriptError); fixture.LogMessages.Should() .Contain(x => x.Contains("Cannot assign 'null' when type is undefined")); } diff --git a/tests/HydraScript.IntegrationTests/ErrorPrograms/VariableInitializationTests.cs b/tests/HydraScript.IntegrationTests/ErrorPrograms/VariableInitializationTests.cs index 91370c99..04cf4444 100644 --- a/tests/HydraScript.IntegrationTests/ErrorPrograms/VariableInitializationTests.cs +++ b/tests/HydraScript.IntegrationTests/ErrorPrograms/VariableInitializationTests.cs @@ -8,8 +8,7 @@ public class VariableInitializationTests(TestHostFixture fixture) : IClassFixtur public void VariableWithoutTypeDeclared_AccessedBeforeInitialization_HydraScriptError(string script) { using var runner = fixture.GetRunner(new TestHostFixture.Options(InMemoryScript: script)); - var code = runner.Invoke(); - code.Should().Be(Executor.ExitCodes.HydraScriptError); + runner.Invoke().Should().Be(Executor.ExitCodes.HydraScriptError); fixture.LogMessages.Should() .Contain(x => x.Contains("Cannot access 'x' before initialization")); } diff --git a/tests/HydraScript.IntegrationTests/ErrorPrograms/VoidAssignmentTests.cs b/tests/HydraScript.IntegrationTests/ErrorPrograms/VoidAssignmentTests.cs index 96b26e4d..c1de85b2 100644 --- a/tests/HydraScript.IntegrationTests/ErrorPrograms/VoidAssignmentTests.cs +++ b/tests/HydraScript.IntegrationTests/ErrorPrograms/VoidAssignmentTests.cs @@ -17,8 +17,7 @@ function func(b: boolean) { let x = func(true) """; using var runner = fixture.GetRunner(new TestHostFixture.Options(InMemoryScript: script)); - var code = runner.Invoke(); - code.Should().Be(Executor.ExitCodes.HydraScriptError); + runner.Invoke().Should().Be(Executor.ExitCodes.HydraScriptError); fixture.LogMessages.Should() .Contain(x => x.Contains("Cannot assign void")); } diff --git a/tests/HydraScript.IntegrationTests/SuccessPrograms/ArithmeticTests.cs b/tests/HydraScript.IntegrationTests/SuccessPrograms/ArithmeticTests.cs index f9889839..28f98cfb 100644 --- a/tests/HydraScript.IntegrationTests/SuccessPrograms/ArithmeticTests.cs +++ b/tests/HydraScript.IntegrationTests/SuccessPrograms/ArithmeticTests.cs @@ -24,8 +24,7 @@ public void Equality_AdditionToTheLeft_Success() using var runner = fixture.GetRunner( new TestHostFixture.Options( InMemoryScript: script)); - var code = runner.Invoke(); - code.Should().Be(Executor.ExitCodes.Success); + runner.Invoke().Should().Be(Executor.ExitCodes.Success); fixture.LogMessages.Should() .Contain(log => log.Contains("i is 5")); } diff --git a/tests/HydraScript.IntegrationTests/SuccessPrograms/DumpOptionTests.cs b/tests/HydraScript.IntegrationTests/SuccessPrograms/DumpOptionTests.cs index 7102853d..aec0d118 100644 --- a/tests/HydraScript.IntegrationTests/SuccessPrograms/DumpOptionTests.cs +++ b/tests/HydraScript.IntegrationTests/SuccessPrograms/DumpOptionTests.cs @@ -1,5 +1,6 @@ using System.IO.Abstractions; using HydraScript.Domain.BackEnd; +using HydraScript.Infrastructure; using Microsoft.Extensions.DependencyInjection; using NSubstitute; @@ -21,7 +22,7 @@ public void Invoke_DumpOptionPassed_FilesCreated() outputWriter.WriteLine(x.ArgAt(1)); }); - runner.Invoke(); + runner.Invoke().Should().Be(Executor.ExitCodes.Success); fileSystemMock.File.Received(1) .WriteAllText( Arg.Is(s => s.EndsWith(TestHostFixture.ScriptFileName + ".tokens")), diff --git a/tests/HydraScript.IntegrationTests/SuccessPrograms/InputTests.cs b/tests/HydraScript.IntegrationTests/SuccessPrograms/InputTests.cs index 10b82031..0bb251ec 100644 --- a/tests/HydraScript.IntegrationTests/SuccessPrograms/InputTests.cs +++ b/tests/HydraScript.IntegrationTests/SuccessPrograms/InputTests.cs @@ -1,4 +1,5 @@ using HydraScript.Domain.BackEnd; +using HydraScript.Infrastructure; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using NSubstitute; @@ -22,7 +23,7 @@ public void Invoke_Input_Success() env.When(x => x.SetEnvironmentVariable("SOME_NUMBER", "1")) .Do(_ => env.GetEnvironmentVariable("SOME_NUMBER").Returns("1")); - runner.Invoke(); + runner.Invoke().Should().Be(Executor.ExitCodes.Success); console.Received(1).WriteLine("1"); } diff --git a/tests/HydraScript.IntegrationTests/SuccessPrograms/SuccessfulProgramsTests.cs b/tests/HydraScript.IntegrationTests/SuccessPrograms/SuccessfulProgramsTests.cs index 9cf78807..609055bd 100644 --- a/tests/HydraScript.IntegrationTests/SuccessPrograms/SuccessfulProgramsTests.cs +++ b/tests/HydraScript.IntegrationTests/SuccessPrograms/SuccessfulProgramsTests.cs @@ -13,8 +13,7 @@ public void Invoke_NoError_ReturnCodeIsZero(string relativePathToFile) FileName: relativePathToFile, MockFileSystem: false, MockEnv: false)); - var code = runner.Invoke(); - code.Should().Be(Executor.ExitCodes.Success); + runner.Invoke().Should().Be(Executor.ExitCodes.Success); } public class SuccessfulPrograms : TheoryData diff --git a/tests/HydraScript.UnitTests/Domain/FrontEnd/AstNodeTests.cs b/tests/HydraScript.UnitTests/Domain/FrontEnd/AstNodeTests.cs index b55c9664..d2bc9560 100644 --- a/tests/HydraScript.UnitTests/Domain/FrontEnd/AstNodeTests.cs +++ b/tests/HydraScript.UnitTests/Domain/FrontEnd/AstNodeTests.cs @@ -1,12 +1,16 @@ +using System.Text.RegularExpressions; +using HydraScript.Domain.FrontEnd.Parser.Impl.Ast; using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes; using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Declarations; using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Declarations.AfterTypesAreLoaded; +using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Expressions; +using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Expressions.AccessExpressions; using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Expressions.PrimaryExpressions; using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Statements; namespace HydraScript.UnitTests.Domain.FrontEnd; -public class AstNodeTests +public partial class AstNodeTests { [Fact] public void ChildOf_Precedence_Success() @@ -32,7 +36,44 @@ public void ChildOf_Precedence_Success() [Fact] public void IfStatement_ThenIsNotBlockAndElseIsNull_NotEmpty() { - var ifStatement = new IfStatement(Literal.Boolean(true), new InsideStatementJump("break")); + var ifStatement = new IfStatement( + Literal.Boolean(true), + new InsideStatementJump("break")); ifStatement.Empty.Should().BeFalse(); } + + [Fact] + public void Clone_MemberExpressionWithChain_ReturnsDeepCopy() + { + // obj.arr[0].x + var id = new IdentifierReference("obj"); + var dotArr = new DotAccess(new IdentifierReference("arr")); + var arrIndex = new IndexAccess(Literal.Number(0), dotArr); + var dotX = new DotAccess(new IdentifierReference("x"), arrIndex); + + var accessChain = new LinkedList( + [ + dotArr, + arrIndex, + dotX + ]); + var member = new MemberExpression(id, accessChain); + + var clone = member.Clone(); + + Assert.NotSame(member, clone); + Assert.NotSame(member.Id, clone.Id); + Assert.NotSame(member.AccessChain, clone.AccessChain); + + var memberAst = new AbstractSyntaxTree(member).ToString(); + var cloneAst = new AbstractSyntaxTree(clone).ToString(); + + Assert.NotEqual(memberAst, cloneAst); + Assert.Equal( + RemoveHashCodeDigits.Replace(memberAst, string.Empty), + RemoveHashCodeDigits.Replace(cloneAst, string.Empty)); + } + + [GeneratedRegex("[0-9]+")] + private static partial Regex RemoveHashCodeDigits { get; } } \ No newline at end of file From b2992f631ecbebcd1074ef40fc9f5696b660b509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D0=BD=D0=B8=D0=BD=20=D0=A1=D1=82=D0=B5=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B8=D1=87?= Date: Fri, 22 May 2026 18:55:52 +0300 Subject: [PATCH 17/20] refactor(MemberExpression) - remove computed --- .../Visitors/ExpressionInstructionProvider.cs | 2 +- .../IComputedTypesStorage.cs | 10 ------ .../Impl/ComputedTypesStorage.cs | 18 ---------- .../ServiceCollectionExtensions.cs | 1 - .../Visitors/SemanticChecker.cs | 35 +++++++++---------- .../AccessExpressions/AccessExpression.cs | 15 +++----- .../AccessExpressions/DotAccess.cs | 4 ++- .../AccessExpressions/IndexAccess.cs | 4 ++- .../Ast/Nodes/Expressions/MemberExpression.cs | 34 +++++++++++------- .../Parser/Impl/TopDownParser.Expression.cs | 9 ++--- 10 files changed, 52 insertions(+), 80 deletions(-) delete mode 100644 src/Application/HydraScript.Application.StaticAnalysis/IComputedTypesStorage.cs delete mode 100644 src/Application/HydraScript.Application.StaticAnalysis/Impl/ComputedTypesStorage.cs diff --git a/src/Application/HydraScript.Application.CodeGeneration/Visitors/ExpressionInstructionProvider.cs b/src/Application/HydraScript.Application.CodeGeneration/Visitors/ExpressionInstructionProvider.cs index fce4fcec..7d1f8aaa 100644 --- a/src/Application/HydraScript.Application.CodeGeneration/Visitors/ExpressionInstructionProvider.cs +++ b/src/Application/HydraScript.Application.CodeGeneration/Visitors/ExpressionInstructionProvider.cs @@ -270,7 +270,7 @@ public AddressedInstructions Visit(AssignmentExpression visitable) public AddressedInstructions Visit(MemberExpression visitable) => visitable.Empty() ? [] - : visitable.Tail?.Accept(This) ?? []; + : visitable.AccessChain.Last?.Value.Accept(This) ?? []; public AddressedInstructions Visit(DotAccess visitable) { diff --git a/src/Application/HydraScript.Application.StaticAnalysis/IComputedTypesStorage.cs b/src/Application/HydraScript.Application.StaticAnalysis/IComputedTypesStorage.cs deleted file mode 100644 index 8c210aa1..00000000 --- a/src/Application/HydraScript.Application.StaticAnalysis/IComputedTypesStorage.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace HydraScript.Application.StaticAnalysis; - -public interface IComputedTypesStorage -{ - public Guid Save(Type computedType); - - public Type Get(Guid computedTypeGuid); - - public void Clear(); -} \ No newline at end of file diff --git a/src/Application/HydraScript.Application.StaticAnalysis/Impl/ComputedTypesStorage.cs b/src/Application/HydraScript.Application.StaticAnalysis/Impl/ComputedTypesStorage.cs deleted file mode 100644 index a8d81510..00000000 --- a/src/Application/HydraScript.Application.StaticAnalysis/Impl/ComputedTypesStorage.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace HydraScript.Application.StaticAnalysis.Impl; - -internal class ComputedTypesStorage : IComputedTypesStorage -{ - private readonly Dictionary _computedTypes = []; - - public Guid Save(Type computedType) - { - var guid = Guid.NewGuid(); - _computedTypes[guid] = computedType; - return guid; - } - - public Type Get(Guid computedTypeGuid) => - _computedTypes[computedTypeGuid]; - - public void Clear() => _computedTypes.Clear(); -} \ No newline at end of file diff --git a/src/Application/HydraScript.Application.StaticAnalysis/ServiceCollectionExtensions.cs b/src/Application/HydraScript.Application.StaticAnalysis/ServiceCollectionExtensions.cs index ae8dd7b3..e6c0a120 100644 --- a/src/Application/HydraScript.Application.StaticAnalysis/ServiceCollectionExtensions.cs +++ b/src/Application/HydraScript.Application.StaticAnalysis/ServiceCollectionExtensions.cs @@ -15,7 +15,6 @@ public static IServiceCollection AddStaticAnalysis(this IServiceCollection servi services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Application/HydraScript.Application.StaticAnalysis/Visitors/SemanticChecker.cs b/src/Application/HydraScript.Application.StaticAnalysis/Visitors/SemanticChecker.cs index 7ae491e5..df5f8689 100644 --- a/src/Application/HydraScript.Application.StaticAnalysis/Visitors/SemanticChecker.cs +++ b/src/Application/HydraScript.Application.StaticAnalysis/Visitors/SemanticChecker.cs @@ -49,7 +49,6 @@ internal class SemanticChecker : VisitorBase, private readonly IFunctionWithUndefinedReturnStorage _functionStorage; private readonly IMethodStorage _methodStorage; private readonly ISymbolTableStorage _symbolTables; - private readonly IComputedTypesStorage _computedTypes; private readonly IAmbiguousInvocationStorage _ambiguousInvocations; private readonly IVisitor _typeBuilder; @@ -58,7 +57,6 @@ public SemanticChecker( IFunctionWithUndefinedReturnStorage functionStorage, IMethodStorage methodStorage, ISymbolTableStorage symbolTables, - IComputedTypesStorage computedTypes, IAmbiguousInvocationStorage ambiguousInvocations, IVisitor typeBuilder) { @@ -66,7 +64,6 @@ public SemanticChecker( _functionStorage = functionStorage; _methodStorage = methodStorage; _symbolTables = symbolTables; - _computedTypes = computedTypes; _ambiguousInvocations = ambiguousInvocations; _typeBuilder = typeBuilder; } @@ -83,7 +80,6 @@ public Type Visit(ScriptBody visitable) _methodStorage.Clear(); _symbolTables.Clear(); - _computedTypes.Clear(); _ambiguousInvocations.Clear(); return _typesService.Undefined; @@ -341,17 +337,18 @@ public Type Visit(AssignmentExpression visitable) public Type Visit(MemberExpression visitable) { IAbstractSyntaxTreeNode id = visitable.Id; - var idType = id.Accept(This); - visitable.ComputedIdTypeGuid = _computedTypes.Save(idType); - return visitable.Empty() ? idType : visitable.AccessChain?.Accept(This) ?? _typesService.Undefined; + return visitable.Empty() + ? id.Accept(This) + : visitable.AccessChain.Last?.Value.Accept(This) ?? _typesService.Undefined; } public Type Visit(IndexAccess visitable) { - var prevTypeGuid = - visitable.Prev?.ComputedTypeGuid - ?? (visitable.Parent as MemberExpression)!.ComputedIdTypeGuid; - var prevType = _computedTypes.Get(prevTypeGuid); + var prevType = + visitable.Prev?.Accept(This) ?? + (visitable.Parent is MemberExpression { Id: IAbstractSyntaxTreeNode id } + ? id.Accept(This) + : _typesService.Undefined); if (!prevType.TryGetOperator("[]", out var indexOperator)) throw new NonAccessibleType(prevType); @@ -361,16 +358,16 @@ public Type Visit(IndexAccess visitable) if (!indexOperator.TryGetResultType(indexAccessDescriptor, out var elemType)) throw new ArrayAccessException(visitable.Segment, indexType); - visitable.ComputedTypeGuid = _computedTypes.Save(elemType); - return visitable.HasNext() ? visitable.Next?.Accept(This) ?? _typesService.Undefined : elemType; + return elemType; } public Type Visit(DotAccess visitable) { - var prevTypeGuid = - visitable.Prev?.ComputedTypeGuid - ?? (visitable.Parent as MemberExpression)!.ComputedIdTypeGuid; - var prevType = _computedTypes.Get(prevTypeGuid); + var prevType = + visitable.Prev?.Accept(This) ?? + (visitable.Parent is MemberExpression { Id: IAbstractSyntaxTreeNode id } + ? id.Accept(This) + : _typesService.Undefined); if (prevType is not ObjectType objectType) throw new NonAccessibleType(prevType); @@ -381,8 +378,8 @@ public Type Visit(DotAccess visitable) return hasMethod ? objectType : throw new ObjectAccessException(visitable.Segment, objectType, visitable.Property); - visitable.ComputedTypeGuid = _computedTypes.Save(fieldType); - return visitable.HasNext() ? visitable.Next?.Accept(This) ?? _typesService.Undefined : fieldType; + + return fieldType; } public ObjectType Visit(WithExpression visitable) diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/AccessExpressions/AccessExpression.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/AccessExpressions/AccessExpression.cs index bb7d9289..1e101f52 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/AccessExpressions/AccessExpression.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/AccessExpressions/AccessExpression.cs @@ -2,12 +2,9 @@ namespace HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Expressions.AccessEx public abstract class AccessExpression : Expression { - public AccessExpression? Next { get; private set; } + protected AccessExpression? Next { get; private set; } - public AccessExpression? Prev => - Parent as AccessExpression; - - public Guid ComputedTypeGuid { get; set; } = Guid.Empty; + public AccessExpression? Prev => Parent as AccessExpression; protected AccessExpression(AccessExpression? prev) { @@ -18,12 +15,10 @@ protected AccessExpression(AccessExpression? prev) } } - public bool HasNext() => - Next is not null; - - public bool HasPrev() => - Prev is not null; + public bool HasPrev() => Prev is not null; public abstract override TReturn Accept( IVisitor visitor); + + public abstract override AccessExpression Clone(); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/AccessExpressions/DotAccess.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/AccessExpressions/DotAccess.cs index 663c0555..5c142ed5 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/AccessExpressions/DotAccess.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/AccessExpressions/DotAccess.cs @@ -6,7 +6,7 @@ namespace HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Expressions.AccessEx public partial class DotAccess : AccessExpression { protected override IReadOnlyList Children => - HasNext() ? [Property, Next!] : [Property]; + Next is { } next ? [Property, next] : [Property]; public IdentifierReference Property { get; } @@ -17,4 +17,6 @@ public DotAccess(IdentifierReference property, AccessExpression? prev = null) : } protected override string NodeRepresentation() => "."; + + public override DotAccess Clone() => new(Property.Clone(), Prev?.Clone()); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/AccessExpressions/IndexAccess.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/AccessExpressions/IndexAccess.cs index 633dd27c..8bef772c 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/AccessExpressions/IndexAccess.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/AccessExpressions/IndexAccess.cs @@ -4,7 +4,7 @@ namespace HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Expressions.AccessEx public partial class IndexAccess : AccessExpression { protected override IReadOnlyList Children => - HasNext() ? [Index, Next!] : [Index]; + Next is { } next ? [Index, next] : [Index]; public Expression Index { get; } @@ -15,4 +15,6 @@ public IndexAccess(Expression index, AccessExpression? prev = null) : base(prev) } protected override string NodeRepresentation() => "[]"; + + public override IndexAccess Clone() => new(Index.Clone(), Prev?.Clone()); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/MemberExpression.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/MemberExpression.cs index 07decfb6..e47331db 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/MemberExpression.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/MemberExpression.cs @@ -9,28 +9,24 @@ public partial class MemberExpression : LeftHandSideExpression private readonly IdentifierReference _identifierReference; protected override IReadOnlyList Children => - AccessChain is not null ? [Id, AccessChain] : [Id]; + AccessChain.First is { Value: { } head } ? [Id, head] : [Id]; - public AccessExpression? AccessChain { get; } - public AccessExpression? Tail { get; } + public LinkedList AccessChain { get; } - public Guid ComputedIdTypeGuid { get; set; } = Guid.Empty; - - public MemberExpression(IdentifierReference identifierReference) + public MemberExpression(IdentifierReference identifierReference) : + this(identifierReference, []) { - _identifierReference = identifierReference; - _identifierReference.Parent = this; } public MemberExpression( IdentifierReference identifierReference, - AccessExpression? accessChain, - AccessExpression? tail) : this(identifierReference) + LinkedList accessChain) { - AccessChain = accessChain; - AccessChain?.Parent = this; + _identifierReference = identifierReference; + _identifierReference.Parent = this; - Tail = tail; + AccessChain = accessChain; + AccessChain.First?.Value.Parent = this; } public override IdentifierReference Id => _identifierReference; @@ -38,4 +34,16 @@ public MemberExpression( public bool Empty() => AccessChain.Count == 0; protected override string NodeRepresentation() => nameof(MemberExpression); + + public override MemberExpression Clone() + { + var clonedAccessChain = new LinkedList(); + var clonedTail = AccessChain.Last?.Value.Clone(); + while (clonedTail != null) + { + clonedAccessChain.AddFirst(clonedTail); + clonedTail = clonedTail.Prev; + } + return new MemberExpression(Id.Clone(), clonedAccessChain); + } } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs index e8c26124..f4143fcb 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs @@ -83,10 +83,7 @@ private MemberExpression MemberExpression() } } - return new MemberExpression( - memberRoot, - accessChain.First?.Value, - tail: accessChain.Last?.Value) + return new MemberExpression(memberRoot, accessChain) { Segment = memberRoot.Segment }; @@ -208,8 +205,8 @@ private Expression EqualityExpression() private Expression RelationExpression() { var left = AdditiveExpression(); - while (CurrentIsOperator(">") || CurrentIsOperator("<") || CurrentIsOperator(">=") || - CurrentIsOperator("<=")) + while (CurrentIsOperator(">") || CurrentIsOperator("<") || + CurrentIsOperator(">=") || CurrentIsOperator("<=")) { var op = Expect("Operator"); var right = AdditiveExpression(); From 0c9809c99517293d1abacde6a1104c9c3ed4ce9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D0=BD=D0=B8=D0=BD=20=D0=A1=D1=82=D0=B5=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B8=D1=87?= Date: Fri, 22 May 2026 19:02:14 +0300 Subject: [PATCH 18/20] feat(TopDownParser): AssignmentExpression Desugar Compound Assignment Closes #233 --- .../Parser/Impl/TopDownParser.Expression.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs index f4143fcb..2f470a02 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/TopDownParser.Expression.cs @@ -19,7 +19,13 @@ private Expression Expression() if (expr is MemberExpression lhs && CurrentIs("Assign")) { var assign = Expect("Assign"); - return new AssignmentExpression(lhs, Expression()) + var source = assign.Value is "=" + ? Expression() + : new BinaryExpression( + lhs.Empty() ? lhs.Id.Clone() : lhs.Clone(), + assign.Value[..^1], + Expression()); + return new AssignmentExpression(lhs, source) { Segment = assign.Segment }; } From ba5ce245fc7deb5ff095f488a3d6610c250ee83f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D0=BD=D0=B8=D0=BD=20=D0=A1=D1=82=D0=B5=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B8=D1=87?= Date: Fri, 22 May 2026 19:02:40 +0300 Subject: [PATCH 19/20] test(HydraScript): compound assignment Closes #233 --- .../Samples/compound_assign.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 tests/HydraScript.IntegrationTests/Samples/compound_assign.js diff --git a/tests/HydraScript.IntegrationTests/Samples/compound_assign.js b/tests/HydraScript.IntegrationTests/Samples/compound_assign.js new file mode 100644 index 00000000..dc5891ff --- /dev/null +++ b/tests/HydraScript.IntegrationTests/Samples/compound_assign.js @@ -0,0 +1,19 @@ +let x = { + prop: 2; +} +x.prop += 4 + 5 * 3 +x.prop *= 7 +>>> x + +let y = 1 +y -= (y + 2) * (3 + 4) +y /= 5 +>>> y + +let arr: number[] = [] +let i = 0 +while (i < 5) { + arr ++= [i] + i += 1 +} +>>> arr \ No newline at end of file From bbe2292eb09fb5da610663349ac1f6c4b90d304c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D0=BD=D0=B8=D0=BD=20=D0=A1=D1=82=D0=B5=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B8=D1=87?= Date: Fri, 22 May 2026 19:06:33 +0300 Subject: [PATCH 20/20] revert(AST): Clone Leave Expression the only cloning nodes --- .../Parser/Impl/Ast/AbstractSyntaxTreeNode.cs | 2 -- .../AfterTypesAreLoaded/FunctionDeclaration.cs | 8 -------- .../AfterTypesAreLoaded/IFunctionArgument.cs | 12 ------------ .../AfterTypesAreLoaded/LexicalDeclaration.cs | 11 ----------- .../Ast/Nodes/Declarations/TypeDeclaration.cs | 2 -- .../Impl/Ast/Nodes/Expressions/Expression.cs | 2 +- .../Parser/Impl/Ast/Nodes/ScriptBody.cs | 3 --- .../Parser/Impl/Ast/Nodes/StatementListItem.cs | 15 +++------------ .../Impl/Ast/Nodes/Statements/BlockStatement.cs | 3 --- .../Ast/Nodes/Statements/ExpressionStatement.cs | 2 -- .../Impl/Ast/Nodes/Statements/IfStatement.cs | 3 --- .../Impl/Ast/Nodes/Statements/InputStatement.cs | 2 -- .../Ast/Nodes/Statements/InsideStatementJump.cs | 2 -- .../Impl/Ast/Nodes/Statements/OutputStatement.cs | 2 -- .../Impl/Ast/Nodes/Statements/ReturnStatement.cs | 2 -- .../Impl/Ast/Nodes/Statements/WhileStatement.cs | 2 -- 16 files changed, 4 insertions(+), 69 deletions(-) diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/AbstractSyntaxTreeNode.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/AbstractSyntaxTreeNode.cs index b14e6686..bf6dbd2a 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/AbstractSyntaxTreeNode.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/AbstractSyntaxTreeNode.cs @@ -36,8 +36,6 @@ IEnumerator IEnumerable.GetEnumerator() => public IReadOnlyList GetAllNodes() => new TraverseEnumerator(this).AsValueEnumerable().ToArray(); - public abstract IAbstractSyntaxTreeNode Clone(); - /// /// Метод возвращает true, если узел - потомок заданного типа и выполняется заданное условие.
/// В случае, когда условие не задано, проверяется просто соответствие типов. diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/FunctionDeclaration.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/FunctionDeclaration.cs index 8e01be68..259a8970 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/FunctionDeclaration.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/FunctionDeclaration.cs @@ -56,12 +56,4 @@ public override void InitScope(Scope? scope = null) protected override string NodeRepresentation() => ZString.Concat("function", ' ', Name); - - public override FunctionDeclaration Clone() => - new( - Name.Clone(), - ReturnTypeValue.DeepClone(), - _arguments.Select(x => x.DeepClone()).ToList(), - Statements.Clone(), - IndexOfFirstDefaultArgument); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/IFunctionArgument.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/IFunctionArgument.cs index c54503fa..5a37e3fd 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/IFunctionArgument.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/IFunctionArgument.cs @@ -9,8 +9,6 @@ public interface IFunctionArgument public TypeValue TypeValue { get; } public ValueDto Info { get; } - - public IFunctionArgument DeepClone(); } public record NamedArgument( @@ -21,11 +19,6 @@ public override string ToString() => $"{Name}: {TypeValue}"; public ValueDto Info { get; } = ValueDto.NameDto(Name); - - public IFunctionArgument DeepClone() => this with - { - TypeValue = TypeValue.DeepClone() - }; } public record DefaultValueArgument : IFunctionArgument @@ -43,11 +36,6 @@ public DefaultValueArgument(string name, Literal literal) public ValueDto Info { get; } - public IFunctionArgument DeepClone() => - new DefaultValueArgument( - Name, - new Literal(TypeValue.DeepClone(), Info.Value, label: Info.Label)); - public override string ToString() => $"{Name} = {Info.Label}"; } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/LexicalDeclaration.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/LexicalDeclaration.cs index cdeb8f55..bd0c65d0 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/LexicalDeclaration.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/LexicalDeclaration.cs @@ -21,15 +21,4 @@ public void AddAssignment(AssignmentExpression assignment) protected override string NodeRepresentation() => ReadOnly ? "const" : "let"; - - public override LexicalDeclaration Clone() - { - var clone = new LexicalDeclaration(ReadOnly); - for (var i = 0; i < _assignments.Count; i++) - { - clone.AddAssignment(_assignments[i].Clone()); - } - - return clone; - } } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/TypeDeclaration.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/TypeDeclaration.cs index a226e7cc..e4c7cbfb 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/TypeDeclaration.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/TypeDeclaration.cs @@ -17,6 +17,4 @@ public override void InitScope(Scope? scope = null) protected override string NodeRepresentation() => $"type {TypeId.Name} = {TypeValue}"; - - public override TypeDeclaration Clone() => new(TypeId.Clone(), TypeValue.DeepClone()); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/Expression.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/Expression.cs index 9ab3884e..c3500d9f 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/Expression.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Expressions/Expression.cs @@ -2,7 +2,7 @@ namespace HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Expressions; public abstract class Expression : AbstractSyntaxTreeNode { - public abstract override Expression Clone(); + public abstract Expression Clone(); public abstract override TReturn Accept( IVisitor visitor); diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/ScriptBody.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/ScriptBody.cs index 49c8a760..2c179707 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/ScriptBody.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/ScriptBody.cs @@ -23,7 +23,4 @@ public override void InitScope(Scope? scope = null) } protected override string NodeRepresentation() => "Script"; - - public override ScriptBody Clone() => - new(_statementList.Select(x => x.Clone()).ToList()); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/StatementListItem.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/StatementListItem.cs index e58ae446..e48633da 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/StatementListItem.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/StatementListItem.cs @@ -1,19 +1,10 @@ namespace HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes; public abstract class StatementListItem : - AbstractSyntaxTreeNode -{ - public abstract override StatementListItem Clone(); -} + AbstractSyntaxTreeNode; public abstract class Statement : - StatementListItem -{ - public abstract override Statement Clone(); -} + StatementListItem; public abstract class Declaration : - StatementListItem -{ - public abstract override Declaration Clone(); -} \ No newline at end of file + StatementListItem; \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/BlockStatement.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/BlockStatement.cs index 341220d4..562ecd94 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/BlockStatement.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/BlockStatement.cs @@ -24,7 +24,4 @@ public override void InitScope(Scope? scope = null) } protected override string NodeRepresentation() => "{}"; - - public override BlockStatement Clone() => - new(_statementList.Select(x => x.Clone()).ToList()); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/ExpressionStatement.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/ExpressionStatement.cs index 86c1c18b..0d112cdf 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/ExpressionStatement.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/ExpressionStatement.cs @@ -16,6 +16,4 @@ public ExpressionStatement(Expression expression) } protected override string NodeRepresentation() => nameof(ExpressionStatement); - - public override ExpressionStatement Clone() => new(Expression.Clone()); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/IfStatement.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/IfStatement.cs index 3a624ae3..800415f6 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/IfStatement.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/IfStatement.cs @@ -32,7 +32,4 @@ public IfStatement(Expression test, Statement then, Statement? @else = null) public bool HasElseBlock() => Else is { Count: > 0 }; protected override string NodeRepresentation() => "if"; - - public override IfStatement Clone() => - new(Test.Clone(), Then.Clone(), Else?.Clone()); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/InputStatement.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/InputStatement.cs index a37b2732..178e7b0f 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/InputStatement.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/InputStatement.cs @@ -18,6 +18,4 @@ public InputStatement(IdentifierReference destination) } protected override string NodeRepresentation() => "input"; - - public override InputStatement Clone() => new(Destination.Clone()); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/InsideStatementJump.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/InsideStatementJump.cs index 87e0a878..bf7ebce0 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/InsideStatementJump.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/InsideStatementJump.cs @@ -6,6 +6,4 @@ public partial class InsideStatementJump(string keyword) : Statement public string Keyword { get; } = keyword; protected override string NodeRepresentation() => Keyword; - - public override InsideStatementJump Clone() => new(Keyword); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/OutputStatement.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/OutputStatement.cs index 6cd8bc79..4a38d162 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/OutputStatement.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/OutputStatement.cs @@ -16,6 +16,4 @@ public OutputStatement(Expression expression) } protected override string NodeRepresentation() => "output"; - - public override OutputStatement Clone() => new(Expression.Clone()); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/ReturnStatement.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/ReturnStatement.cs index e389993c..b8764458 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/ReturnStatement.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/ReturnStatement.cs @@ -16,6 +16,4 @@ public ReturnStatement(Expression? expression = null) } protected override string NodeRepresentation() => "return"; - - public override ReturnStatement Clone() => new(Expression?.Clone()); } \ No newline at end of file diff --git a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/WhileStatement.cs b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/WhileStatement.cs index f730a5a1..cffb4a4d 100644 --- a/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/WhileStatement.cs +++ b/src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Statements/WhileStatement.cs @@ -20,6 +20,4 @@ public WhileStatement(Expression condition, Statement statement) } protected override string NodeRepresentation() => "while"; - - public override WhileStatement Clone() => new(Condition.Clone(), Statement.Clone()); } \ No newline at end of file