Skip to content

Commit

Permalink
add string concatenation
Browse files Browse the repository at this point in the history
* docs: Update README

fixes #94

* docs: update LICENSE year

fixes #98

* refactor: add nullabe to Oberon0.System

* style: Remove empty statement

* feat: enable tool generation

fixes #96

* ci: remove non-existing file from solution

* ci: remove old sonar settings

* feat: implement `Length()`function for type `STRING`

Fixes #103

* build: add sonarqube cloud connection and fixup project settings

* style: fix SQ code smells

* style: add comment for `RemoveFunction`

* test: add exception test for StandardFunctionRepository.RemoveFunction

* refactor: update internal function resolver for testing

* refactor: move internal functions in compiler to separate namespace

* fix: add internal solver optimization for ToString(int, real, bool)

* feat: STRING concatenation

fixes #106

* chore: add "Mult" to team dictionary

* refactor: There should be no case for an non-existing variable at expression creation.

This is an internal case as variable name checking is done beforehand.

* tests: allow test compilation to support xunit output

* feat: handle string multiplication.

This commit implements also some enhancement to binary operator processing: In the past the statement
`x := TRUE & 1` resulted in an exception. Now an appropriate error message is display that there's no operation that allows AND between boolean and integer.

#108

* style: add justification for `ExcludeFromCoverage`

* refactor(msil): `HandleRepeat`: take `BinaryExpression.Create`-result for granted.

* feat: add string multiplication for MSIL

* refactor: remove unused parts from coverage

* build: remove global.json

* build: remove .netstandard dependency and old msbuild dependencies

* tests: add more tests for coverage

---------

Co-authored-by: Stephen Reindl <[email protected]>
  • Loading branch information
steven-r and Stephen Reindl authored Nov 16, 2024
1 parent f2a84ba commit 42b260b
Show file tree
Hide file tree
Showing 27 changed files with 528 additions and 138 deletions.
40 changes: 27 additions & 13 deletions Oberon0.CompilerSupport/TestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,26 +92,40 @@ void DumpErrors(IEnumerable<CompilerError> compilerErrors)
/// Helper to compile some code with a standard application surrounding
/// </summary>
/// <param name="operations"></param>
/// <param name="outputHelper"></param>
/// <param name="expectedErrors"></param>
/// <returns></returns>
public static Module CompileSingleStatement(string operations, params string[] expectedErrors)
public static Module CompileSingleStatement(string operations, ITestOutputHelper? outputHelper, params string[] expectedErrors)
{
return CompileString(
@$"
MODULE test;
CONST
true_ = TRUE;
false_ = FALSE;
VAR
a,b,c,d,e,f,g,h,x,y,z: BOOLEAN;
i, j, k, l: INTEGER;
r, s, t: REAL;
BEGIN
{operations}
END test.",
$"""
MODULE test;
CONST
true_ = TRUE;
false_ = FALSE;
VAR
a,b,c,d,e,f,g,h,x,y,z: BOOLEAN;
i, j, k, l: INTEGER;
r, s, t: REAL;
BEGIN
{operations}
END test.
""",
outputHelper,
expectedErrors);
}

/// <summary>
/// Helper to compile some code with a standard application surrounding
/// </summary>
/// <param name="operations"></param>
/// <param name="expectedErrors"></param>
/// <returns></returns>
public static Module CompileSingleStatement(string operations, params string[] expectedErrors)
{
return CompileSingleStatement(operations, null, expectedErrors);
}

