Skip to content

Commit 93a2183

Browse files
paulirwinclaude
andcommitted
Preserve the L suffix on converted long literals
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 `long a = 0xFFFFFFFFFFFFFFFFL` became `long a = 0xFFFFFFFFFFFFFFFF` -- a ulong that will not implicitly convert to long (CS0266). Re-append the suffix when building the literal token, and strip the incoming L/l as a suffix rather than via a blanket Replace over the whole string. A hex literal above long.MaxValue stays invalid in C# even with the suffix, since the literal is typed by its magnitude. For that case emit the wrapped decimal value, which is the number Java means: 0xFFFFFFFFFFFFFFFFL -> -1L. The existing long-literal tests only asserted the parsed numeric Value, which is why this went unnoticed; the new cases assert the emitted token text. LongLiterals.java covers it end to end through the compile-and-run harness. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 673e6cb commit 93a2183

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)