Skip to content

Commit

Permalink
(compiler) Add BigInt support for compiled mode (#418)
Browse files Browse the repository at this point in the history
See #415 for more design details.
  • Loading branch information
perlun authored Dec 30, 2023
1 parent 8e894cd commit d254df0
Show file tree
Hide file tree
Showing 18 changed files with 2,544 additions and 25 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ editor](https://github.com/mono/mono/blob/main/mcs/tools/csharp/getline.cs),
dual-licensed under the terms of the MIT X11 license or the Apache License 2.0.
Copyright (c) 2008 Novell, Inc. Copyright (c) 2016 Xamarin Inc.

[src/stdlib/src/bigint.hpp](src/stdlib/src/bigint.hpp) includes content from
Syed Faheel Ahmad's `BigInt` library, available at
https://github.com/faheel/BigInt, licensed under the terms of the MIT license.
Copyright (c) 2017 - 2018 Syed Faheel Ahmad.

[src/stdlib/src/double-conversion](src/stdlib/src/double-conversion) includes
content from the Google `double-conversion` library, available at
https://github.com/google/double-conversion, licensed under the BSD 3-Clause
Expand Down
19 changes: 19 additions & 0 deletions src/Perlang.Common/Compiler/CompilerFlags.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#pragma warning disable S2344
using System;

namespace Perlang.Compiler;

[Flags]
public enum CompilerFlags
{
/// <summary>
/// No compiler flags have been specified.
/// </summary>
None = 0,

/// <summary>
/// This flag disables all caching of compiled Perlang code, to ensure that all parts of the compilation is being
/// used.
/// </summary>
CacheDisabled = 1
}
4 changes: 2 additions & 2 deletions src/Perlang.Common/TypeReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ public Type? ClrType
var t when t == typeof(Double) => "double",
var t when t == typeof(bool) => "bool",
var t when t == typeof(void) => "void",
var t when t == typeof(BigInteger) => throw new NotImplementedInCompiledModeException("BigInteger is not yet supported in compiled mode"),
null => throw new InvalidOperationException($"Internal error: ClrType was unexpectedly null"),
var t when t == typeof(BigInteger) => "BigInt",
null => throw new InvalidOperationException("Internal error: ClrType was unexpectedly null"),

// TODO: Differentiate from these on the C++ level as well
var t when t.FullName == "Perlang.Lang.AsciiString" => "const char *",
Expand Down
3 changes: 2 additions & 1 deletion src/Perlang.ConsoleApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using System.Text;
using System.Threading.Tasks;
using Mono.Terminal;
using Perlang.Compiler;
using Perlang.Internal;
using Perlang.Interpreter;
using Perlang.Interpreter.Compiler;
Expand Down Expand Up @@ -452,7 +453,7 @@ internal int Run(string source, CompilerWarningHandler compilerWarningHandler)

private void CompileAndRun(string source, string path, CompilerWarningHandler compilerWarningHandler)
{
compiler.CompileAndRun(source, path, ScanError, ParseError, NameResolutionError, ValidationError, ValidationError, compilerWarningHandler);
compiler.CompileAndRun(source, path, CompilerFlags.None, ScanError, ParseError, NameResolutionError, ValidationError, ValidationError, compilerWarningHandler);
}

private void ParseAndPrint(string source)
Expand Down
31 changes: 27 additions & 4 deletions src/Perlang.Interpreter/Compiler/PerlangCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ private void RegisterGlobalClasses()
/// </summary>
/// <param name="source">The Perlang program to compile.</param>
/// <param name="path">The path to the source file (used for generating error messages).</param>
/// <param name="compilerFlags">One or more <see cref="CompilerFlags"/> to use.</param>
/// <param name="scanErrorHandler">A handler for scanner errors.</param>
/// <param name="parseErrorHandler">A handler for parse errors.</param>
/// <param name="nameResolutionErrorHandler">A handler for resolve errors.</param>
Expand All @@ -207,6 +208,7 @@ private void RegisterGlobalClasses()
public string? CompileAndRun(
string source,
string path,
CompilerFlags compilerFlags,
ScanErrorHandler scanErrorHandler,
ParseErrorHandler parseErrorHandler,
NameResolutionErrorHandler nameResolutionErrorHandler,
Expand All @@ -217,6 +219,7 @@ private void RegisterGlobalClasses()
string? executablePath = Compile(
source,
path,
compilerFlags,
scanErrorHandler,
parseErrorHandler,
nameResolutionErrorHandler,
Expand Down Expand Up @@ -271,6 +274,7 @@ private void RegisterGlobalClasses()
/// </summary>
/// <param name="source">The Perlang program to compile.</param>
/// <param name="path">The path to the source file (used for generating error messages).</param>
/// <param name="compilerFlags">One or more <see cref="CompilerFlags"/> to use.</param>
/// <param name="scanErrorHandler">A handler for scanner errors.</param>
/// <param name="parseErrorHandler">A handler for parse errors.</param>
/// <param name="nameResolutionErrorHandler">A handler for resolve errors.</param>
Expand All @@ -281,6 +285,7 @@ private void RegisterGlobalClasses()
private string? Compile(
string source,
string path,
CompilerFlags compilerFlags,
ScanErrorHandler scanErrorHandler,
ParseErrorHandler parseErrorHandler,
NameResolutionErrorHandler nameResolutionErrorHandler,
Expand All @@ -300,7 +305,7 @@ private void RegisterGlobalClasses()
// TODO: Check the creation time of *all* dependencies here, including the stdlib (both .so/.dll and .h files
// TODO: ideally). Right now, rebuilding the stdlib doesn't trigger a cache invalidation which is annoying
// TODO: when developing the stdlib.
if (!CompilationCacheDisabled &&
if (!(compilerFlags.HasFlag(CompilerFlags.CacheDisabled) || CompilationCacheDisabled) &&
File.GetCreationTime(targetCppFile) > File.GetCreationTime(path) &&
File.GetCreationTime(targetExecutable) > File.GetCreationTime(path))
{
Expand Down Expand Up @@ -457,6 +462,7 @@ private void RegisterGlobalClasses()
#include <math.h> // fmod()
#include <stdint.h>
#include ""bigint.hpp"" // BigInt
#include ""stdlib.hpp""
");
Expand Down Expand Up @@ -517,6 +523,18 @@ private void RegisterGlobalClasses()
"-Wall",
"-Werror",

// Enable warnings on e.g. narrowing conversion from `long long` to `unsigned long long`.
"-Wconversion",

// ...but do not warn on implicit conversion from `int` to `float` or `double`. For now, we are
// aiming at mimicking the C# semantics in this.
"-Wno-implicit-int-float-conversion",

// Certain narrowing conversions are problematic; we have seen this causing issues when implementing
// the BigInt support. For example, `uint64_t` must not be implicitly converted to call a `long
// long` constructor/method.
"-Wimplicit-int-conversion",

// C# allows cast from e.g. 2147483647 to float without warnings, but here's an interesting thing:
// clang emits a very nice warning for this ("implicit conversion from 'int' to 'float' changes
// value from 2147483647 to 2147483648"). We might want to consider doing something similar for
Expand All @@ -531,6 +549,10 @@ private void RegisterGlobalClasses()
// Handled on the Perlang side
"-Wno-logical-op-parentheses",

// Probably enabled by -Wconversion, but this causes issues with valid code like `2147483647 /
// 4294967295U`.
"-Wno-sign-conversion",

// We currently overflow without warnings, but we could consider implementing something like this
// warning in Perlang as well. Here's what the warning produces on `9223372036854775807 << 2`:
// error: signed shift result (0x1FFFFFFFFFFFFFFFC) requires 66 bits to represent, but 'long' only has 64 bits
Expand Down Expand Up @@ -1014,10 +1036,11 @@ public object VisitLiteralExpr(Expr.Literal expr)
throw new PerlangCompilerException($"Internal compiler error: unsupported floating point literal type {expr.Value.GetType().ToTypeKeyword()} encountered");
}
}
else if (expr.Value is IntegerLiteral<BigInteger>)
else if (expr.Value is IntegerLiteral<BigInteger> bigintLiteral)
{
// TODO: Use something like https://github.com/faheel/BigInt for this
throw new NotImplementedInCompiledModeException($"BigInteger literals is not yet implemented in compiled mode");
currentMethod.Append("BigInt(\"");
currentMethod.Append(bigintLiteral.Value.ToString());
currentMethod.Append("\")");
}
else if (expr.Value is INumericLiteral numericLiteral)
{
Expand Down
26 changes: 25 additions & 1 deletion src/Perlang.Tests.Integration/EvalHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,29 @@ internal static EvalResult<ValidationError> EvalWithValidationErrorCatch(string
/// result of the provided expression. If not provided a valid expression, <see cref="EvalResult{T}.Value"/>
/// will be set to `null`.</returns>
internal static EvalResult<Exception> EvalWithResult(string source, params string[] arguments)
{
return EvalWithResult(source, CompilerFlags.None, arguments);
}

/// <summary>
/// Evaluates the provided expression or list of statements, returning an <see cref="EvalResult{T}"/> with <see
/// cref="EvalResult{T}.Value"/> set to the evaluated value.
///
/// Output printed to the standard output stream will be available in <see cref="EvalResult{T}.Output"/>.
///
/// This method will propagate all kinds of errors to the caller, throwing an exception on the first error
/// encountered. If any warnings are emitted, they will be available in the returned <see
/// cref="EvalResult{T}.CompilerWarnings"/> property. This can be seen as "warnings as errors" is disabled
/// for all warnings; the caller need to explicitly check for warnings and fail if appropriate.
/// </summary>
/// <param name="source">A valid Perlang program.</param>
/// <param name="compilerFlags">One or more <see cref="CompilerFlags"/> to use if compilation is
/// enabled.</param>
/// <param name="arguments">Zero or more arguments to be passed to the program.</param>
/// <returns>An <see cref="EvalResult{T}"/> with the <see cref="EvalResult{T}.Value"/> property set to the
/// result of the provided expression. If not provided a valid expression, <see cref="EvalResult{T}.Value"/>
/// will be set to `null`.</returns>
internal static EvalResult<Exception> EvalWithResult(string source, CompilerFlags compilerFlags, params string[] arguments)
{
if (PerlangMode.ExperimentalCompilation)
{
Expand All @@ -254,6 +277,7 @@ internal static EvalResult<Exception> EvalWithResult(string source, params strin
result.ExecutablePath = compiler.CompileAndRun(
source,
CreateTemporaryPath(source),
compilerFlags,
AssertFailScanErrorHandler,
AssertFailParseErrorHandler,
AssertFailNameResolutionErrorHandler,
Expand All @@ -266,7 +290,7 @@ internal static EvalResult<Exception> EvalWithResult(string source, params strin
{
// This exception is thrown to make it possible for integration tests to skip tests for code which
// is known to not yet work.
throw new SkipException(e.Message);
throw new SkipException(e.Message, e);
}

// Return something else than `null` to make it reasonable for callers to distinguish that compiled mode
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Linq;
using FluentAssertions;
using Perlang.Compiler;
using Xunit;
using static Perlang.Tests.Integration.EvalHelper;

Expand All @@ -21,7 +22,7 @@ public void combined_and_and_or_operators_emits_expected_warning_for_statement()
var a = false && false || true;
";

var result = EvalWithResult(source);
var result = EvalWithResult(source, CompilerFlags.CacheDisabled);
var compilerWarning = result.CompilerWarnings.FirstOrDefault();

Assert.Single(result.CompilerWarnings);
Expand All @@ -37,7 +38,7 @@ public void combined_and_and_or_operators_emits_expected_warning_for_expression(
false && false || true
";

var result = EvalWithResult(source);
var result = EvalWithResult(source, CompilerFlags.CacheDisabled);
var compilerWarning = result.CompilerWarnings.FirstOrDefault();

Assert.Single(result.CompilerWarnings);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Perlang.Tests.Integration.Operator.Binary
{
public class AdditionAssignmentTests
{
[SkippableTheory]
[Theory]
[MemberData(nameof(BinaryOperatorData.AdditionAssignment_result), MemberType = typeof(BinaryOperatorData))]
public void performs_addition_assignment(string i, string j, string expectedResult)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ public static class BinaryOperatorData
new object[] { "9223372036854775807", "18446744073709551616", "false" },
new object[] { "9223372036854775807", "340282349999999991754788743781432688640.0f", "false" },
new object[] { "9223372036854775807", "12.0", "false" },
new object[] { "9223372036854775807", "9223372036854775807.0", "true" },
new object[] { "18446744073709551615", "2147483647", "false" },
new object[] { "18446744073709551615", "4294967295", "false" },
new object[] { "18446744073709551615", "9223372036854775807", "false" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace Perlang.Tests.Integration.Operator.Binary
// https://github.com/munificent/craftinginterpreters/blob/c6da0e61e6072271de404464c34b51c2fdc39e59/test/operator/divide_num_nonnum.lox
public class DivisionTests
{
[SkippableTheory]
[Theory]
[MemberData(nameof(BinaryOperatorData.Division_result), MemberType = typeof(BinaryOperatorData))]
void performs_division(string i, string j, string expectedResult)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class MultiplicationTests
//
// Tests for the * (multiplication) operator
//
[SkippableTheory]
[Theory]
[MemberData(nameof(BinaryOperatorData.Multiplication_result), MemberType = typeof(BinaryOperatorData))]
public void performs_multiplication(string i, string j, string expectedResult)
{
Expand Down
7 changes: 4 additions & 3 deletions src/Perlang.Tests.Integration/Typing/TypingTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma warning disable SA1025
using System;
using System.Linq;
using Perlang.Compiler;
using Xunit;
using static Perlang.Tests.Integration.EvalHelper;

Expand Down Expand Up @@ -198,7 +199,7 @@ public void var_declaration_with_initializer_emits_warning_on_null_usage()
print s;
";

var result = EvalWithResult(source);
var result = EvalWithResult(source, CompilerFlags.CacheDisabled);

Assert.Empty(result.Errors);
Assert.Single(result.CompilerWarnings);
Expand Down Expand Up @@ -263,7 +264,7 @@ public void var_declaration_emits_warning_on_reassignment_to_null_for_reference_
print s;
";

EvalResult<Exception> result = EvalWithResult(source);
EvalResult<Exception> result = EvalWithResult(source, CompilerFlags.CacheDisabled);

Assert.Empty(result.Errors);
Assert.Single(result.CompilerWarnings);
Expand Down Expand Up @@ -350,7 +351,7 @@ fun foo(s: String): void {
foo(null);
";

var result = EvalWithResult(source);
var result = EvalWithResult(source, CompilerFlags.CacheDisabled);

Assert.Empty(result.Errors);
Assert.Single(result.CompilerWarnings);
Expand Down
10 changes: 6 additions & 4 deletions src/stdlib/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ cmake_minimum_required(VERSION 3.0)
project(stdlib VERSION 0.1.0)

set(headers
src/bigint.hpp
src/stdlib.hpp
src/double-conversion/utils.h
)

add_library(
stdlib
src/bigint.cpp
src/Base64.cpp
src/print.cpp
)
Expand Down Expand Up @@ -35,7 +37,7 @@ install(
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
)

# The testing code is not yet ready for macOS, since it uses a different linker which doesnät support --wrap. We'll
# The testing code is not yet ready for macOS, since it uses a different linker which doesn't support --wrap. We'll
# live with only building/running the tests on other platforms for now.
if (NOT ${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
enable_testing()
Expand All @@ -58,9 +60,9 @@ if (NOT ${CMAKE_SYSTEM_NAME} MATCHES "Darwin")

target_link_libraries(cctest stdlib)

# We enable wrapping for fwrite(), to be able to capture its output in tests. The reason why we don't do this on
# macOS is because it does not use GNU ld, so the --wrap linker option isn't available.
target_link_options(cctest PRIVATE -Wl,--wrap=fwrite)
# We enable wrapping for fwrite(), to be able to capture its output in tests. The reason why we don't do this on
# macOS is because it does not use GNU ld, so the --wrap linker option isn't available.
target_link_options(cctest PRIVATE -Wl,--wrap=fwrite)

add_test(NAME test_fast_dtoa
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
Expand Down
Loading

0 comments on commit d254df0

Please sign in to comment.