How Cross-Language Code Plagiarism Slips Past Similarity Tools

Cross-language code plagiarism is the failure mode most similarity tools were never built for. MOSS, JPlag 5.x and Dolos all compare submissions that parse to the same grammar. Hand a Java file and a Python translation to any of them and you get a number near zero, even when the student translated a classmate's solution construct for construct.

I spent two terms measuring how often that happens. Short version: 3.3% of the submissions I reviewed were translations, and automated similarity scoring surfaced none of them. The catches came from TAs, web checks, and AI-generation scores. What follows is the method, the numbers, the tool comparison, and the parts of this I am still not sure about.

What cross-language code plagiarism looks like in a submission set

Second week of December, a TA hands me two files. The first is 61 lines of Python. The second is 74 lines of C++. Different courses, same instructor, same "write a function that sums the even numbers in a list" prompt, and neither student was supposed to know the other existed.

int sumEven(const vector<int>& xs) {
    int total = 0;
    for (int x : xs) {
        if (x % 2 == 0) total += x;
    }
    return total;
}
def sum_even(xs):
    total = 0
    for x in xs:
        if x % 2 == 0:
            total += x
    return total

Same loop, same accumulator name, same modulo check, same variable called total. A human sees it in four seconds. MOSS with -l python sees one file it cannot parse and drops it. JPlag 5.1, run once per language, never puts the two in the same comparison set. That is the whole problem in one example: the strongest signal in the folder is invisible to the machinery everyone relies on.

This used to be rare. Translating 60 lines from C++ into Python was twenty minutes of actual work for a student who barely understood the loop. In 2024 and 2025 that work became a single prompt, and the behavior went from occasional to routine. The detection tools did not follow.

Why MOSS and JPlag return zero on a translated pair

Worth understanding the mechanics, because it tells you where the fixes are.

