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.
///