Skip to content

Commit

Permalink
(stdlib) Add basic StringBuilder class
Browse files Browse the repository at this point in the history
...and use it everywhere instead of the .NET class.

This can be used to construct Perlang strings with dynamic content. The
`StringBuilder` class uses a byte buffer behind the scenes, allowing the
string to be mutated until the user is "finished" with it. Once that
happens, an (immutable) string is produced which is then carried along
in the program.

https://gitlab.perlang.org/perlang/perlang/-/merge_requests/548
  • Loading branch information
perlun committed Oct 30, 2024
1 parent 21ab6e7 commit fa14166
Show file tree
Hide file tree
Showing 19 changed files with 355 additions and 56 deletions.
5 changes: 5 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
root = true

[*.cs]
# CA2000: Dispose objects before losing scope
dotnet_diagnostic.CA2000.severity = error
1 change: 1 addition & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ test:
script:
- make auto-generated
- make stdlib
- make perlang_cli_install_integration_test_release
- >
if [[ "$TEST_MODE" == 'valgrind' ]]; then
PERLANG_RUN_WITH_VALGRIND=true dotnet test --configuration Release --verbosity minimal --logger "junit;LogFilePath=perlang-junit-log.xml";
Expand Down
16 changes: 13 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# src/Perlang.Common/CommonConstants.Generated.cs is not phony, but we always want
# to force it to be regenerated.
.PHONY: \
all auto-generated clean darkerfx-push docs docs-serve docs-test-examples \
docs-validate-api-docs install install-latest-snapshot perlang_cli \
perlang_cli_clean perlang_cli_install_debug perlang_cli_install_release perlang_cli_install_integration_test_debug \
all auto-generated clean darkerfx-push docs docs-serve docs-test-examples valgrind-docs-test-examples \
docs-validate-api-docs install install-latest-snapshot perlang_cli perlang_cli_clean perlang_cli_install_debug \
perlang_cli_install_release perlang_cli_install_integration_test_debug perlang_cli_install_integration_test_release \
publish-release release run run-hello-world-example valgrind-perlang-run-hello-world-example valgrind-hello-world-example \
test test-stdlib src/Perlang.Common/CommonConstants.Generated.cs

