What Cross-Language Code Plagiarism Detection Can and Cannot See

Cross-language code plagiarism detection answers a narrow question: is this Python submission the same program as that Java submission? Detectors do it by normalizing both programs into something language-neutral (token classes, control-flow graphs, sometimes bytecode) and comparing structure rather than text. It works when the translation was mechanical, and it falls apart when the student rewrote the algorithm, which is exactly the case that matters most.

Two submissions from a Fall 2023 data structures course illustrate the problem. Assignment 3 was a hash table in Java. Assignment 4 was the same hash table in Python. MOSS reported nothing between them, for the simple reason that MOSS runs one language per submission set. The -l flag applies to the whole batch. You cannot drop a .java file and a .py file into the same run and expect a number back.

The instructor noticed anyway, because the Python submission used the variable name bucketCount and a sentinel constant of 1000003, both of which appeared in the Java version four weeks earlier. Detection in this space is often a story about artifacts, not algorithms.

Why Most Code Plagiarism Detectors Are Language-Bound

Winnowing, the algorithm behind MOSS, was published by Schleimer, Wilkerson, and Aiken in 2003. Hash every k-gram (they used k=50), keep a subset of those hashes selected by a sliding window, and any shared passage of decent length will produce at least one shared fingerprint. It is a beautiful piece of algorithm design and it is entirely about text. Change for (int i = 0; i < n; i++) into for i in range(n): and the 50-character windows on either side stop overlapping in any useful way.

JPlag (Prechelt, Malpohl, and Philippsen, 2002) does better within a language. It converts source into a token sequence and runs greedy string tiling over it. JPlag 5.1, the 2024 Rust rewrite, ships tokenizers for roughly twenty languages and returns a similarity percentage plus matched token ranges, which is genuinely useful for showing a student where the overlap lives. Every one of those tokenizers is language-specific, and the comparison assumes both sides came out of the same one.

The limitation is structural rather than a missing feature. The tools are language-bound because the token alphabets are. Comparing Java to Python requires a decision that none of these tools makes on your behalf.

What Actually Survives a Translation

Less than you would hope, more than you would think. When a student translates mechanically, four things tend to survive: identifier spelling, control-flow shape, literal constants, and odd artifacts like a comment or a typo. Take this pair.

// Java
static int checksum(String s) {
    int h = 17;
    for (int i = 0; i < s.length(); i++) {
        h = (h * 31 + s.charAt(i)) % 1000003;
    }
    return h;
}
# Python
def checksum(s):
    h = 17
    for i in range(len(s)):
        h = (h * 31 + ord(s[i])) % 1000003
    return h

The seed 17, the multiplier 31, and the modulus 1000003 are all artifacts of the original. A student who wrote both versions independently would be unlikely to choose the same modulus, and very unlikely to choose 1000003 over the rounder 1000000. The function name checksum, the variable h, and the loop index i carried across untouched.

What does not survive is everything the translator had to replace. Java's charAt becomes Python's ord(s[i]). The typed declaration vanishes. If the translator restructured the loop into a comprehension or a fold, the control-flow shape goes with it, and now you are comparing intent rather than code. Intent is much harder to diff.

Every detector in this space is an approximation of one question: would a competent human grader, reading both submissions side by side, call this the same work? The algorithm is just a cheap way to ask it at scale.

How Cross-Language Code Plagiarism Detection Compares Submissions

Lexicon-mapped token streams

The cheapest approach is to tokenize each language with its own lexer, then map every token to a coarse class: identifier, integer literal, string literal, loop keyword, branch keyword, and so on. Both programs become sequences over a shared alphabet, and you can run the same greedy tiling you would run within a single language.

This works. Mapped this way, the two checksum functions above produce token streams that overlap heavily, and 1000003 matches as an integer literal on both sides. It also generates noise, because every loop in every language looks alike after mapping. Precision comes from weighting matched literal and identifier values far more heavily than structural tokens.

Graph comparison over control flow

Compilers people reach for Ferrante, Ottenstein, and Warren (1987) here. Build a control-flow graph or a program dependence graph for each submission, normalize the node labels, then run a graph isomorphism or edit-distance comparison. Baxter and colleagues did clone detection over ASTs in 1998; Kamiya, Kusumoto, and Inoue built CCFinder on token sequences in 2002. The Bellon et al. 2007 comparison of six clone detectors is still the best answer to how much of this generalizes.

