Skip to content

Refuse input integers outside 32 bits - #413

Merged
jserv merged 2 commits into
sysprog21:mainfrom
hankliuliu:refuse-out-of-range-candidate-integers
Oct 21, 2026
Merged

jserv merged 2 commits into
sysprog21:mainfrom
hankliuliu:refuse-out-of-range-candidate-integers

Conversation

@hankliuliu

@hankliuliu hankliuliu commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

A candidate case is checked once, when it is added, and its integers only had to pass Number.isInteger, but the C, C++ and Java harnesses write every integer into an int. This change refuses a value outside -2147483648 to 2147483647 at that point, the same for every language. Line numbers below are at 7b10c56.

Why [3000000000] fails

candidateTypeMatches checks integer with Number.isInteger and nothing else (web/runners.js:142). Python and JavaScript then receive the number unchanged, by JSON.stringify and json.loads and by structured clone (web/runners.js:1175, :1058, :833). C, C++ and Java do not: every harness declares integer as int (web/compiler-explorer.js:9, :21, :33), the type vocabulary has no wider integer, and nativeLiteral writes the value into the source as String(value) (:73), so the harness contains int n = 3000000000;.

Each language then applies its own rule to a constant that does not fit in int:

  • C: 3000000000 is a long constant, and converting an out-of-range value to int is implementation-defined (C17 6.3.1.3p3). The clang on Compiler Explorer (cclang1910) wrapped it modulo 2^32 with only a warning, so the solution received -1294967296; another compiler is free to do otherwise.
  • C++: since C++20, copy-initialization converts modulo 2^32, so int n = 3000000000; also gives -1294967296, while list-initialization of an int from that constant is narrowing and ill-formed, which g++ reports as an error.
  • Java: a decimal int literal above 2147483647 is a compile-time error (JLS 3.10.1), whatever it is assigned to. The error sits on a harness line, so remapDiagnostics adds HARNESS_SIGNATURE_NOTE (web/compiler-explorer.js:252), which blames a signature the candidate did not change.

A compile error replaces the whole run with a setup error (web/runners.js:337-343), so the built-in results are lost as well. The compilers do warn, but a run that compiles returns only its stdout (web/compiler-explorer.js:224), so the candidate never sees the warning.

When it fails

From the generators, a candidate case breaks a C, C++ or Java run when it puts an integer outside -2147483648 to 2147483647 at a position the harness writes as an int: an integer parameter, every element of integer[], integer[][] and list<list<integer>>, the values of a linked list, a list of linked lists, a binary tree, a next-pointer tree and a random-pointer list, a tree node reference, and a class method or constructor argument, including a tree built for a constructor (web/compiler-explorer.js:868-1083, :625-651, :804-820; inferredCandidateType, web/runners.js:195). Three integer positions are held in range by other checks whatever the values: the cycle position by the list length (web/runners.js:86-94), graph neighbors by the node count (:184-186) and a random pointer by the list length (:170-172). A tree node reference is checked only for membership in the tree (:95-100), so it is in range only once the tree's own values are: at 7b10c56, [[3000000000,1],3000000000,1] passes both checks. The value counts however it is typed: [3e9] is 3000000000 after JSON.parse, and [9007199254740993] is already 9007199254740992 before any check.

What the candidate then sees depends on the language and on how the harness writes the value. With the compilers on Compiler Explorer, C wrapped every value measured below, which the standard leaves to the implementation. C++ fails to compile where braces initialize an int directly, which is a vector<int> literal (web/compiler-explorer.js:970, also used by the linked-list helpers) or the constructor call instance{...} (:588), and converts the value elsewhere: a scalar, a method argument (:609), and trees and random lists, whose values pass through the optional<int> and pair<int, optional<int>> constructors of their helpers (:538, :540, :544). Java always fails to compile. A value JavaScript writes in exponent form, such as 1e21, becomes a double literal instead: its conversion to int is undefined in C, and Java reports possible lossy conversion from double to int.

Measurements

Measured at 0fecc33; the inputs are written the same way at 6756678, whose web/compiler-explorer.js changes since then only affect how results are printed. runBrowserTests ran in headless Chromium as the page calls it, with Python in the vendored Pyodide, JavaScript in the blob Worker, and C, C++ and Java on Compiler Explorer (cclang1910, g162, java2501). Each case went through parseCandidateCase first, each solution returns the value it was given, and every row also ran with 7 in place of 3000000000 as a control, which returned 7 in all five languages.

Position (judge), value 3000000000 Python JavaScript C C++ Java
integer (robot-stride-plans) exact exact wrapped wrapped too large
integer[] (replica-quorum-version) exact exact wrapped narrowing too large
integer[][] (batch-health-check-windows) exact exact wrapped narrowing too large
list<list<integer>> (mine-level-descent) exact exact wrapped narrowing too large
linked list (firmware-event-chain-ordering) exact exact wrapped narrowing too large
linked list with a cycle position (workflow-handoff-loop) exact exact wrapped narrowing too large
list of linked lists (shard-result-consolidation) exact exact wrapped narrowing too large
binary tree (right-to-left-layout) exact exact wrapped wrapped too large
next-pointer tree (org-chart-row-navigation) exact exact wrapped wrapped too large
tree with a node reference (shared-approver-lookup) exact exact wrapped wrapped too large
random-pointer list (outline-snapshot-duplicator) exact exact wrapped wrapped too large
class method argument (bounded-event-queue, push) exact exact no C harness wrapped too large
class constructor argument (bounded-event-queue) exact exact no C harness narrowing too large
class constructor tree (ordered-index-cursor) exact exact no C harness wrapped too large