Expand Down Expand Up @@ -61,6 +61,10 @@ docs-test-examples: perlang_cli_install_release
for e in docs/examples/quickstart/*.per ; do echo -e \\n\\e[1m[$$e]\\e[0m ; $(RELEASE_PERLANG) $$e ; done
for e in docs/examples/the-language/*.per ; do echo -e \\n\\e[1m[$$e]\\e[0m ; $(RELEASE_PERLANG) $$e ; done

valgrind-docs-test-examples: perlang_cli_install_release
for e in docs/examples/quickstart/*.per ; do echo -e \\n\\e[1m[$$e]\\e[0m ; $(VALGRIND) $(DEBUG_PERLANG) $$e ; done
for e in docs/examples/the-language/*.per ; do echo -e \\n\\e[1m[$$e]\\e[0m ; $(VALGRIND) $(DEBUG_PERLANG) $$e ; done

docs-serve:
live-server _site

Expand Down Expand Up @@ -142,6 +146,12 @@ perlang_cli_install_integration_test_debug: perlang_cli
mkdir -p src/Perlang.Tests.Integration/bin/Debug/net8.0
cp lib/perlang_cli/lib/perlang_cli.so src/Perlang.Tests.Integration/bin/Debug/net8.0

perlang_cli_install_integration_test_release: perlang_cli
mkdir -p src/Perlang.Tests/bin/Release/net8.0
mkdir -p src/Perlang.Tests.Integration/bin/Release/net8.0
cp lib/perlang_cli/lib/perlang_cli.so src/Perlang.Tests/bin/Release/net8.0
cp lib/perlang_cli/lib/perlang_cli.so src/Perlang.Tests.Integration/bin/Release/net8.0

test:
dotnet test --configuration Release

Expand Down
1 change: 1 addition & 0 deletions global.ruleset
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
<Rule Id="S3260" Action="None"/> <!-- Private classes or records which are not derived in the current assembly should be marked as 'sealed' -->
<Rule Id="S3267" Action="None"/> <!-- Loops should be simplified with "LINQ" expressions -->
<Rule Id="S3376" Action="None"/>
<Rule Id="S3881" Action="None"/> <!-- "IDisposable" should be implemented correctly -->
<Rule Id="S3925" Action="None"/>
</Rules>
</RuleSet>
18 changes: 11 additions & 7 deletions src/Perlang.ConsoleApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using System.CommandLine.Parsing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Perlang.Compiler;
using Perlang.Interpreter;
Expand All @@ -24,7 +23,7 @@

namespace Perlang.ConsoleApp
{
public class Program
public class Program : IDisposable
{
internal enum ExitCodes
{
Expand Down Expand Up @@ -179,7 +178,7 @@ public static int Main(string[] args)
string? outputFileName = parseResult.GetValueForOption(outputOption);
bool idempotent = parseResult.GetValueForOption(idempotentOption);
var program = new Program(
using var program = new Program(
replMode: false,
standardOutputHandler: Console.WriteLine,
disabledWarningsAsErrors: disabledWarningsAsErrorsList,
Expand All @@ -197,7 +196,7 @@ public static int Main(string[] args)
if (parseResult.Tokens.Count == 1)
{
var program = new Program(
using var program = new Program(
replMode: false,
standardOutputHandler: Console.WriteLine,
disabledWarningsAsErrors: disabledWarningsAsErrorsList,
Expand All @@ -210,7 +209,7 @@ public static int Main(string[] args)
{
var remainingArguments = parseResult.GetValueForArgument(scriptArguments);
var program = new Program(
using var program = new Program(
replMode: false,
arguments: remainingArguments,
standardOutputHandler: Console.WriteLine,
Expand Down Expand Up @@ -241,7 +240,7 @@ public static int Main(string[] args)
.Invoke(args);
}

internal Program(
private Program(
bool replMode,
Action<Lang.String> standardOutputHandler,
IEnumerable<string>? arguments = null,
Expand All @@ -265,6 +264,11 @@ internal Program(
);
}

public void Dispose()
{
compiler.Dispose();
}

private int RunFile(string path, string? outputPath)
{
if (!File.Exists(path))
Expand Down Expand Up @@ -293,7 +297,7 @@ private int RunFile(string path, string? outputPath)

private int CompileAndAssembleFile(string[] scriptFiles, string? targetPath, bool idempotent)
{
var completeSource = new StringBuilder();
using var completeSource = NativeStringBuilder.Create();

foreach (string scriptFile in scriptFiles)
{
Expand Down
69 changes: 41 additions & 28 deletions src/Perlang.Interpreter/Compiler/PerlangCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
using System.Linq;
using System.Numerics;
using System.Reflection;
using System.Text;
using Perlang.Attributes;
using Perlang.Compiler;
using Perlang.Exceptions;
Expand All @@ -22,6 +21,7 @@
using Perlang.Interpreter.NameResolution;
using Perlang.Interpreter.Typing;
using Perlang.Lang;
using Perlang.Native;
using Perlang.Parser;
using Perlang.Stdlib;
using static Perlang.TokenType;
Expand Down Expand Up @@ -58,7 +58,7 @@ namespace Perlang.Interpreter.Compiler;
/// Also, this class is not thread safe. A single instance of this class cannot safely be used from multiple
/// threads.</remarks>
/// </summary>
public class PerlangCompiler : Expr.IVisitor<object?>, Stmt.IVisitor<object>
public class PerlangCompiler : Expr.IVisitor<object?>, Stmt.IVisitor<object>, IDisposable
{
/// <summary>
/// Gets a value indicating whether caching of compiled results should be disabled or not.
Expand Down Expand Up @@ -106,7 +106,7 @@ private static bool CompilationCacheDisabled
/// <summary>
/// The location in the output we are currently writing to. Can be an enum, function, etc.
/// </summary>
private readonly StringBuilder mainMethodContent;
private readonly NativeStringBuilder mainMethodContent;

private int indentationLevel = 1;

Expand All @@ -128,7 +128,8 @@ public PerlangCompiler(
this.standardOutputHandler = standardOutputHandler;
this.BindingHandler = bindingHandler ?? new BindingHandler();

this.mainMethodContent = new StringBuilder();
this.mainMethodContent = NativeStringBuilder.Create();

this.methods = new Dictionary<string, Method>();
this.enums = new Dictionary<string, string>();

Expand All @@ -142,6 +143,15 @@ public PerlangCompiler(
nativeClasses = RegisterGlobalFunctionsAndClasses();
}

public void Dispose()
{
mainMethodContent.Dispose();

foreach (Method method in methods.Values) {
method.MethodBody.Dispose();
}
}

private IImmutableDictionary<string, Type> CreateSuperGlobals(ImmutableList<string> argumentsList)
{
// Set up the super-global ARGV variable.
Expand Down Expand Up @@ -824,7 +834,7 @@ private void RegisterGlobalClasses()
/// <returns>The C++ source for the given statements.</returns>
private string Compile(IEnumerable<Stmt> statements)
{
var result = new StringBuilder();
using var result = NativeStringBuilder.Create();

foreach (Stmt statement in statements)
{
Expand Down Expand Up @@ -857,7 +867,7 @@ public object VisitAssignExpr(Expr.Assign expr)

public object VisitBinaryExpr(Expr.Binary expr)
{
var result = new StringBuilder();
using var result = NativeStringBuilder.Create();
string? leftCast = null;
string? rightCast = null;

Expand Down Expand Up @@ -1206,7 +1216,7 @@ public object VisitCallExpr(Expr.Call expr)
// TODO: binding at an earlier stage and e.g. validate the number and type of arguments with the method
// TODO: definition. Right now, we'll leave this to the C++ compiler to deal with...

var result = new StringBuilder();
using var result = NativeStringBuilder.Create();

result.Append(expr.Callee.Accept(this));
result.Append('(');
Expand Down Expand Up @@ -1237,7 +1247,7 @@ public object VisitIndexExpr(Expr.Index expr)
// TODO: One way to deal with this: make `foo[bar]` be safe and add an "unsafe" indexing operator `foo[[bar]]`
// TODO: which can be used when the user believes he knows best. Investigate how Rust is doing this.

var result = new StringBuilder();
using var result = NativeStringBuilder.Create();

ITypeReference indexeeTypeReference = expr.Indexee.TypeReference;

Expand All @@ -1262,7 +1272,7 @@ public object VisitIndexExpr(Expr.Index expr)

public object VisitGroupingExpr(Expr.Grouping expr)
{
var result = new StringBuilder();
using var result = NativeStringBuilder.Create();

result.Append('(');
result.Append(expr.Expression.Accept(this));
Expand All @@ -1273,7 +1283,7 @@ public object VisitGroupingExpr(Expr.Grouping expr)

public object VisitCollectionInitializerExpr(Expr.CollectionInitializer collectionInitializer)
{
var result = new StringBuilder();
using var result = NativeStringBuilder.Create();

result.Append($"std::make_shared<{collectionInitializer.TypeReference.CppType}>(");

Expand Down Expand Up @@ -1309,7 +1319,7 @@ public object VisitCollectionInitializerExpr(Expr.CollectionInitializer collecti

public object VisitLiteralExpr(Expr.Literal expr)
{
var result = new StringBuilder();
using var result = NativeStringBuilder.Create();

if (expr.Value is AsciiString)
{
Expand Down Expand Up @@ -1393,7 +1403,7 @@ public object VisitLiteralExpr(Expr.Literal expr)

public object VisitLogicalExpr(Expr.Logical expr)
{
var result = new StringBuilder();
using var result = NativeStringBuilder.Create();

switch (expr.Operator.Type)
{
Expand All @@ -1419,7 +1429,7 @@ public object VisitLogicalExpr(Expr.Logical expr)

public object VisitUnaryPrefixExpr(Expr.UnaryPrefix expr)
{
var result = new StringBuilder();
using var result = NativeStringBuilder.Create();

switch (expr.Operator.Type)
{
Expand All @@ -1443,7 +1453,7 @@ public object VisitUnaryPrefixExpr(Expr.UnaryPrefix expr)

public object VisitUnaryPostfixExpr(Expr.UnaryPostfix expr)
{
var result = new StringBuilder();
using var result = NativeStringBuilder.Create();

switch (expr.Operator.Type)
{
Expand All @@ -1470,7 +1480,7 @@ public object VisitIdentifierExpr(Expr.Identifier expr) =>

public object VisitGetExpr(Expr.Get expr)
{
var result = new StringBuilder();
using var result = NativeStringBuilder.Create();

if (expr.Object is Expr.Identifier identifier)
{
Expand Down Expand Up @@ -1530,7 +1540,7 @@ public object VisitGetExpr(Expr.Get expr)

public object VisitBlockStmt(Stmt.Block block)
{
var result = new StringBuilder();
using var result = NativeStringBuilder.Create();

result.Append(Indent(indentationLevel));
result.AppendLine("{");
Expand All @@ -1555,7 +1565,8 @@ public object VisitClassStmt(Stmt.Class stmt)

public object VisitEnumStmt(Stmt.Enum stmt)
{
var stringBuilder = new StringBuilder();
using var stringBuilder = NativeStringBuilder.Create();

int localIndentationLevel = 0;

// The "enum class" concept as defined in C++11 would be preferable here since it provides better type safety, but
Expand All @@ -1571,8 +1582,7 @@ public object VisitEnumStmt(Stmt.Enum stmt)
stringBuilder.AppendLine($"enum {stmt.Name.Lexeme} {{");
localIndentationLevel++;

foreach (KeyValuePair<string, Expr?> enumMember in stmt.Members)
{
foreach (KeyValuePair<string, Expr?> enumMember in stmt.Members) {
stringBuilder.Append(Indent(localIndentationLevel));
stringBuilder.Append(enumMember.Key);

Expand All @@ -1594,15 +1604,15 @@ public object VisitEnumStmt(Stmt.Enum stmt)

enums[stmt.Name.Lexeme] = stringBuilder.ToString();

// Does not need to return the StringBuilder here, since it's been stored in the methods dictionary already.
// Does not need to return the StringBuilder here, since it's been stored in the enums dictionary already.
return VoidObject.Void;
}

public object VisitExpressionStmt(Stmt.ExpressionStmt stmt)
{
var result = new StringBuilder();
using var result = NativeStringBuilder.Create();

// We only emit the "wrapping" (whitespace before + semicolon and newline after expression) in this case; the
// We only emit the "wrapping" (whitespace before + semicolon and newline after expression) in this case; the
// rest comes from visiting the expression itself
result.Append(Indent(indentationLevel));
result.Append(stmt.Expression.Accept(this));
Expand All @@ -1613,7 +1623,10 @@ public object VisitExpressionStmt(Stmt.ExpressionStmt stmt)

public object VisitFunctionStmt(Stmt.Function functionStmt)
{
var functionContent = new StringBuilder();
// Disposed via Dispose() method for class
#pragma warning disable CA2000
var functionContent = NativeStringBuilder.Create();
#pragma warning restore CA2000

if (methods.ContainsKey(functionStmt.Name.Lexeme))
{
Expand Down Expand Up @@ -1641,7 +1654,7 @@ public object VisitFunctionStmt(Stmt.Function functionStmt)

public object VisitIfStmt(Stmt.If stmt)
{
var result = new StringBuilder();
using var result = NativeStringBuilder.Create();

result.Append(Indent(indentationLevel));
result.Append($"if ({stmt.Condition.Accept(this)}) ");
Expand Down Expand Up @@ -1678,7 +1691,7 @@ public object VisitPrintStmt(Stmt.Print stmt) =>

public object VisitReturnStmt(Stmt.Return stmt)
{
var result = new StringBuilder();
using var result = NativeStringBuilder.Create();

if (stmt.Value == null)
{
Expand All @@ -1697,7 +1710,7 @@ public object VisitReturnStmt(Stmt.Return stmt)

public object VisitVarStmt(Stmt.Var stmt)
{
var result = new StringBuilder();
using var result = NativeStringBuilder.Create();

string variableName = stmt.Name.Lexeme;

Expand All @@ -1717,7 +1730,7 @@ public object VisitVarStmt(Stmt.Var stmt)

public object VisitWhileStmt(Stmt.While whileStmt)
{
var result = new StringBuilder();
using var result = NativeStringBuilder.Create();

result.Append(Indent(indentationLevel));
result.Append($"while ({whileStmt.Condition.Accept(this)}) ");
Expand All @@ -1729,7 +1742,7 @@ public object VisitWhileStmt(Stmt.While whileStmt)

private static string Indent(int level) => String.Empty.PadLeft(level * 4);

private record Method(string Name, IImmutableList<Parameter> Parameters, string ReturnType, StringBuilder MethodBody)
private record Method(string Name, IImmutableList<Parameter> Parameters, string ReturnType, NativeStringBuilder MethodBody)
{
/// <summary>
/// Gets the method parameters as a comma-separated string, in C++ format. Example: `int foo, int bar`.
Expand Down
Loading

0 comments on commit fa14166

Please sign in to comment.