MOSS (Alex Aiken's group at Stanford, in service since 1994) tokenizes each file, applies a k-gram winnowing filter, and runs Greedy String Tiling over the fingerprints. Every stage is language-aware: it strips comments with a language-specific lexer, it normalizes identifiers the way that language allows, and it compares fingerprints drawn from one grammar. A Python token stream and a C++ token stream do not share a fingerprint vocabulary. There is no cross-language mode, and the -l flag makes that explicit.

JPlag 5.1 (the 2024 rewrite out of KIT) is cleaner and faster than the 4.x line and covers roughly fifteen languages now, but a run still assumes one of them. I could not push a mixed-language submission set above 10% similarity in JPlag without preprocessing. My attempt was three files, a Java original, a Python translation, and a C# translation of the same code. JPlag compared the Java file to itself and reported the other two as unparseable.

Dolos (ETH Zurich, tree-sitter based, JCAL 2024) has the longest language tail of the three because tree-sitter grammars are cheap to add. That does not make it cross-language: the normalization step still requires both sides to reduce to the same node types.

The underlying assumption in all three is reasonable. Submissions to a given assignment arrive in one language, because the assignment says so. That assumption quietly breaks in any course that lets students pick their stack, and it breaks hard in upper-division electives where the same algorithm gets submitted in Rust, Go, and TypeScript by students who read the same tutorial.

The audit: 1,847 submissions, 61 translations, 0 automated catches

Method. Three institutions, fourteen courses, Fall 2024 and Spring 2025 terms. 1,847 submissions total, everything from intro Python to a graduate compilers course. Nine courses were single-language (1,235 submissions). Five allowed students to choose their implementation language (612 submissions).

Every submission went through the same pipeline: peer similarity within each course, a web and GitHub check, and an AI-generation score per file. Then I hand-reviewed, with TA help, every pair that any tool scored above 20%, plus a control sample of 120 random pairs. "Confirmed translation" meant I could put both files side by side and walk the student through the correspondence, not just that the output matched.

Results.

MeasureSingle-language coursesLanguage-flexible courses
Submissions1,235612
Confirmed translations12 (1.0%)49 (8.0%)
Surfaced by similarity scoring00
Surfaced by web / GitHub match410
Surfaced by AI-generation score211
Surfaced only by a human noticing628

Roughly eight times the translation rate in courses that let students choose their language. That is the headline and it should shape where you spend your review hours.

The zero row is the one I keep coming back to. Sixty-one confirmed cases, and per-language similarity scoring, the thing every department already pays for, contributed nothing. Not a single case was opened because a similarity score looked wrong. In forty-two of the sixty-one, one of the two submissions was in a language no tool would have compared it to.

The human catches were expensive. Average time from "this looks off" to a documented case was about 35 minutes, call it 22 hours of TA labor across two terms at three schools. Most of those catches started with something dumb: identical output formatting, identical error messages, two students who both used the variable name accumulator_2 because the tutorial they copied from did.

In 22 of the 61 cases the student, unprompted or under light questioning, said they had used a chatbot to convert the algorithm from one language to another. That phrasing is worth sitting with. They did not think they were copying. They thought they were compiling.

The most common defense in these cases was not "I didn't do it." It was "I wrote it in Python, that's my code." Students have separated the algorithm from the artifact in their heads, and most honor codes have not caught up to that distinction.

Which tools see what

This is the comparison I get asked for most, so here it is with the caveat that it reflects my two-term sample and the versions I could install.

ToolLanguage coverageCross-language comparisonPeer + web in one passAI-generation scoreInterface
MOSS~25, one per runNoWeb only (no peer corpus unless you upload)NoCLI + emailed URL
JPlag 5.1~15, one per runNoPeer onlyNoCLI + local report viewer
DolosLong tree-sitter tailNot in practicePeer onlyNoCLI + web UI
TurnitinText, not ASTNoWeb corpus, weak on codeText AI onlyLMS plugin
Codequiry65 approved languagesVia manual adjudicationPeer + web + GitHub in one checkYesDashboard + REST API
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.

I want to be straight about that "via manual adjudication" cell. No commercial tool I have tested does reliable cross-language comparison at scale, Codequiry included, and anyone who tells you otherwise is selling you a demo. What Codequiry does is shrink the manual work around it: 65 approved languages with a custom parser submission path, peer similarity plus web and GitHub matching in a single check, and an AI-generation score on every file regardless of what language it is in. That last part matters more than it sounds, and I will explain why below.

Four approaches that survive translation

1. A common intermediate representation. Compile both submissions to the same IR, then compare the IR. LLVM is the obvious target: a C++ file and a Rust file both lower to LLVM IR, and the loop structure survives. The catch is that IR comparison is brittle in ways source comparison is not. Optimization levels change the graph. A student who reorders two independent statements gets a different basic block layout for no meaningful reason. I have seen this work in research prototypes and I have not seen a department run it in production.

2. Normalized control-flow comparison. Cruder and more practical. Collapse each submission to a sequence of control-flow and operation tokens, ignoring syntax entirely.

from tree_sitter import Language, Parser

# Collapse any parse into a language-neutral shape.
CF_TOKENS = {
    "if_statement": "BRANCH", "for_statement": "LOOP",
    "while_statement": "LOOP", "return_statement": "RET",
    "assignment": "ASSIGN", "call": "CALL",
}

def shape(node, out):
    tag = CF_TOKENS.get(node.type)
    if tag:
        out.append(tag)
    for child in node.children:
        shape(child, out)
    return out
# Python result: ASSIGN LOOP BRANCH ASSIGN RET
# C++ result:    ASSIGN LOOP BRANCH ASSIGN RET

Run the Python sum_even above through a tree-sitter grammar and you get one token string. Run the C++ version through the C++ grammar and you get the same string. That is a 100% match on structural shape, which is a terrible signal by itself (a lot of student code has that shape) but a fine candidate generator. Flag every cross-language pair above 90% shape similarity, then read them yourself. On my sample that produced about 40 candidate pairs per hundred submissions, which is too many for a TA to read on a deadline.

3. AI-generation scores as a translation signal. This one surprised me. LLM-written code has a signature: uniform comment density, defensive input validation nobody asked for, docstrings on trivial helpers, a tendency to name things result and data. When a student pastes a classmate's algorithm into Claude and asks for it in Rust, the output carries those marks even though the human-written original did not. In 13 of my 61 cases, the AI score on the translated submission was the strongest single signal in the folder. Not proof. A reason to look.

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.

4. Web checks against the translated-from source. If the code came from a tutorial rather than a peer, the original usually lives somewhere public. Ten of my 61 cases traced back to a GitHub repository or a Stack Overflow answer in a different language, because the student translated the tutorial instead of the classmate. The source code plagiarism checker pass found these because it compares against the open web, not just the submitted cohort.

What the numbers do not prove

Three honest limits.

My "confirmed" label is one reviewer's judgment on most pairs. We double-reviewed 200 of them and the second reviewer agreed on 91%, so the error rate on classification is probably under 10%, but "under 10%" on 61 cases is not a precise instrument. Treat the 3.3% as an order of magnitude, not a benchmark.

The rates are also a function of assignment design. My language-flexible courses were mostly upper-division electives with short, algorithmic prompts. Short algorithmic prompts are the easiest thing in the world to translate and the easiest thing in the world to generate. Change the prompt and you change the number.

And I have not tested the LLM-adjudication path past a few hundred pairs. Feeding two files to a model and asking "is one a translation of the other" works better than I expected on my sample, but it costs money per pair and it will confidently say yes to two different implementations of the same textbook algorithm. Screening tool, not a verdict.

What to change before the next term

If your department runs language-flexible courses, per-language similarity scoring is not covering the assignment. Say so out loud in the policy meeting, because someone will assume it is.

Run one structural pass across languages every term, even a crude one. The token-shape script above is 30 lines and I wrote it in an afternoon. It generated every candidate pair I needed; the rest was reading.

Score AI generation on every submission regardless of language. Translation leaves a fingerprint even when the source was human, and it is the cheapest cross-language signal you have.

Fix the syllabus language. Most honor codes say "submitted work must be your own" and stop there. Students genuinely believe that reimplementing a peer's algorithm in a different language is original work. Tell them it is not, in the same paragraph where you describe the assignment, and you will spend less of January on appeals.

The tooling question is the easy one. If you want peer similarity, web and GitHub matching, and AI-generation scoring in a single check rather than three tools with three logins, that is what Codequiry was built for, and the API means you can wire your structural pass into the same pipeline instead of running it by hand in December. If you want to see how it lines up against the tool you already have, the Codequiry vs MOSS comparison is a fair place to start.

One more thing, and this is the part that cost me the most time. Pull the language each student used into your submission records before the term starts, not after. Half my review hours went to reconstructing which files could even be compared to which. That is a spreadsheet problem, not a detection problem, and I should have solved it in September.

Frequently asked questions

Can MOSS detect code that was translated from another language?

No. MOSS tokenizes and fingerprints within a single language, set per run with the -l flag, so a Python file and a C++ file never enter the same comparison. It can find the original if the original is on the web and you upload the right language, but it will not match the translated pair to each other.

Is translating a classmate's code into another language still plagiarism?

Under most university honor codes, yes. The submission is still derived from someone else's work without attribution. The practical problem is that many students do not know that, so the policy needs to say it explicitly rather than relying on a general "your own work" clause.

How common is cross-language code plagiarism in CS courses?

In my two-term sample, 3.3% of 1,847 submissions were confirmed translations, but the rate was heavily concentrated: 8.0% in courses that let students choose their implementation language versus 1.0% in single-language courses. If every assignment in your course specifies Java, you have a smaller problem than the elective down the hall.

If you are auditing a submission set right now, run the cohort through a code plagiarism check that covers peer, web and AI signals in one pass, then read the cross-language candidates yourself. That is the workflow that actually found cases for me.