Skip to content

Commit 539f413

Browse files
paulirwinclaude
andauthored
Preserve the L suffix on converted long literals (#175)
## Problem `LongLiteralExpressionVisitor` stripped the trailing `L`/`l` from a Java long literal and emitted the remaining text verbatim, so the generated C# lost the suffix that makes it a long. That produced code which **does not compile**. C# types a bare numeric literal as `int`, so: ```java long allOnes = 0xFFFFFFFFFFFFFFFFL; ``` converted to: ```csharp long allOnes = 0xFFFFFFFFFFFFFFFF; // error CS0266: cannot implicitly convert 'ulong' to 'long' ``` ## Fix - Re-append the `L` suffix when building the literal token. - Strip the incoming `L`/`l` as a *suffix* rather than via a blanket `Replace` over the whole string, which was scanning every character of a value we echo back into the generated source. - A hex literal above `long.MaxValue` stays invalid in C# even with the suffix, because the literal is typed by its magnitude. For that case emit the wrapped decimal value, which is the number Java means: `0xFFFFFFFFFFFFFFFFL` → `-1L`. ## Why it went unnoticed The existing long-literal tests asserted only the parsed numeric `Value`, never the emitted token text — so a literal with a correct value but uncompilable text passed. The new cases assert `Text`. ## Testing - New `VisitLiteralExpression_Long_PreservesSuffixInText` theory covering hex, binary, octal, underscore separators, lowercase `l`, and both overflow boundaries. - Extra `VisitLiteralExpression_Long` cases for the two's-complement and above-`int.MaxValue` values. - `Resources/LongLiterals.java` wired into `FullIntegrationTests`, which compiles the generated C# and asserts its runtime output matches Java's. Full suite: **378 passed, 0 failed** (was 363). ## Note on #128 The binary-literal crash originally reported in #128 (`0b10`, `0b100`) **no longer reproduces on master** — it was fixed sometime after the 3.0.0 build in that report. Investigating it surfaced this adjacent defect in the long-literal path, which this PR fixes. Worth confirming whether #128 should now be closed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 673e6cb commit 539f413

4 files changed

Lines changed: 77 additions & 5 deletions

File tree

‎JavaToCSharp.Tests/IntegrationTests.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ public void GeneralUnsuccessfulConversionTest(string filePath)
8989
[InlineData("Resources/StaticImports.java")]
9090
[InlineData("Resources/LabeledBreakContinue.java")]
9191
[InlineData("Resources/ExceptionGetMessage.java")]
92+
[InlineData("Resources/LongLiterals.java")]
9293
public void FullIntegrationTests(string filePath, bool allowWarnings = false)
9394
=> RunFullIntegrationTest(filePath, allowWarnings);
9495

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/// Expect:
2+
/// - output: "-1 9223372036854775807 10 2147483648 255 8 1000000\n"
3+
package example;
4+
5+
public class Program {
6+
public static void main(String[] args) {
7+
// An all-ones hex long is -1 in Java's two's-complement representation. Without the
8+
// L suffix the generated C# literal is a ulong and fails to compile (CS0266).
9+
long allOnes = 0xFFFFFFFFFFFFFFFFL;
10+
long maxValue = 0x7FFFFFFFFFFFFFFFL;
11+
long small = 10L;
12+
// Above int.MaxValue, so a bare literal would not be typed as int in C#.
13+
long aboveIntMax = 2147483648L;
14+
long hex = 0xFFL;
15+
long octal = 010L;
16+
long separated = 1_000_000L;
17+
18+
System.out.println(allOnes + " " + maxValue + " " + small + " " + aboveIntMax
19+
+ " " + hex + " " + octal + " " + separated);
20+
}
21+
}

‎JavaToCSharp.Tests/VisitLiteralExpressionTests.cs‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,42 @@ public void VisitLiteralExpression_Integer(string javaLiteral, int expected)
5555
[InlineData("0B1010L", 10L)]
5656
[InlineData("0x1FL", 31L)]
5757
[InlineData("010L", 8L)]
58+
[InlineData("10L", 10L)]
59+
[InlineData("2147483648L", 2147483648L)]
60+
// Java long literals are two's-complement, so an all-ones hex literal is -1.
61+
[InlineData("0xFFFFFFFFFFFFFFFFL", -1L)]
62+
[InlineData("0x7FFFFFFFFFFFFFFFL", long.MaxValue)]
63+
// A lowercase l suffix is equally valid Java.
64+
[InlineData("42l", 42L)]
5865
public void VisitLiteralExpression_Long(string javaLiteral, long expected)
5966
{
6067
var expr = ExpressionVisitor.VisitExpression(new ConversionContext(new JavaConversionOptions()), new LongLiteralExpr(javaLiteral));
6168
Assert.Equal(expected, expr?.GetFirstToken().Value);
6269
}
70+
71+
/// <summary>
72+
/// The emitted text must keep the L suffix. C# types a bare numeric literal as int, so
73+
/// dropping it makes 0xFFFFFFFFFFFFFFFF a ulong that will not implicitly convert to long
74+
/// (CS0266), and pushes any value above int.MaxValue to a different inferred type.
75+
/// </summary>
76+
[Theory]
77+
// Above long.MaxValue C# would type the hex literal as ulong (CS0266), so the wrapped
78+
// decimal value is emitted instead.
79+
[InlineData("0xFFFFFFFFFFFFFFFFL", "-1L")]
80+
[InlineData("0x8000000000000000L", "-9223372036854775808L")]
81+
[InlineData("2147483648L", "2147483648L")]
82+
[InlineData("0x1FL", "0x1FL")]
83+
[InlineData("0b10L", "0b10L")]
84+
[InlineData("10L", "10L")]
85+
[InlineData("42l", "42L")]
86+
// Underscores are separators in Java and are dropped from the emitted literal.
87+
[InlineData("1_000_000L", "1000000L")]
88+
// Java octal has no C# equivalent, so it is rewritten in decimal - still suffixed.
89+
[InlineData("010L", "8L")]
90+
public void VisitLiteralExpression_Long_PreservesSuffixInText(string javaLiteral, string expectedText)
91+
{
92+
var expr = ExpressionVisitor.VisitExpression(new ConversionContext(new JavaConversionOptions()), new LongLiteralExpr(javaLiteral));
93+
Assert.Equal(expectedText, expr?.GetFirstToken().Text);
94+
}
6395
}
6496