The catch is cost. Graph edit distance is expensive, and the normalizations you need across languages (Java's checked exceptions, Python's implicit self, C's pointer arithmetic) are the kind of thing you tune per course rather than per tool release. That is a maintenance burden a TA inherits every August.

Common intermediate representation

Compile both sides and compare what comes out. Java gives you JVM bytecode, C and C++ and Rust give you LLVM IR, Python gives you dis output. A translated function leaves a recognizable bytecode shape behind.

  2           0 LOAD_CONST               1 (17)
              2 STORE_FAST               0 (h)
  3           4 LOAD_GLOBAL              0 (range)
              6 LOAD_GLOBAL              1 (len)
              8 LOAD_FAST                0 (s)
             10 CALL_FUNCTION            1

This is where the approach hits a wall, and the wall is not the algorithm. JVM bytecode and CPython bytecode describe two different machines. There is no cheap mapping between a stack VM with typed local slots and one with a dynamic global namespace. Constants survive, which helps, and the control-flow skeleton survives, which also helps, but the instruction sets do not line up. I have not seen a production detector that reliably matches a Java submission against a Python one at the level of a human grader, and I do not expect one in the next few years.

Practical detectors cover this gap by checking more languages, not by being more clever about two. Codequiry's parser set spans 65 approved languages with a submission path for custom grammars, which in practice matters more than any single comparison technique, because a course that changes languages every term cannot standardize on one tokenizer.

Codequiry supported programming languages page listing 65 approved languages and custom parser submission
65 supported languages, plus custom parsers for anything unusual a course or codebase throws at the engine.

LLM Translation Changed the Cheat

The reason cross-language copying deserves attention now is that the mechanism changed. A student in 2015 who wanted to recycle a Java solution in Python had to do the translation by hand, which is more work than writing it fresh. In 2024 they paste it into a chat model and ask for the Python version. The result is usually faithful, keeps most identifiers, and preserves literal constants with startling regularity.

That cuts both ways. An LLM translation is a cleaner signal than a human translation, because the model preserves structure and the student tends not to touch the output before submitting. It also means every submission now has a plausible non-plagiarist explanation, which is why single-signal detection stopped being defensible. Hindle and colleagues noted in 2012 that source code is far more predictable than natural language, and that predictability is what makes a statistical model of authorship possible at all. Ippolito et al. (2020) showed the flip side for prose: detection is easiest exactly when the generated output is boring, and hardest when a human has edited it.

The answer is to stack signals rather than search for one. Peer similarity catches the recycling. Web matching catches the tutorial the original came from. Codequiry runs those alongside an AI code detector in a single pass, which matters here because the three failures look identical from the instructor's chair until you can see which score moved and why.

Codequiry AI code detection report with average AI score, highest file score and a risk distribution
AI code detection: probability scores per file, flagging submissions likely written by ChatGPT, Copilot, Claude or Gemini.

Where the Original Usually Came From

Cross-language translations rarely start from another student's work. They usually start from a public source: a LeetCode discussion, a tutorial repository with a permissive license and a comment header nobody copied. That makes the web dimension of the check non-optional. A source code plagiarism checker that only compares peer submissions will miss the case where four students independently translated the same Stack Overflow answer into four languages.

Web matching also solves a due process problem. Telling a student "your code looks like a translation" is an accusation. Showing them the GitHub repository with the identical 1000003 constant, the commit date, and the matching line is a conversation. Those are different meetings, and only one of them ends with everyone still in the room.

Codequiry web results tracing copied code to GitHub repositories and other web sources with per-domain scores
Web results: every domain a submission matched, scored per source, from GitHub repos to tutorial sites.

A Workflow That Actually Catches This

Here is the part that took us two semesters to get right. Run the check per language first, because that is what your existing tooling handles well and it keeps your statistics comparable across terms. Then run the whole cohort as one normalized corpus, mapped through common token classes, and look at the cross-language pairs that surface above threshold.

We got it wrong the obvious way. We ran the Java set and the Python set as two separate checks, and every report came back clean. Nothing compared across checks, and nobody noticed until a TA mentioned she was hearing the same questions twice in office hours. The fix was a few lines of preprocessing and a second report. No tool change, just a workflow that nobody had written down.

The other half is assignment design. Requiring one implementation language per assignment removes the problem entirely, at the cost of teaching breadth. A middle path is to require both versions but grade against adversarial inputs the student generates, plus a short design note explaining the two data structures they chose and why. Translations survive the tests. The design note is where the translation shows, because a translated solution comes with a translated explanation and the explanation is usually thinner than the code.

Side-by-side code comparison in Codequiry showing a 91% match between two student submissions
Side-by-side comparison: Codequiry lines up matching code between two submissions, with confirmed and false-positive review labels.

For courses running this at scale, a code plagiarism checker for teachers organized around courses and cohorts removes the preprocessing step, and having peer, web, and AI scores in one report is the difference between a five-minute decision and a folder of screenshots nobody can reconstruct in May. That matters less for the base case of two identical Java files and much more for the messy one, where a translation, a public source, and a chat model all appear in a single submission and you need to explain the finding to a student conduct panel.

Frequently Asked Questions

Can a cross-language plagiarism checker compare a Java submission to a Python one?

Not directly, and no mainstream tool does it out of the box. What works is normalizing both sides to a shared token class alphabet or an intermediate representation, then comparing structure with heavy weighting on matching literals and identifiers. Treat the resulting number as a screening signal rather than a verdict.

Does renaming variables defeat cross-language detection?

Renaming alone does not, because literals, control flow, and statement order still align. Renaming plus restructuring plus changing the algorithm does defeat it, but at that point you are looking at a reimplementation rather than a translation, and most honor codes treat those differently anyway.

How common is cross-language copying in practice?

In a 200-student course with assignments in two languages, we see a handful of pairs a term, so low single digits. We have not run this at a scale beyond a few thousand submissions, so take the rate as directional. The trend line has gone up every term since chat models became the easy way to translate, and that part is not subtle.

If you want to see what a stacked report looks like on your own submissions, Codequiry's code plagiarism checker runs the peer, web, and AI checks together, and starting with one assignment in one language is enough to tell whether any of this applies to your course.