"wrapped" is -1294967296 with no error; "narrowing" is narrowing conversion of '3000000000' from 'long int' to 'int' and "too large" is integer number too large, both followed by the signature note. Both bounds are exact in all five languages, 4294967296 arrived in C and C++ as 0, and in workflow-handoff-loop the wrap turned a true answer into false in C.

Change

candidateTypeMatches takes the integer check as a parameter, isInt32 by default, and applies it to integer and to the integer values of lists, trees and random lists, through the [], list<> and list-of-lists recursion. When a case is refused, the caller asks again with Number.isInteger: if the value passes then, its only fault is the range, and the refusal reads Parameter <i> (<name>) holds an integer outside -2147483648 to 2147483647., or Arguments for operation <name> hold ... for a class judge. Any other mistake keeps the message it had before, so a number given for a string parameter still reads "must match string". The cycle position, graph neighbor and random pointer checks are unchanged, and a tree node reference is bounded through its tree. web/compiler-explorer.js, the problem bank and docs/ are unchanged.

Tests

tests/browser/runners.test.js accepts both bounds and refuses 2147483648 and -2147483649 in every integer-bearing position: an integer, an element of integer[] and integer[][], a list<list<integer>> value, the values of a linked list, a list of linked lists, a binary tree, a next-pointer tree and a random-pointer list, and a class method argument, constructor argument and constructor-built tree. A second test checks that the cycle position, tree node reference and graph neighbor keep their own refusals, and that a reference into a tree holding 2147483648 is refused at the tree. Both tests fail at 7b10c56 without the change, with "Missing expected exception", and pass with it. Each part of the change was also undone separately in a scratch copy, such as accepting 2147483648 again or showing the range message for every error, and the tests failed for all 14 variants. All 802 built-in cases in web/judges/ are still accepted by parseCandidateCase.

Validation

Prettier and ESLint pass on both files, and the commit message passes scripts/check-commit-log.sh. At 7b10c56 on Windows, npm test ran 1,338 browser tests: 1,009 passed, 324 were skipped, and 5 failed, all toolchain harnesses C++ subtests, because the local g++ (MinGW 8.1.0) rejects -std=c++20. In WSL (Ubuntu 24.04, g++ 13.3, JDK 21), at 6756678, whose harness code and its tests are unchanged at 7b10c56, tests/browser/compiler-explorer.test.js ran all 43 of its tests, those five included, with none failing or skipped. The Rust lane was not run locally, since this change touches no Rust code; CI covers it.

Screenshots

Both screenshots are from a live interview on Robot Stride Plans, where I entered [3000000000] under Add a case. Before, the case is accepted and listed as "Your case 1: [3000000000]" with "1/5 cases ready." After, with this branch's web/runners.js served in place of the release's copy, the same input is refused with "Parameter 1 (n) holds an integer outside -2147483648 to 2147483647." and nothing is added to the list.

before:
螢幕擷取畫面 (366)

after:
螢幕擷取畫面 (367)

Closes #387


Summary by cubic

Refuses candidate integers outside the 32-bit range when a case is added, so a value like 3000000000 no longer silently wraps to -1294967296 in C and C++ or makes javac fail the whole run with a note blaming the signature.

  • The range check applies to integer values and the integer values of lists, trees and random lists; cycle position, graph neighbor and random pointer checks are unchanged.
  • A tree node reference is now bounded through its tree's values.
  • A case is refused with "holds an integer outside -2147483648 to 2147483647" only when the range is the sole fault; other type errors keep their existing messages.
  • Java harnesses now emit whole-number doubles with a .0 suffix, since javac reads a numeral without a point or exponent as an int literal even when assigned to a double.
  • tests/browser/runners.test.js covers both bounds in every integer-bearing position, including class method arguments, constructor arguments and constructor-built trees.

Closes #387

Written for commit 7b2ce54. Summary will update on new commits.

View guided diff

A candidate case was typed as an integer whenever Number.isInteger
held, but the C, C++ and Java harnesses write every integer into an
int, so 3000000000 reached C and C++ as -1294967296 and made javac
refuse the whole run with a note blaming the signature. Integers and
the integer values of lists, trees and random lists are now bounded to
32 bits when the case is added, the same for every language, and the
range is named only when it is the sole fault.

Closes sysprog21#387
cubic-dev-ai[bot]

This comment was marked as resolved.

Comment thread web/runners.js
nativeLiteral wrote a double as String(value), so 2147483648 came out
with no point. javac reads such a numeral as an int literal whatever
it is assigned to and refuses one past the int range, so the whole run
failed with the signature note. A double whose text has no point or
exponent now gets ".0", which C, C++ and Java all read as the same
double.
@jserv
jserv requested review from ColtenOuO and alanhc October 21, 2026 14:38
@jserv
jserv merged commit b09f736 into sysprog21:main Oct 21, 2026
10 checks passed
@jserv

jserv commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Thank @hankliuliu for contributing!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Added test cases accept integers that C, C++ and Java cannot hold

2 participants