‎JavaToCSharp/Expressions/LongLiteralExpressionVisitor.cs‎

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,30 @@ public class LongLiteralExpressionVisitor : ExpressionVisitor<LiteralStringValue
99
protected override ExpressionSyntax Visit(ConversionContext context, LiteralStringValueExpr expr)
1010
{
1111
string value = expr is LongLiteralExpr longLiteralExpr ? longLiteralExpr.getValue() : expr.toString();
12-
value = value.Trim('\"')
13-
.Replace("L", string.Empty)
14-
.Replace("l", string.Empty)
15-
.Replace("_", string.Empty);
12+
value = value.Trim('\"').Replace("_", string.Empty);
13+
14+
// Java marks a long literal with a trailing L/l. Strip it as a suffix only: a blanket
15+
// Replace would also corrupt digits in a value we echo back into the generated source.
16+
if (value.EndsWith('L') || value.EndsWith('l'))
17+
{
18+
value = value[..^1];
19+
}
1620

1721
long int64Value;
1822

1923
if (value.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
2024
{
25+
// Convert.ToInt64 accepts the 0x prefix and wraps values above long.MaxValue
26+
// (e.g. 0xFFFFFFFFFFFFFFFF -> -1), matching Java's two's-complement semantics.
2127
int64Value = Convert.ToInt64(value, 16);
28+
29+
// C# types a hex literal by its magnitude, so anything above long.MaxValue becomes
30+
// ulong and will not implicitly convert to long (CS0266) even with the L suffix.
31+
// Emit the wrapped decimal value instead, which is the number Java means.
32+
if (int64Value < 0)
33+
{
34+
value = int64Value.ToString();
35+
}
2236
}
2337
else if (value.StartsWith("0b", StringComparison.OrdinalIgnoreCase))
2438
{
@@ -34,6 +48,10 @@ protected override ExpressionSyntax Visit(ConversionContext context, LiteralStri
3448
int64Value = Convert.ToInt64(value);
3549
}
3650

37-
return SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(value, int64Value));
51+
// Re-append the L suffix. C# infers int for a bare literal, so without it a value above
52+
// int.MaxValue either fails to compile (0xFFFFFFFFFFFFFFFF is ulong) or changes type.
53+
return SyntaxFactory.LiteralExpression(
54+
SyntaxKind.NumericLiteralExpression,
55+
SyntaxFactory.Literal(value + "L", int64Value));
3856
}
3957
}

0 commit comments

Comments
 (0)