From 2387036821ea003f031bc8b074a230b3e523a761 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 08:27:33 +0000 Subject: [PATCH] [patch] Parenthesise a number literal used as a call receiver A signed literal receiver lost its grouping: -2.5.abs() is -(2.5.abs()) in every target, so the call ran and returned the wrong value. An integer receiver ran the dot into the literal, and 5.toString() does not parse in JavaScript or Python. GenerateCallExpression now wraps any number literal receiver in parentheses. Fixes #100 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HsaGezbczZSL9fXfQafs7x --- .../Languages/CallReceiverLiteralTests.cs | 77 +++++++++++++++++++ Coder/Languages/LanguageGeneratorBase.cs | 30 ++++++++ 2 files changed, 107 insertions(+) create mode 100644 Coder.Test/Languages/CallReceiverLiteralTests.cs diff --git a/Coder.Test/Languages/CallReceiverLiteralTests.cs b/Coder.Test/Languages/CallReceiverLiteralTests.cs new file mode 100644 index 0000000..9507d32 --- /dev/null +++ b/Coder.Test/Languages/CallReceiverLiteralTests.cs @@ -0,0 +1,77 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Coder.Test.Languages; + +using System.Linq; +using ktsu.Coder.Ast; +using ktsu.Coder.Languages; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Tests that a number literal used as a call's receiver keeps its grouping. +/// +/// +/// -2.5.abs() is -(2.5.abs()) in every target, because unary minus binds looser than +/// member access, and 5.toString() does not parse in JavaScript or Python. +/// +[TestClass] +public class CallReceiverLiteralTests +{ + /// + /// Every generator that writes a receiver in front of the callee. C passes it as an argument instead. + /// + private static readonly ILanguageGenerator[] Generators = + [ + new CSharpGenerator(), + new CppGenerator(), + new GoGenerator(), + new RustGenerator(), + new PythonGenerator(), + new JavaScriptGenerator(), + ]; + + /// + /// Tests that a negative literal receiver is parenthesised, so the call applies to the negative value. + /// + [TestMethod] + public void ANegativeLiteralReceiverIsParenthesised() + { + CallExpression call = new(Literal.DecimalValue(-2.5), "abs"); + + foreach (ILanguageGenerator generator in Generators) + { + string code = generator.Generate(call).Trim(); + + Assert.StartsWith("(-2.5", code, StringComparison.Ordinal, $"{generator.GetType().Name} wrote {code}"); + Assert.EndsWith(").abs()", code, StringComparison.Ordinal, $"{generator.GetType().Name} wrote {code}"); + } + } + + /// + /// Tests that an integer literal receiver is parenthesised, so the dot is not read as a decimal point. + /// + [TestMethod] + public void AnIntegerLiteralReceiverIsParenthesised() + { + CallExpression call = new(Literal.Number(5), "toString"); + + foreach (ILanguageGenerator generator in Generators.Where(g => g is JavaScriptGenerator or PythonGenerator)) + { + Assert.AreEqual("(5).toString()", generator.Generate(call).Trim(), generator.GetType().Name); + } + } + + /// + /// Tests that a receiver that is not a literal is still written bare. + /// + [TestMethod] + public void ANameReceiverIsWrittenBare() + { + CallExpression call = new(new VariableReference("value"), "abs"); + + foreach (ILanguageGenerator generator in Generators) + { + Assert.AreEqual("value.abs()", generator.Generate(call).Trim(), generator.GetType().Name); + } + } +} diff --git a/Coder/Languages/LanguageGeneratorBase.cs b/Coder/Languages/LanguageGeneratorBase.cs index 38ac6d7..b1218f2 100644 --- a/Coder/Languages/LanguageGeneratorBase.cs +++ b/Coder/Languages/LanguageGeneratorBase.cs @@ -558,6 +558,12 @@ protected void GenerateExpressionStatement(ExpressionStatement statement, CodeBl /// is written verbatim. Nothing here maps a function's name /// between languages, and nothing pretends to — see the node's own remarks for why. /// + /// + /// A number literal receiver is parenthesised. A negative one would otherwise negate the call's + /// result rather than its receiver, since unary minus binds looser than member access, so + /// -2.5.abs() is -(2.5.abs()); and an integer one would run the dot into the + /// literal, which JavaScript and Python read as a malformed number rather than a member call. + /// /// protected virtual void GenerateCallExpression(CallExpression callExpr, CodeBlocker code) { @@ -566,7 +572,20 @@ protected virtual void GenerateCallExpression(CallExpression callExpr, CodeBlock if (callExpr.Receiver is not null) { + bool parenthesise = IsNumberLiteral(callExpr.Receiver); + + if (parenthesise) + { + code.Write("("); + } + GenerateInternal(callExpr.Receiver, code); + + if (parenthesise) + { + code.Write(")"); + } + code.Write("."); } @@ -714,6 +733,17 @@ LiteralExpression floatLit when float.IsNegative(floatLit.Value) => '-', _ => null, }; + /// + /// Reports whether a node is a number literal. + /// + /// The node to inspect. + /// True for an integer or floating-point literal of any sign; otherwise false. + private static bool IsNumberLiteral(AstNode? node) => node is LiteralExpression + or LiteralExpression + or LiteralExpression + or LiteralExpression + or AstLeafNode; + /// /// Reports whether a node is one of the standard shapes a generator built on these helpers accepts. ///