From 93740117595e2a0af8700bb50d0d797fec9353a2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 04:39:14 +0000 Subject: [PATCH] Type a shift as its left operand [patch] StaticType typed every non-comparison binary as the wider of its operands, shifts included, so int << long was taken for a long: Go and Rust skipped the widening a long receiver needs and failed to build, and JavaScript ran a chained int shift in 64 bits. A shift now has its left operand's type, the rule the binary and assignment widening paths already follow. Fixes #362 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017vfJW4VpqLXsf6UDQnX46h --- Coder.Test/Languages/NumericWideningTests.cs | 61 +++++++++++++++++++- Coder/Languages/StandardLanguageGenerator.cs | 4 ++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/Coder.Test/Languages/NumericWideningTests.cs b/Coder.Test/Languages/NumericWideningTests.cs index 69c895b..7156da3 100644 --- a/Coder.Test/Languages/NumericWideningTests.cs +++ b/Coder.Test/Languages/NumericWideningTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2023-2026 ktsu-dev contributors +// Copyright (c) 2023-2026 ktsu-dev contributors namespace ktsu.Coder.Test.Languages; @@ -55,7 +55,24 @@ internal static SourceFile Widening() called.Body.Add(new ReturnStatement(call)); file.Members.Add(called); + // A shift has its left operand's type, so int << long is an int that a long receives (#362). + FunctionDeclaration shiftedLocal = new("shiftedLocal") { ReturnType = new TypeReference("long") }; + shiftedLocal.Parameters.Add(new Parameter("b", "int")); + shiftedLocal.Parameters.Add(new Parameter("n", "long")); + shiftedLocal.Body.Add(new VariableDeclaration("s", initialValue: Shift(BinaryOperator.LeftShift))); + shiftedLocal.Body.Add(new ReturnStatement(new VariableReference("s"))); + file.Members.Add(shiftedLocal); + + FunctionDeclaration shiftedReturn = new("shiftedReturn") { ReturnType = new TypeReference("long") }; + shiftedReturn.Parameters.Add(new Parameter("b", "int")); + shiftedReturn.Parameters.Add(new Parameter("n", "long")); + shiftedReturn.Body.Add(new ReturnStatement(Shift(BinaryOperator.RightShift))); + file.Members.Add(shiftedReturn); + return file; + + static BinaryExpression Shift(BinaryOperator shift) => + new(new VariableReference("b"), shift, new VariableReference("n")); } /// Tests where Rust converts. @@ -70,6 +87,9 @@ public void Rust_WidensTheNarrowerSide() Assert.Contains("h = (b as f64);", source, StringComparison.Ordinal); Assert.Contains("return (b as i64);", source, StringComparison.Ordinal); Assert.Contains("mixed((b as i64), b)", source, StringComparison.Ordinal); + Assert.Contains("let mut s = (b << n);", source, StringComparison.Ordinal, source); + Assert.Contains("return (s as i64);", source, StringComparison.Ordinal, source); + Assert.Contains("return ((b >> n) as i64);", source, StringComparison.Ordinal, source); } /// Tests where Go converts. @@ -84,6 +104,45 @@ public void Go_WidensTheNarrowerSide() Assert.Contains("h = float64(b)", source, StringComparison.Ordinal); Assert.Contains("return int64(b)", source, StringComparison.Ordinal); Assert.Contains("mixed(int64(b), b)", source, StringComparison.Ordinal); + Assert.Contains("return int64(s)", source, StringComparison.Ordinal, source); + Assert.Contains("return int64((b >> n))", source, StringComparison.Ordinal, source); + } + + /// + /// Tests that JavaScript keeps (x << n) << 31 on an int x in 32 bits + /// when n is a long, because the inner shift is an int (#362). + /// + [TestMethod] + public void JavaScript_AShiftByALongStaysInItsLeftOperandsWidth() + { + FunctionDeclaration chained = new("chained") { ReturnType = new TypeReference("int") }; + chained.Parameters.Add(new Parameter("x", "int")); + chained.Parameters.Add(new Parameter("n", "long")); + chained.Body.Add(new ReturnStatement(new BinaryExpression( + new BinaryExpression(new VariableReference("x"), BinaryOperator.LeftShift, new VariableReference("n")), + BinaryOperator.LeftShift, + Literal.Number(31)))); + SourceFile file = new("shifts"); + file.Members.Add(chained); + + string source = new JavaScriptGenerator().Generate(file); + Assert.DoesNotContain("BigInt", source, StringComparison.Ordinal, source); + + string? node = ToolchainHarness.FindOnPath("--version", "node"); + if (node is null) + { + Assert.Inconclusive("No JavaScript runtime on the path, so nothing was run."); + return; + } + + string check = source + Environment.NewLine + + "if (chained(1, 1) !== 0) { throw new Error(`chained(1, 1): expected 0, got ${chained(1, 1)}`); }" + Environment.NewLine; + ToolchainHarness.InTemporaryDirectory(directory => + { + File.WriteAllText(Path.Combine(directory, "check.js"), check); + (int exitCode, string output) = ToolchainHarness.Run(node, "check.js", directory); + Assert.AreEqual(0, exitCode, $"{output}{Environment.NewLine}{check}"); + }); } /// Tests that a language converting by itself is written as before. diff --git a/Coder/Languages/StandardLanguageGenerator.cs b/Coder/Languages/StandardLanguageGenerator.cs index f1baaf9..17fac0a 100644 --- a/Coder/Languages/StandardLanguageGenerator.cs +++ b/Coder/Languages/StandardLanguageGenerator.cs @@ -530,6 +530,10 @@ or BinaryOperator.GreaterThanOrEqual or BinaryOperator.LogicalAnd or BinaryOperator.LogicalOr: return new TypeReference("bool"); + case BinaryExpression { Operator: BinaryOperator.LeftShift or BinaryOperator.RightShift } shift: + // A shift has its left operand's type in every target: the count does not widen the + // value it shifts (#362). + return StaticType(shift.Left); case BinaryExpression binary: return Promoted(StaticType(binary.Left), StaticType(binary.Right)); case VariableReference reference: