Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions Coder.Test/Languages/IntegerDivisionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Coder.Test.Languages;

using System.Globalization;
using System.Text;
using ktsu.Coder.Ast;
using ktsu.Coder.Languages;
using Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// Tests that integer division and remainder mean the same thing in Python and JavaScript as in the
/// C family.
/// </summary>
/// <remarks>
/// The AST means <c>-7 / 2</c> on two integers as truncating division, which is <c>-3</c> in C, C++,
/// C#, Go and Rust. Python's and JavaScript's <c>/</c> are true division and give <c>-3.5</c>, and
/// Python's <c>%</c> floors, giving <c>1</c> for <c>-7 % 2</c> where the C family gives <c>-1</c>.
/// The generated code is run rather than read, because the spelling of a correct quotient is a choice
/// and its value is not.
/// </remarks>
[TestClass]
public class IntegerDivisionTests
{
private static readonly (int Left, int Right)[] Operands = [(-7, 2), (7, -2), (-7, -2), (7, 2), (-6, 3), (0, -5)];

/// <summary>
/// The functions under test: integer parameters, integer literals, a typed local, and an operand
/// that is a call, which is the one a remainder must not evaluate twice.
/// </summary>
private static IEnumerable<FunctionDeclaration> Functions()
{
foreach (BinaryOperator op in new[] { BinaryOperator.Divide, BinaryOperator.Modulo })
{
string name = op == BinaryOperator.Divide ? "div" : "mod";

FunctionDeclaration parameters = new($"{name}_params") { ReturnType = new TypeReference("int") };
parameters.Parameters.Add(new Parameter("a", "int"));
parameters.Parameters.Add(new Parameter("b", "int"));
parameters.Body.Add(new ReturnStatement(new BinaryExpression(
new VariableReference("a"), op, new VariableReference("b"))));
yield return parameters;

FunctionDeclaration literals = new($"{name}_literals") { ReturnType = new TypeReference("int") };
literals.Body.Add(new ReturnStatement(new BinaryExpression(
new LiteralExpression<int>(-7), op, new LiteralExpression<int>(2))));
yield return literals;

FunctionDeclaration local = new($"{name}_local") { ReturnType = new TypeReference("int") };
local.Parameters.Add(new Parameter("a", "int"));
local.Parameters.Add(new Parameter("b", "int"));
local.Body.Add(new VariableDeclaration("q", "int", new VariableReference("a")));
local.Body.Add(new ReturnStatement(new BinaryExpression(
new VariableReference("q"), op, new VariableReference("b"))));
yield return local;

FunctionDeclaration call = new($"{name}_call") { ReturnType = new TypeReference("int") };
call.Parameters.Add(new Parameter("a", "int"));
call.Parameters.Add(new Parameter("b", "int"));
CallExpression counted = new("counted") { ExpectedType = "int" };
counted.Arguments.Add(new VariableReference("a"));
call.Body.Add(new ReturnStatement(new BinaryExpression(counted, op, new VariableReference("b"))));
yield return call;
}
}

private static int Expected(string function, int left, int right)
{
bool divide = function.StartsWith("div", StringComparison.Ordinal);
(int a, int b) = function.EndsWith("_literals", StringComparison.Ordinal) ? (-7, 2) : (left, right);
return divide ? a / b : a % b;
}

/// <summary>
/// Tests that Python computes the C family's quotient and remainder, and calls an operand once.
/// </summary>
[TestMethod]
public void Python_MatchesTheCFamily()
{
string? python = ToolchainHarness.FindOnPath("--version", "python3", "python");
if (python is null)
{
Assert.Inconclusive("No Python interpreter on the path, so nothing was run.");
return;
}

PythonGenerator generator = new();
StringBuilder module = new();
module.AppendLine("calls = 0");
module.AppendLine("def counted(x):");
module.AppendLine(" global calls");
module.AppendLine(" calls += 1");
module.AppendLine(" return x");
foreach (FunctionDeclaration function in Functions())
{
module.AppendLine(generator.Generate(function));
}

foreach (FunctionDeclaration function in Functions())
{
foreach ((int left, int right) in Operands)
{
string arguments = function.Parameters.Count == 0
? string.Empty
: string.Create(CultureInfo.InvariantCulture, $"{left}, {right}");
int expected = Expected(function.Name!, left, right);
module.AppendLine(CultureInfo.InvariantCulture, $"calls = 0");
module.AppendLine(CultureInfo.InvariantCulture, $"r = {function.Name}({arguments})");
module.AppendLine(CultureInfo.InvariantCulture, $"assert type(r) is int and r == {expected}, f\"{function.Name}({arguments}) = {{r!r}}, expected {expected}\"");
if (function.Name!.EndsWith("_call", StringComparison.Ordinal))
{
module.AppendLine(CultureInfo.InvariantCulture, $"assert calls == 1, f\"{function.Name} called its operand {{calls}} times\"");
}
}
}

Run(python, "check.py", module.ToString());
}

/// <summary>
/// Tests that JavaScript computes the C family's quotient and remainder.
/// </summary>
[TestMethod]
public void JavaScript_MatchesTheCFamily()
{
string? node = ToolchainHarness.FindOnPath("--version", "node");
if (node is null)
{
Assert.Inconclusive("No JavaScript runtime on the path, so nothing was run.");
return;
}

JavaScriptGenerator generator = new();
StringBuilder module = new();
module.AppendLine("function counted(x) { return x; }");
foreach (FunctionDeclaration function in Functions())
{
module.AppendLine(generator.Generate(function));
}

foreach (FunctionDeclaration function in Functions())
{
foreach ((int left, int right) in Operands)
{
string arguments = function.Parameters.Count == 0
? string.Empty
: string.Create(CultureInfo.InvariantCulture, $"{left}, {right}");
int expected = Expected(function.Name!, left, right);

// Strict equality rather than Object.is: JavaScript's -0 is an integer zero to every
// arithmetic and comparison a consumer would make of it.
module.AppendLine(CultureInfo.InvariantCulture, $"{{ const r = {function.Name}({arguments}); if (r !== {expected}) {{ throw new Error(`{function.Name}({arguments}) = ${{r}}, expected {expected}`); }} }}");
}
}

Run(node, "check.js", module.ToString());
}

/// <summary>
/// Tests that a division the AST does not know to be on integers is left as true division.
/// </summary>
/// <param name="language">The generator to ask.</param>
[TestMethod]
[DataRow("python")]
[DataRow("javascript")]
public void NonIntegerDivision_IsUnchanged(string language)
{
ILanguageGenerator generator = language == "python" ? new PythonGenerator() : new JavaScriptGenerator();

BinaryExpression doubles = new(new LiteralExpression<double>(7.0), BinaryOperator.Divide, new LiteralExpression<int>(2));
BinaryExpression unknown = new(new VariableReference("a"), BinaryOperator.Divide, new VariableReference("b"));

Assert.AreEqual("(7.0 / 2)", generator.Generate(doubles).Trim());
Assert.AreEqual("(a / b)", generator.Generate(unknown).Trim());
}

private static void Run(string runtime, string fileName, string source) =>
ToolchainHarness.InTemporaryDirectory(directory =>
{
File.WriteAllText(Path.Combine(directory, fileName), source);
(int exitCode, string output) = ToolchainHarness.Run(runtime, fileName, directory);
Assert.AreEqual(0, exitCode, $"{output}{Environment.NewLine}{source}");
});
}
27 changes: 27 additions & 0 deletions Coder/Languages/JavaScriptGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ protected override void GenerateFunctionDeclaration(FunctionDeclaration funcDecl
Ensure.NotNull(funcDecl);
Ensure.NotNull(code);

using IDisposable types = OpenStaticTypeScope(funcDecl);
WritePreamble(funcDecl, code, PreambleSite.Function);

code.Write($"function {funcDecl.Name ?? "unnamedFunction"}(");
Expand Down Expand Up @@ -455,6 +456,7 @@ private void GenerateField(VariableDeclaration field, CodeBlocker code)
/// <param name="code">The writer to emit into.</param>
private void GenerateMethod(FunctionDeclaration method, TypeReference? baseType, CodeBlocker code)
{
using IDisposable types = OpenStaticTypeScope(method);
WritePreamble(method, code, PreambleSite.Method);

if (method.Definition != FunctionDefinition.Provided)
Expand Down Expand Up @@ -575,6 +577,7 @@ protected override void GenerateVariableDeclaration(VariableDeclaration varDecl,
{
Ensure.NotNull(varDecl);
Ensure.NotNull(code);
RegisterStaticType(varDecl);

// `const` needs an initializer, so an uninitialized constant has to be declared with `let`.
string keyword = varDecl.IsConstant && varDecl.InitialValue is not null ? "const" : "let";
Expand Down Expand Up @@ -628,6 +631,30 @@ protected override void GenerateEntryPoint(EntryPoint entryPoint, CodeBlocker co
: $"main({arguments});");
}

/// <inheritdoc/>
/// <remarks>
/// The AST means integer <see cref="BinaryOperator.Divide"/> the way the C family does, truncating
/// toward zero, so <c>-7 / 2</c> is <c>-3</c>. JavaScript has one number type and its <c>/</c> is
/// true division, giving <c>-3.5</c>, so when both operands are known to be integers the quotient
/// is truncated with <c>Math.trunc</c> — not <c>Math.floor</c>, which would give <c>-4</c>.
/// JavaScript's <c>%</c> already takes the sign of the dividend, the same as the C family's.
/// </remarks>
protected override void GenerateBinary(BinaryExpression binary, CodeBlocker code)
{
Ensure.NotNull(binary);
Ensure.NotNull(code);

if (binary.Operator != BinaryOperator.Divide || !IsIntegerArithmetic(binary))
{
base.GenerateBinary(binary, code);
return;
}

code.Write("Math.trunc(");
GenerateBinaryExpression(binary, code, "/");
code.Write(")");
}

/// <summary>
/// Maps a binary operator to its JavaScript spelling.
/// </summary>
Expand Down
61 changes: 61 additions & 0 deletions Coder/Languages/PythonGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@
/// <c>float("nan")</c> does not.
/// </remarks>
protected override string SpellNonFiniteDouble(double value) =>
double.IsNaN(value) ? "float(\"nan\")" : value > 0 ? "float(\"inf\")" : "float(\"-inf\")";

Check warning on line 222 in Coder/Languages/PythonGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 222 in Coder/Languages/PythonGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 222 in Coder/Languages/PythonGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 222 in Coder/Languages/PythonGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 222 in Coder/Languages/PythonGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 222 in Coder/Languages/PythonGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 222 in Coder/Languages/PythonGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 222 in Coder/Languages/PythonGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

/// <inheritdoc/>
protected override IEnumerable<string> RequiredImports(SourceFile file)
Expand Down Expand Up @@ -421,6 +421,7 @@
Ensure.NotNull(funcDecl);
Ensure.NotNull(code);

using IDisposable types = OpenStaticTypeScope(funcDecl);
WritePreamble(funcDecl, code, PreambleSite.Function);

// Function signature
Expand Down Expand Up @@ -578,6 +579,7 @@
/// </remarks>
private void GenerateMethod(FunctionDeclaration method, ClassDeclaration declaration, CodeBlocker code)
{
using IDisposable types = OpenStaticTypeScope(method);
WritePreamble(method, code, PreambleSite.Method);

if (method.Definition != FunctionDefinition.Provided)
Expand Down Expand Up @@ -915,6 +917,7 @@
{
Ensure.NotNull(varDecl);
Ensure.NotNull(code);
RegisterStaticType(varDecl);

code.Write(varDecl.Name);

Expand All @@ -930,6 +933,64 @@
}
}

/// <inheritdoc/>
/// <remarks>
/// The AST means integer <see cref="BinaryOperator.Divide"/> and <see cref="BinaryOperator.Modulo"/>
/// the way the C family does: the quotient truncates toward zero and the remainder takes the sign
/// of the dividend, so <c>-7 / 2</c> is <c>-3</c> and <c>-7 % 2</c> is <c>-1</c>. Python's <c>/</c>
/// is true division and gives <c>-3.5</c>, a float, and its <c>//</c> and <c>%</c> floor, giving
/// <c>-4</c> and <c>1</c>. So when both operands are known to be integers the quotient is truncated
/// with <c>int()</c>, and the remainder is what is left once the truncated quotient is taken away.
/// <para>
/// The remainder names each operand twice. An operand that is a literal or a variable reads the
/// same both times; anything else is bound once, as the parameters of a lambda called where it
/// stands, so a call is not made twice. <c>math.fmod</c> would say it in one call, but it needs an
/// import and an expression can be generated outside any file that could carry one.
/// </para>
/// </remarks>
protected override void GenerateBinary(BinaryExpression binary, CodeBlocker code)
{
Ensure.NotNull(binary);
Ensure.NotNull(code);

if (binary.Operator is not (BinaryOperator.Divide or BinaryOperator.Modulo) || !IsIntegerArithmetic(binary))
{
base.GenerateBinary(binary, code);
return;
}

if (binary.Operator == BinaryOperator.Divide)
{
code.Write("int(");
GenerateBinaryExpression(binary, code, "/");
code.Write(")");
return;
}

if (IsReadTwiceSafely(binary.Left) && IsReadTwiceSafely(binary.Right))
{
code.Write("(");
GenerateInternal(binary.Left, code);
code.Write(" - ");
GenerateInternal(binary.Right, code);
code.Write(" * int(");
GenerateBinaryExpression(binary, code, "/");
code.Write("))");
return;
}

code.Write("(lambda a, b: a - b * int(a / b))(");
GenerateInternal(binary.Left, code);
code.Write(", ");
GenerateInternal(binary.Right, code);
code.Write(")");
}

private static bool IsReadTwiceSafely(Expression operand) =>
operand is VariableReference
or LiteralExpression<int>
or LiteralExpression<long>;

/// <summary>
/// Maps a binary operator to its Python spelling.
/// </summary>
Expand Down
Loading
Loading