Skip to content

Commit 3af7a43

Browse files
committed
Merge remote-tracking branch 'refs/remotes/origin/2-5-0' into feature/net-10
2 parents 0e64d41 + 078f106 commit 3af7a43

File tree

11 files changed

+192
-39
lines changed

11 files changed

+192
-39
lines changed

src/Application/HydraScript.Application.CodeGeneration/Visitors/InstructionProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ public AddressedInstructions Visit(FunctionDeclaration visitable)
139139
}
140140

141141
result.AddRange(visitable.Statements.Accept(This));
142-
if (!visitable.HasReturnStatement())
142+
if (!visitable.HasReturnStatement)
143143
result.Add(new Return());
144144

145145
result.Add(new EndBlock(BlockType.Function, blockId: functionInfo.ToString()), functionInfo.End.Name);

src/Application/HydraScript.Application.StaticAnalysis/ServiceCollectionExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using HydraScript.Application.StaticAnalysis.Visitors;
33
using HydraScript.Domain.FrontEnd.Parser;
44
using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Declarations;
5+
using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Declarations.AfterTypesAreLoaded;
56
using Microsoft.Extensions.DependencyInjection;
67

78
namespace HydraScript.Application.StaticAnalysis;
@@ -24,6 +25,7 @@ public static IServiceCollection AddStaticAnalysis(this IServiceCollection servi
2425
services.AddSingleton<IAmbiguousInvocationStorage, AmbiguousInvocationStorage>();
2526

2627
services.AddSingleton<IVisitor<TypeValue, Type>, TypeBuilder>();
28+
services.AddSingleton<IVisitor<FunctionDeclaration, ReturnAnalyzerResult>, ReturnAnalyzer>();
2729

2830
services.AddSingleton<IVisitor<IAbstractSyntaxTreeNode>, SymbolTableInitializer>();
2931
services.AddSingleton<IVisitor<IAbstractSyntaxTreeNode>, TypeSystemLoader>();

src/Application/HydraScript.Application.StaticAnalysis/Visitors/DeclarationVisitor.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,22 @@ internal class DeclarationVisitor : VisitorNoReturnBase<IAbstractSyntaxTreeNode>
2020
private readonly ISymbolTableStorage _symbolTables;
2121
private readonly IAmbiguousInvocationStorage _ambiguousInvocations;
2222
private readonly IVisitor<TypeValue, Type> _typeBuilder;
23+
private readonly IVisitor<FunctionDeclaration, ReturnAnalyzerResult> _returnAnalyzer;
2324

2425
public DeclarationVisitor(
2526
IFunctionWithUndefinedReturnStorage functionStorage,
2627
IMethodStorage methodStorage,
2728
ISymbolTableStorage symbolTables,
2829
IAmbiguousInvocationStorage ambiguousInvocations,
29-
IVisitor<TypeValue, Type> typeBuilder)
30+
IVisitor<TypeValue, Type> typeBuilder,
31+
IVisitor<FunctionDeclaration, ReturnAnalyzerResult> returnAnalyzer)
3032
{
3133
_functionStorage = functionStorage;
3234
_methodStorage = methodStorage;
3335
_symbolTables = symbolTables;
3436
_ambiguousInvocations = ambiguousInvocations;
3537
_typeBuilder = typeBuilder;
38+
_returnAnalyzer = returnAnalyzer;
3639
}
3740

3841
public override VisitUnit Visit(IAbstractSyntaxTreeNode visitable)
@@ -71,6 +74,10 @@ public VisitUnit Visit(LexicalDeclaration visitable)
7174

