Skip to content

Commit

Permalink
(many) Support string+float/double and float/double+string concatenat…
Browse files Browse the repository at this point in the history
…ion (#477)
  • Loading branch information
perlun authored May 11, 2024
1 parent 993dc82 commit db6d935
Show file tree
Hide file tree
Showing 12 changed files with 252 additions and 35 deletions.
2 changes: 2 additions & 0 deletions release-notes/v0.5.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
- Support more types in string+int and int+string concatenation [[#473][473]]
- Fix ASCIIString+ASCIIString concatenation to return ASCIIString [[#474][474]]
- Support string+bigint and bigint+string concatenation [[#475][475]]
- Support string+float/double and float/double+string concatenation [[#477][477]]

### Changed
#### Data types
Expand Down Expand Up @@ -65,3 +66,4 @@
[474]: https://github.com/perlang-org/perlang/pull/474
[475]: https://github.com/perlang-org/perlang/pull/475
[476]: https://github.com/perlang-org/perlang/pull/476
[477]: https://github.com/perlang-org/perlang/pull/477
12 changes: 6 additions & 6 deletions src/Perlang.Interpreter/Typing/TypeResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,36 +127,36 @@ public override VoidObject VisitBinaryExpr(Expr.Binary expr)
}
else if (expr.Operator.Type == TokenType.PLUS &&
(leftTypeReference.ClrType == typeof(Lang.String) &&
new[] { typeof(int), typeof(long), typeof(uint), typeof(ulong), typeof(BigInteger) }.Contains(rightTypeReference.ClrType))) {
new[] { typeof(int), typeof(long), typeof(uint), typeof(ulong), typeof(BigInteger), typeof(float), typeof(double) }.Contains(rightTypeReference.ClrType))) {
// "string" + 42
expr.TypeReference.ClrType = typeof(Lang.String);
}
else if (expr.Operator.Type == TokenType.PLUS &&
(leftTypeReference.ClrType == typeof(AsciiString) &&
new[] { typeof(int), typeof(long), typeof(uint), typeof(ulong), typeof(BigInteger) }.Contains(rightTypeReference.ClrType))) {
new[] { typeof(int), typeof(long), typeof(uint), typeof(ulong), typeof(BigInteger), typeof(float), typeof(double) }.Contains(rightTypeReference.ClrType))) {
// "string" + 42
expr.TypeReference.ClrType = typeof(AsciiString);
}
else if (expr.Operator.Type == TokenType.PLUS &&
(leftTypeReference.ClrType == typeof(Utf8String) &&
new[] { typeof(int), typeof(long), typeof(uint), typeof(ulong), typeof(BigInteger) }.Contains(rightTypeReference.ClrType))) {
new[] { typeof(int), typeof(long), typeof(uint), typeof(ulong), typeof(BigInteger), typeof(float), typeof(double) }.Contains(rightTypeReference.ClrType))) {
// "åäö string" + 42
expr.TypeReference.ClrType = typeof(Utf8String);
}
else if (expr.Operator.Type == TokenType.PLUS &&
(new[] { typeof(int), typeof(long), typeof(uint), typeof(ulong), typeof(BigInteger) }.Contains(leftTypeReference.ClrType) &&
(new[] { typeof(int), typeof(long), typeof(uint), typeof(ulong), typeof(BigInteger), typeof(float), typeof(double) }.Contains(leftTypeReference.ClrType) &&
rightTypeReference.ClrType == typeof(Lang.String))) {
// 42 + "string"
expr.TypeReference.ClrType = typeof(Lang.String);
}
else if (expr.Operator.Type == TokenType.PLUS &&
(new[] { typeof(int), typeof(long), typeof(uint), typeof(ulong), typeof(BigInteger) }.Contains(leftTypeReference.ClrType) &&
(new[] { typeof(int), typeof(long), typeof(uint), typeof(ulong), typeof(BigInteger), typeof(float), typeof(double) }.Contains(leftTypeReference.ClrType) &&
rightTypeReference.ClrType == typeof(AsciiString))) {
// 42 + "string" + 42
expr.TypeReference.ClrType = typeof(AsciiString);
}
else if (expr.Operator.Type == TokenType.PLUS &&
(new[] { typeof(int), typeof(long), typeof(uint), typeof(ulong), typeof(BigInteger) }.Contains(leftTypeReference.ClrType) &&
(new[] { typeof(int), typeof(long), typeof(uint), typeof(ulong), typeof(BigInteger), typeof(float), typeof(double) }.Contains(leftTypeReference.ClrType) &&
rightTypeReference.ClrType == typeof(Utf8String))) {
// 42 + "åäö string"
expr.TypeReference.ClrType = typeof(Utf8String);
Expand Down
64 changes: 49 additions & 15 deletions src/Perlang.Tests.Integration/Operator/Binary/AdditionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -279,46 +279,80 @@ void addition_of_string_and_bigint_coerces_number_to_string()
.Be("abc18446744073709551616");
}

[SkippableTheory]
[Theory]
[ClassData(typeof(TestCultures))]
async Task addition_of_float_and_string_coerces_number_to_string(CultureInfo cultureInfo)
async Task addition_of_float_and_ascii_string_coerces_number_to_string(CultureInfo cultureInfo)
{
Skip.If(PerlangMode.ExperimentalCompilation, "float+string is not yet supported in compiled mode");

CultureInfo.CurrentCulture = cultureInfo;

string source = @"
var i = 123.45;
var s = ""abc"";
string source = """
var i = 123.45f;
var s = "abc";

print i + s;
";
""";

string result = EvalReturningOutputString(source);

result.Should()
.Be("123.45abc");
}

[SkippableTheory]
[Theory]
[ClassData(typeof(TestCultures))]
async Task addition_of_string_and_float_coerces_number_to_string(CultureInfo cultureInfo)
async Task addition_of_float_and_utf8_string_coerces_number_to_string(CultureInfo cultureInfo)
{
Skip.If(PerlangMode.ExperimentalCompilation, "string+float is not yet supported in compiled mode");
CultureInfo.CurrentCulture = cultureInfo;

string source = """
var i = 123.45f;
var s = "abcåäö";

print i + s;
""";

string result = EvalReturningOutputString(source);

result.Should()
.Be("123.45abcåäö");
}

[Theory]
[ClassData(typeof(TestCultures))]
async Task addition_of_ascii_string_and_float_coerces_number_to_string(CultureInfo cultureInfo)
{
CultureInfo.CurrentCulture = cultureInfo;

string source = @"
var s = ""abc"";
var i = 123.45;
string source = """
var s = "abc";
var i = 123.45f;

print s + i;
";
""";

string result = EvalReturningOutputString(source);

result.Should()
.Be("abc123.45");
}

[Theory]
[ClassData(typeof(TestCultures))]
async Task addition_of_utf8_string_and_float_coerces_number_to_string(CultureInfo cultureInfo)
{
CultureInfo.CurrentCulture = cultureInfo;

string source = """
var s = "abcåäö";
var i = 123.45f;

print s + i;
""";

string result = EvalReturningOutputString(source);

result.Should()
.Be("abcåäö123.45");
}
}
}
8 changes: 2 additions & 6 deletions src/Perlang.Tests.Integration/Typing/StringTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,9 @@ public void ascii_string_can_be_concatenated_with_int_and_ascii_string()
.Be("temperature is 85 degrees fahrenheit");
}

[SkippableFact]
[Fact]
public void ascii_string_can_be_concatenated_with_double_and_string()
{
Skip.If(PerlangMode.ExperimentalCompilation, "string+double is not yet supported in compiled mode");

string source = """
var s1: string = "temperature is ";
var i: double = 85.2;
Expand All @@ -209,11 +207,9 @@ public void ascii_string_can_be_concatenated_with_double_and_string()
.Be("temperature is 85.2 degrees fahrenheit");
}

[SkippableFact]
[Fact]
public void utf8_string_can_be_concatenated_with_double_and_ascii_string()
{
Skip.If(PerlangMode.ExperimentalCompilation, "string+double is not yet supported in compiled mode");

string source = """
var s1: string = "Den årliga medeltemperaturen i Vasa är ";
var d: double = 3.4;
Expand Down
3 changes: 2 additions & 1 deletion src/stdlib/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
cmake_minimum_required(VERSION 3.0)
cmake_minimum_required(VERSION 3.5)
project(stdlib VERSION 0.1.0)

set(headers
src/ascii_string.h
src/bigint.h
src/internal/string_utils.h
src/perlang_stdlib.h
src/perlang_string.h
src/utf8_string.h
Expand Down
57 changes: 57 additions & 0 deletions src/stdlib/src/ascii_string.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include "ascii_string.h"
#include "bigint.h"
#include "internal/string_utils.h"

namespace perlang
{
Expand Down Expand Up @@ -151,6 +152,34 @@ namespace perlang
return from_owned_string(bytes, length);
}

std::shared_ptr<const String> ASCIIString::operator+(float rhs) const
{
std::string str = internal::float_to_string(rhs);

size_t length = str.length() + this->length_;
char *bytes = (char*)malloc(length + 1);

memcpy(bytes, this->bytes_, this->length_);
memcpy((bytes + this->length_), str.c_str(), str.length());
bytes[length] = '\0';

return from_owned_string(bytes, length);
}

std::shared_ptr<const String> ASCIIString::operator+(double rhs) const
{
std::string str = internal::double_to_string(rhs);

size_t length = str.length() + this->length_;
char *bytes = (char*)malloc(length + 1);

memcpy(bytes, this->bytes_, this->length_);
memcpy((bytes + this->length_), str.c_str(), str.length());
bytes[length] = '\0';

return from_owned_string(bytes, length);
}

std::shared_ptr<const String> ASCIIString::operator+(const BigInt& rhs) const
{
std::string str = rhs.to_string();
Expand Down Expand Up @@ -198,4 +227,32 @@ namespace perlang

return ASCIIString::from_owned_string(bytes, length);
}

std::shared_ptr<const ASCIIString> operator+(const float lhs, const ASCIIString& rhs)
{
std::string str = internal::float_to_string(lhs);

size_t length = str.length() + rhs.length();
char *bytes = (char*)malloc(length + 1);

memcpy(bytes, str.c_str(), str.length());
memcpy((bytes + str.length()), rhs.bytes(), rhs.length());
bytes[length] = '\0';

return ASCIIString::from_owned_string(bytes, length);
}

std::shared_ptr<const ASCIIString> operator+(const double lhs, const ASCIIString& rhs)
{
std::string str = internal::double_to_string(lhs);

size_t length = str.length() + rhs.length();
char *bytes = (char*)malloc(length + 1);

memcpy(bytes, str.c_str(), str.length());
memcpy((bytes + str.length()), rhs.bytes(), rhs.length());
bytes[length] = '\0';

return ASCIIString::from_owned_string(bytes, length);
}
}
18 changes: 18 additions & 0 deletions src/stdlib/src/ascii_string.h
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ namespace perlang
[[nodiscard]]
std::shared_ptr<const String> operator+(uint64_t rhs) const override;

// Concatenates this string with a float. The memory for the new string is allocated from the heap.
[[nodiscard]]
std::shared_ptr<const String> operator+(float rhs) const override;

// Concatenates this string with a double. The memory for the new string is allocated from the heap.
[[nodiscard]]
std::shared_ptr<const String> operator+(double rhs) const override;

// Concatenates this string with a BigInt. The memory for the new string is allocated from the heap.
[[nodiscard]]
std::shared_ptr<const String> operator+(const BigInt& rhs) const override;
Expand Down Expand Up @@ -132,6 +140,16 @@ namespace perlang
[[nodiscard]]
std::shared_ptr<const ASCIIString> operator+(uint64_t lhs, const ASCIIString& rhs);

// Concatenate a float+ASCIIString. The memory for the new string is allocated from the heap. This is a free
// function, since the left-hand side is not an ASCIIString.
[[nodiscard]]
std::shared_ptr<const ASCIIString> operator+(float lhs, const ASCIIString& rhs);

// Concatenate a double+ASCIIString. The memory for the new string is allocated from the heap. This is a free
// function, since the left-hand side is not an ASCIIString.
[[nodiscard]]
std::shared_ptr<const ASCIIString> operator+(double lhs, const ASCIIString& rhs);

// Note: must come after the operator+ declarations above, since they are used in the following declarations. In
// C++, the order of function declaration matters.

Expand Down
23 changes: 23 additions & 0 deletions src/stdlib/src/internal/string_utils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#pragma once

#include <string>

// fmt is an open-source formatting library providing a fast and safe alternative to C stdio and C++ iostreams.
// https://github.com/fmtlib/fmt
#define FMT_HEADER_ONLY
#include "../fmt/format.h"

namespace perlang::internal
{
inline std::string float_to_string(const float lhs)
{
// Use the same precision as in C#
return fmt::format("{:.7G}", lhs);
}

inline std::string double_to_string(const double lhs)
{
// Use the same precision as in C#
return fmt::format("{:.15G}", lhs);
}
}
8 changes: 8 additions & 0 deletions src/stdlib/src/perlang_string.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ namespace perlang
[[nodiscard]]
virtual std::shared_ptr<const String> operator+(uint64_t rhs) const = 0;

// Concatenates this string with a float. The memory for the new string is allocated from the heap.
[[nodiscard]]
virtual std::shared_ptr<const String> operator+(float rhs) const = 0;

// Concatenates this string with a double. The memory for the new string is allocated from the heap.
[[nodiscard]]
virtual std::shared_ptr<const String> operator+(double rhs) const = 0;

// Concatenates this string with a BigInt. The memory for the new string is allocated from the heap.
[[nodiscard]]
virtual std::shared_ptr<const String> operator+(const BigInt& rhs) const = 0;
Expand Down
15 changes: 8 additions & 7 deletions src/stdlib/src/print.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,21 @@

#include "ascii_string.h"
#include "bigint.h"
#include "internal/string_utils.h"

namespace perlang
{
void print(const String* str)
{
// Safeguard against both `str` and `str->bytes()` potentially returning `null`
const char* bytes = str != nullptr ? str->bytes() : nullptr;

// Safeguard against both `str` and `str->bytes()` potentially returning `null`
if (bytes == nullptr) {
puts("null");
}
else {
// For plain strings, there's no need to use the overhead which `printf` induces. `puts` can potentially be a
// tiny bit faster.
// For C-style (NUL-terminated, char*) strings, there's no need to use the overhead which `printf` induces.
// `puts` can potentially be a tiny bit faster.
puts(bytes);
}
}
Expand Down Expand Up @@ -82,13 +83,13 @@ namespace perlang

void print(float f)
{
// Use the same precision as on the C# side
fmt::println("{:.7G}", f);
const std::string& str = internal::float_to_string(f) + "\n";
fwrite(str.c_str(), str.length(), 1, stdout);
}

void print(double d)
{
// Use the same precision as on the C# side
fmt::println("{:.15G}", d);
const std::string& str = internal::double_to_string(d) + "\n";
fwrite(str.c_str(), str.length(), 1, stdout);
}
}
Loading

0 comments on commit db6d935

Please sign in to comment.