internal class TestErrorListener<TSymbol>(List<CompilerError> compilerErrors) : IAntlrErrorListener<TSymbol>
{
public void SyntaxError(
Expand Down
114 changes: 114 additions & 0 deletions Oberon0.Generator.MsilBin.Tests/Types/StringTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,118 @@ END Test.
Runner.Execute(assembly, output1);
Assert.Equal($"Hello String", output1.ToString());
}

[Fact]
public void StringMultVarVar()
{
const string source = $"""
MODULE Test;
VAR
a, s: STRING;
b: INTEGER;
BEGIN
a := 'Hello';
b := 5;
WriteString(a * b)
END Test.
""";
var cg = CompileHelper.CompileOberon0Code(source, out string code, output);
Assert.NotEmpty(code);

var syntaxTree = CSharpSyntaxTree.ParseText(code);

byte[] assembly = syntaxTree.CompileAndLoadAssembly(cg, true);
Assert.NotNull(assembly);

using var output1 = new StringWriter();
Runner.Execute(assembly, output1);
Assert.Equal("HelloHelloHelloHelloHello", output1.ToString());
}

[Fact]
public void StringMultVarInt()
{
const string source = $"""
MODULE Test;
VAR
a, s: STRING;
b: INTEGER;
BEGIN
a := 'Hello';
b := 5;
WriteString(a * 3)
END Test.
""";
var cg = CompileHelper.CompileOberon0Code(source, out string code, output);
Assert.NotEmpty(code);

var syntaxTree = CSharpSyntaxTree.ParseText(code);

byte[] assembly = syntaxTree.CompileAndLoadAssembly(cg, true);
Assert.NotNull(assembly);

using var output1 = new StringWriter();
Runner.Execute(assembly, output1);
Assert.Equal("HelloHelloHello", output1.ToString());
}

[Fact]
public void StringMultStringVar()
{
const string source = $"""
MODULE Test;
VAR
a, s: STRING;
b: INTEGER;
BEGIN
a := 'Hello';
b := 4;
WriteString('HI' * b)
END Test.
""";
var cg = CompileHelper.CompileOberon0Code(source, out string code, output);
Assert.NotEmpty(code);

var syntaxTree = CSharpSyntaxTree.ParseText(code);

byte[] assembly = syntaxTree.CompileAndLoadAssembly(cg, true);
Assert.NotNull(assembly);

using var output1 = new StringWriter();
Runner.Execute(assembly, output1);
// ReSharper disable once StringLiteralTypo
Assert.Equal("HIHIHIHI", output1.ToString());
}

[Fact]
public void StringMultVarZero()
{
const string source = $"""
MODULE Test;
VAR
a, s: STRING;
b: INTEGER;
BEGIN
a := 'Hello';
b := 5;
WriteString(a * 0)
END Test.
""";
var cg = CompileHelper.CompileOberon0Code(source, out string code, output);
Assert.NotEmpty(code);

var syntaxTree = CSharpSyntaxTree.ParseText(code);

byte[] assembly = syntaxTree.CompileAndLoadAssembly(cg, true);
Assert.NotNull(assembly);

using var output1 = new StringWriter();
Runner.Execute(assembly, output1);
Assert.Equal("", output1.ToString());
}

}
6 changes: 2 additions & 4 deletions Oberon0.Generator.MsilBin/CreateBinary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Build.Locator;
using Oberon0.Shared;
#pragma warning disable S112

namespace Oberon0.Generator.MsilBin
{
Expand All @@ -30,7 +28,6 @@ public CreateBinary(ICodeGenerator codeGenerator, CreateBinaryOptions? options =
{
ArgumentNullException.ThrowIfNull(codeGenerator);

MSBuildLocator.RegisterDefaults();
_codeGenerator = codeGenerator;
_options = SetOptions(options ?? new CreateBinaryOptions());
if (!Directory.Exists(_options.OutputPath))
Expand All @@ -39,7 +36,7 @@ public CreateBinary(ICodeGenerator codeGenerator, CreateBinaryOptions? options =
}
}

[ExcludeFromCodeCoverage]
[ExcludeFromCodeCoverage(Justification = "This function does not have any impact on execution. It's just a small helper.")]
private static string GetDotnetExe ()
{
string execName = "dotnet";
Expand All @@ -52,6 +49,7 @@ private static string GetDotnetExe ()

private CreateBinaryOptions SetOptions(CreateBinaryOptions options)
{
// ReSharper disable once UnthrowableException
options.ModuleName ??= _codeGenerator.Module.Name ?? throw new NullReferenceException("Name needs to be set");

Check warning on line 53 in Oberon0.Generator.MsilBin/CreateBinary.cs

View workflow job for this annotation

GitHub Actions / build

'System.NullReferenceException' should not be thrown by user code. (https://rules.sonarsource.com/csharp/RSPEC-112)

Check warning on line 53 in Oberon0.Generator.MsilBin/CreateBinary.cs

View workflow job for this annotation

GitHub Actions / build

'System.NullReferenceException' should not be thrown by user code. (https://rules.sonarsource.com/csharp/RSPEC-112)
options.SolutionPath ??= BuildOutputPath(options);
options.OutputPath ??= Environment.CurrentDirectory;
Expand Down
52 changes: 46 additions & 6 deletions Oberon0.Generator.MsilBin/MsilBinGenerator.Expressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@

using System;
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Oberon0.Compiler.Definitions;
using Oberon0.Compiler.Expressions;
using Oberon0.Compiler.Expressions.Constant;
using Oberon0.Compiler.Solver;
using Oberon0.Compiler.Types;
using Oberon0.Generator.MsilBin.GeneratorInfo;

namespace Oberon0.Generator.MsilBin;
Expand All @@ -26,6 +28,11 @@ partial class MsilBinGenerator
{OberonGrammarLexer.NOT, SyntaxKind.LogicalNotExpression}
};

/// <summary>
/// Mapping table to map an Oberon0 binary operation to the corresponding Roslyn mapping. Works for numbers only.
///
/// Please have a look at <see cref="HandleBinaryExpression"/> for handling
/// </summary>
private static readonly Dictionary<int, SyntaxKind> BinaryExpressionMapping = new()
{
{OberonGrammarLexer.EQUAL, SyntaxKind.EqualsExpression},
Expand All @@ -43,16 +50,13 @@ partial class MsilBinGenerator
{OberonGrammarLexer.MOD, SyntaxKind.ModuloExpression}
};

public ExpressionSyntax CompileExpression(Expression compilerExpression)
internal ExpressionSyntax CompileExpression(Expression compilerExpression)
{
return compilerExpression switch
{
UnaryExpression ua => SyntaxFactory.PrefixUnaryExpression(UnaryExpressionMapping[ua.Operator],
CompileExpression(ua.LeftHandSide)),
BinaryExpression be => SyntaxFactory.ParenthesizedExpression(SyntaxFactory.BinaryExpression(
BinaryExpressionMapping[be.Operator],
CompileExpression(be.LeftHandSide),
CompileExpression(be.RightHandSide!))),
BinaryExpression be => HandleBinaryExpression(be),
VariableReferenceExpression vre => GenerateVariableReference(vre.Declaration, vre.Selector),
ConstantExpression ce => GenerateConstantLiteral(ce),
StringExpression se => SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression,
Expand All @@ -63,6 +67,42 @@ public ExpressionSyntax CompileExpression(Expression compilerExpression)
};
}

private ExpressionSyntax HandleBinaryExpression(BinaryExpression be)
{
if (be is { Operator: OberonGrammarLexer.STAR, TargetType.Type: BaseTypes.String })
{
// special treatment for string multiplication:
// implement the following C# code: string.Concat(Enumerable.Repeat(LHS, RHS))
var argumentList = new SyntaxNodeOrTokenList();
argumentList = argumentList.AddRange([
SyntaxFactory.Argument(CompileExpression(be.LeftHandSide)),
SyntaxFactory.Token(SyntaxKind.CommaToken),
SyntaxFactory.Argument(CompileExpression(be.RightHandSide!))
]);
var ie = SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
MapIdentifierName("Enumerable"),
MapIdentifierName("Repeat")))
.WithArgumentList(SyntaxFactory.ArgumentList(
SyntaxFactory.SeparatedList<ArgumentSyntax>(argumentList)));
// ReSharper disable once UseCollectionExpression
argumentList = new SyntaxNodeOrTokenList();
argumentList = argumentList.Add(SyntaxFactory.Argument(ie));
return SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.StringKeyword)),
MapIdentifierName("Concat")))
.WithArgumentList(SyntaxFactory.ArgumentList(
SyntaxFactory.SeparatedList<ArgumentSyntax>(argumentList)));
}
// standard treatment
return SyntaxFactory.ParenthesizedExpression(SyntaxFactory.BinaryExpression(
BinaryExpressionMapping[be.Operator],
CompileExpression(be.LeftHandSide),
CompileExpression(be.RightHandSide!)));
}