7275
public VisitUnit Visit(FunctionDeclaration visitable)
7376
{
77+
var returnAnalyzerResult = visitable.Accept(_returnAnalyzer);
78+
visitable.ReturnStatements = returnAnalyzerResult.ReturnStatements;
79+
visitable.AllCodePathsEndedWithReturn = returnAnalyzerResult.CodePathEndedWithReturn;
80+
7481
var parentTable = _symbolTables[visitable.Parent.Scope];
7582
var indexOfFirstDefaultArgument = visitable.Arguments.AsValueEnumerable()
7683
.Select((x, i) => new { Argument = x, Index = i })
@@ -108,7 +115,7 @@ public VisitUnit Visit(FunctionDeclaration visitable)
108115
Type undefined = "undefined";
109116
if (functionSymbol.Type.Equals(undefined))
110117
{
111-
if (visitable.HasReturnStatement())
118+
if (visitable.HasReturnStatement)
112119
_functionStorage.Save(functionSymbol, visitable);
113120
else
114121
functionSymbol.DefineReturnType("void");
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using HydraScript.Domain.FrontEnd.Parser;
2+
using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Declarations.AfterTypesAreLoaded;
3+
using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Statements;
4+
5+
namespace HydraScript.Application.StaticAnalysis.Visitors;
6+
7+
internal class ReturnAnalyzer : VisitorBase<IAbstractSyntaxTreeNode, ReturnAnalyzerResult>,
8+
IVisitor<FunctionDeclaration, ReturnAnalyzerResult>,
9+
IVisitor<IfStatement, ReturnAnalyzerResult>,
10+
IVisitor<ReturnStatement, ReturnAnalyzerResult>
11+
{
12+
public ReturnAnalyzerResult Visit(FunctionDeclaration visitable)
13+
{
14+
IAbstractSyntaxTreeNode astNode = visitable;
15+
return Visit(astNode);
16+
}
17+
18+
public override ReturnAnalyzerResult Visit(IAbstractSyntaxTreeNode visitable)
19+
{
20+
var result = ReturnAnalyzerResult.AdditiveIdentity;
21+
for (var i = 0; i < visitable.Count; i++)
22+
{
23+
var visitableResult = visitable[i].Accept(This);
24+
if (visitableResult.CodePathEndedWithReturn)
25+
return visitableResult * result;
26+
result += visitableResult;
27+
}
28+
29+
return result;
30+
}
31+
32+
public ReturnAnalyzerResult Visit(IfStatement visitable)
33+
{
34+
var thenReturns = visitable.Then.Accept(This);
35+
36+
if (visitable.Else is null)
37+
return thenReturns + ReturnAnalyzerResult.AdditiveIdentity;
38+
var elseReturns = visitable.Else.Accept(This);
39+
40+
return thenReturns + elseReturns;
41+
}
42+
43+
public ReturnAnalyzerResult Visit(ReturnStatement visitable) =>
44+
new(CodePathEndedWithReturn: true, ReturnStatements: [visitable]);
45+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System.Numerics;
2+
using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Statements;
3+
4+
namespace HydraScript.Application.StaticAnalysis.Visitors;
5+
6+
public sealed record ReturnAnalyzerResult(bool CodePathEndedWithReturn, IReadOnlyList<ReturnStatement> ReturnStatements) :
7+
IAdditiveIdentity<ReturnAnalyzerResult, ReturnAnalyzerResult>,
8+
IAdditionOperators<ReturnAnalyzerResult, ReturnAnalyzerResult, ReturnAnalyzerResult>,
9+
IMultiplyOperators<ReturnAnalyzerResult, ReturnAnalyzerResult, ReturnAnalyzerResult>
10+
{
11+
public static ReturnAnalyzerResult operator +(ReturnAnalyzerResult left, ReturnAnalyzerResult right) =>
12+
new(
13+
left.CodePathEndedWithReturn && right.CodePathEndedWithReturn,
14+
ReturnStatements: [..left.ReturnStatements, ..right.ReturnStatements]);
15+
16+
public static ReturnAnalyzerResult AdditiveIdentity { get; } = new(CodePathEndedWithReturn: false, ReturnStatements: []);
17+
18+
public static ReturnAnalyzerResult operator *(ReturnAnalyzerResult left, ReturnAnalyzerResult right) =>
19+
new(
20+
left.CodePathEndedWithReturn || right.CodePathEndedWithReturn,
21+
ReturnStatements: [..left.ReturnStatements, ..right.ReturnStatements]);
22+
}

src/Application/HydraScript.Application.StaticAnalysis/Visitors/SemanticChecker.cs

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -491,37 +491,26 @@ public Type Visit(FunctionDeclaration visitable)
491491
_functionStorage.RemoveIfPresent(symbol);
492492
visitable.Statements.Accept(This);
493493

494-
var returnStatements = visitable.ReturnStatements
495-
.Select(x => new
496-
{
497-
Statement = x,
498-
Type = x.Accept(This)
499-
});
500494
Type undefined = "undefined";
501-
if (symbol.Type.Equals(undefined))
495+
HashSet<Type> returnTypes = [];
496+
for (var i = 0; i < visitable.ReturnStatements.Count; i++)
502497
{
503-
var returnStatementTypes = returnStatements
504-
.GroupBy(x => x.Type)
505-
.Select(x => x.Key)
506-
.ToList();
507-
if (returnStatementTypes.Count > 1)
498+
var returnStatementType = visitable.ReturnStatements[i].Accept(This);
499+
returnTypes.Add(returnStatementType);
500+
if (returnTypes.Count > 1 && symbol.Type.Equals(undefined))
508501
throw new CannotDefineType(visitable.Segment);
509-
symbol.DefineReturnType(returnStatementTypes.ElementAtOrDefault(0) ?? "void");
510-
}
511-
else
512-
{
513-
var wrongReturn = returnStatements
514-
.FirstOrDefault(x => !symbol.Type.Equals(x.Type));
515-
if (wrongReturn is not null)
502+
if (!symbol.Type.Equals(undefined) && !symbol.Type.Equals(returnStatementType))
516503
throw new WrongReturnType(
517-
wrongReturn.Statement.Segment,
504+
visitable.ReturnStatements[i].Segment,
518505
expected: symbol.Type,
519-
actual: wrongReturn.Type);
506+
actual: returnStatementType);
520507
}
521508

509+
if (symbol.Type.Equals(undefined))
510+
symbol.DefineReturnType(returnTypes.Single());
511+
522512
Type @void = "void";
523-
var hasReturnStatement = visitable.HasReturnStatement();
524-
if (!symbol.Type.Equals(@void) && !hasReturnStatement)
513+
if (!symbol.Type.Equals(@void) && !visitable.AllCodePathsEndedWithReturn)
525514
throw new FunctionWithoutReturnStatement(visitable.Segment);
526515

527516
if (symbol.Type is NullType)

src/Domain/HydraScript.Domain.BackEnd/Impl/Instructions/WithAssignment/AsString.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@ public partial class AsString(IValue value) : Simple(value)
1818
public override IAddress? Execute(IExecuteParams executeParams)
1919
{
2020
var frame = executeParams.Frames.Peek();
21-
frame[Left!] = JsonSerializer.Serialize(
22-
value: Right.right!.Get(frame)!,
23-
AsStringJsonContext.Object);
21+
var value = Right.right!.Get(frame);
22+
frame[Left!] = value is string
23+
? value
24+
: JsonSerializer.Serialize(
25+
value: Right.right!.Get(frame)!,
26+
AsStringJsonContext.Object);
2427

2528
return Address.Next;
2629
}

src/Domain/HydraScript.Domain.FrontEnd/Parser/Impl/Ast/Nodes/Declarations/AfterTypesAreLoaded/FunctionDeclaration.cs

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ public partial class FunctionDeclaration : AfterTypesAreLoadedDeclaration
1818
public BlockStatement Statements { get; }
1919
public bool IsEmpty => Statements.Count == 0;
2020

21+
public IReadOnlyList<ReturnStatement> ReturnStatements { get; set; } = [];
22+
public bool HasReturnStatement => ReturnStatements.Count > 0;
23+
public bool AllCodePathsEndedWithReturn { get; set; }
24+
2125
public string ComputedFunctionAddress { get; set; } = string.Empty;
2226

2327
public FunctionDeclaration(
@@ -33,11 +37,6 @@ public FunctionDeclaration(
3337
Statements = blockStatement;
3438
Statements.Parent = this;
3539
Children = [Statements];
36-
37-
ReturnStatements = Statements
38-
.GetAllNodes()
39-
.OfType<ReturnStatement>()
40-
.ToArray();
4140
}
4241

4342
/// <summary>Стратегия "блока" - углубление скоупа</summary>
@@ -52,11 +51,6 @@ public override void InitScope(Scope? scope = null)
5251
ReturnTypeValue.Scope = Parent.Scope;
5352
}
5453

55-
public bool HasReturnStatement() =>
56-
ReturnStatements.Count > 0;
57-
58-
public IReadOnlyCollection<ReturnStatement> ReturnStatements { get; }
59-
6054
protected override string NodeRepresentation() =>
6155
ZString.Concat<string, char, string>("function", ' ', Name);
6256
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using HydraScript.Infrastructure;
2+
3+
namespace HydraScript.IntegrationTests.ErrorPrograms;
4+
5+
public class FunctionWithoutReturnStatementTests(TestHostFixture fixture) : IClassFixture<TestHostFixture>
6+
{
7+
[Fact]
8+
public void FunctionDeclaration_MissingReturn_HydraScriptError()
9+
{
10+
const string script =
11+
"""
12+
function f(b: boolean) {
13+
if (b)
14+
return 1
15+
}
16+
""";
17+
using var runner = fixture.GetRunner(new TestHostFixture.Options(InMemoryScript: script));
18+
var code = runner.Invoke();
19+
code.Should().Be(Executor.ExitCodes.HydraScriptError);
20+
fixture.LogMessages.Should()
21+
.Contain(x =>
22+
x.Contains("function with non-void return type must have a return statement"));
23+
}
24+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using HydraScript.Application.StaticAnalysis.Visitors;
2+
using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Declarations;
3+
using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Declarations.AfterTypesAreLoaded;
4+
using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Expressions.PrimaryExpressions;
5+
using HydraScript.Domain.FrontEnd.Parser.Impl.Ast.Nodes.Statements;
6+
7+
namespace HydraScript.UnitTests.Application;
8+
9+
public class ReturnAnalyzerTests
10+
{
11+
/// <summary>
12+
/// <code>
13+
/// function f(b: boolean) {
14+
/// if (b)
15+
/// return 1
16+
/// }
17+
/// </code>
18+
/// </summary>
19+
[Fact]
20+
public void Visit_FunctionWithMissingReturn_CodePathEndedWithReturnIsFalse()
21+
{
22+
// Arrange
23+
var functionDeclaration = new FunctionDeclaration(
24+
new IdentifierReference("f"),
25+
new TypeIdentValue(new IdentifierReference("undefined")),
26+
[new NamedArgument("b", new TypeIdentValue(new IdentifierReference("boolean")))],
27+
new BlockStatement([
28+
new IfStatement(
29+
new IdentifierReference("b"),
30+
new ReturnStatement(
31+
new Literal(new TypeIdentValue(new IdentifierReference("number")), 1, "segment")))
32+
]));
33+
34+
// Act
35+
var result = new ReturnAnalyzer().Visit(functionDeclaration);
36+
37+
// Assert
38+
result.CodePathEndedWithReturn.Should().BeFalse();
39+
result.ReturnStatements.Count.Should().Be(1);
40+
result.ReturnStatements[0].Expression.Should().BeOfType<Literal>();
41+
}
42+
}

0 commit comments

Comments
 (0)