private ExpressionSyntax GenerateVariableReference(Declaration declaration, VariableSelector? selector,
bool ignoreReplace = false)
{
Expand All @@ -86,7 +126,7 @@ private ExpressionSyntax GenerateVariableReference(Declaration declaration, Vari
var binaryExpression = BinaryExpression.Create(OberonGrammarLexer.MINUS,
indexSelector.IndexDefinition,
new ConstantIntExpression(1), declaration.Block!);
var solvedExpression = ConstantSolver.Solve(binaryExpression, declaration.Block!);
var solvedExpression = ConstantSolver.Solve(binaryExpression!, declaration.Block!);
var accessor = SyntaxFactory.ElementAccessExpression(
MapIdentifierName(declaration.Name),
SyntaxFactory.BracketedArgumentList(
Expand Down
2 changes: 1 addition & 1 deletion Oberon0.Generator.MsilBin/MsilBinGenerator.Statements.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ private SyntaxList<StatementSyntax> HandleRepeatStatement(SyntaxList<StatementSy
{
var expression = BinaryExpression.Create(OberonGrammarLexer.NOT, repeatStatement.Condition, null,
repeatStatement.Block.Parent!);
var compiled = ConstantSolver.Solve(expression, repeatStatement.Block.Parent!);
var compiled = ConstantSolver.Solve(expression!, repeatStatement.Block.Parent!);
statements = statements.Add(
SyntaxFactory.DoStatement(
SyntaxFactory.Block(GenerateBlockStatements(repeatStatement.Block)),
Expand Down
8 changes: 5 additions & 3 deletions Oberon0.Generator.MsilBin/MsilBinGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
#endregion

using System;
using System.Data;
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Oberon0.Compiler.Definitions;
using Oberon0.Compiler.Exceptions;
using Oberon0.Compiler.Types;
using Oberon0.Generator.MsilBin.PredefinedFunctions;
using Oberon0.Shared;
Expand Down Expand Up @@ -79,7 +79,7 @@ public void GenerateIntermediateCode()
{
if (Module == null)
{
throw new DataException("Please set Module before calling GenerateIntermediateCode()");
throw new InternalCompilerException("Please set Module before calling GenerateIntermediateCode()");
}

_compiledCode = SyntaxFactory.CompilationUnit();
Expand All @@ -92,7 +92,9 @@ public void GenerateIntermediateCode()
.NormalizeWhitespace();

// Add System using statement: (using System)
_namespace = _namespace.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System")),
_namespace = _namespace.AddUsings(
SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System")),
SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System.Linq")),
SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("AnyClone")));

GenerateClass();
Expand Down
Loading

0 comments on commit 42b260b

Please sign in to comment.