Cross-language code plagiarism detection is the task of finding code that was translated from one programming language to another while preserving the underlying algorithm and structure. It works by stripping away language-specific syntax and comparing normalized token streams, abstract syntax trees, control flow graphs, and now code embeddings. I spent most of a weekend in March 2024 staring at two versions of a shunting-yard parser, one in Java and one in Python, and the process is messier than most tool marketing suggests.
The submission came in at 11:47pm, six minutes before the deadline. I teach a 14-week full-stack bootcamp cohort, and in week six we run a language split assignment. Half the class writes a command-line expression evaluator in Java, the other half in Python. The spec is identical, but the language assignment locks students into separate similarity pools. Or so I believed.
Two students submitted code that looked completely different on the surface. One file used Java idiomatic style: getters, explicit types, a helper class called TokenizerUtility. The other was Python: snake_case functions, list comprehensions, a module docstring. MOSS 2.0 returned a 0% match. JPlag 4.2.0 reported 11%, which is below the threshold I would normally open. The only reason I looked closer was an odd set of variable names: upstream_token, current_char, operator_stack. They had not just copied code. They had transliterated a Java solution into Python by hand, function by function.
// Java submission
private boolean isOperator(char c) {
return c == '+' || c == '-' || c == '*' || c == '/';
}
private int precedence(char op) {
if (op == '+' || op == '-') return 1;
if (op == '*' || op == '/') return 2;
return -1;
}
# Python submission
def is_operator(c):
return c == '+' or c == '-' or c == '*' or c == '/'
def precedence(op):
if op == '+' or op == '-':
return 1
if op == '*' or op == '/':
return 2
return -1
Raw text match: zero. But the computation is identical. That is the core problem cross-language detection tries to solve.
A submission that should not have matched
The two files above are short enough to eyeball. Real assignments are not. A typical week six expression evaluator runs between 150 and 400 lines. One student may split the parser into ten functions. The other may use a single function with nested conditionals. When the target language changes, every keyword, every delimiter, every indentation convention shifts. A detector that compares raw lines will see nothing. A detector that compares token streams may see partial overlap. A detector that compares structure can see the original algorithm hiding underneath.
That structural signal is not magic. It is a byproduct of how students actually translate code. They read the original function, understand its control flow, then rewrite it in the target language. A loop stays a loop. An if checking for a right parenthesis stays an if checking for a right parenthesis. A variable that holds the current token still holds the current token, even if its name changes from upstream_token to tok or t.
The thing that survives translation is not the code. It is the shape of the computation.
Why cross language cheating is harder to catch than copy paste
A same-language copy often preserves whitespace, formatting, comments, and even variable names. Text-based fingerprinting like MOSS uses k-gram hashing over language-specific token streams. Two lines pasted from Stack Overflow will produce a long run of matching tokens. Cross-language cheating destroys exactly that signal. The lexical spellings of keywords change, bracket and indentation conventions change, and the code is often rewritten to match the target language's idioms.
But some signals survive because they are properties of the algorithm, not the language. A loop that scans a string from left to right will still be a loop. A recursive descent parser will still have a function that checks the current character. A number like 0.75 that controls a threshold will usually not become 0.8 unless the student has changed the behavior deliberately. A delimiter string like " " used to split tokens often survives verbatim because it is easy to overlook.
JPlag 4.1's Python parser silently dropped docstrings in one release, and a comment-heavy solution looked roughly 20% less similar than it should. That version bug made me rerun a cohort check with JPlag 3.8 before I trusted the numbers. It is the kind of detail you remember when you are comparing tools at 2am.
What cross-language code plagiarism detection actually measures
A good cross-language detector does not try to read code as a human would. It normalizes away everything language-specific and keeps the following signals:
- Control flow structure: loops, conditionals, exception handling, early returns.
- Data dependency: which values feed into which operations.
- Identifier roles: the number of variables, their mutability, and the order they appear.
- Literal fingerprints: numeric constants, delimiter characters, error strings.
- Order of operations: the sequence in which the program touches tokens or AST nodes.
Those signals do not have to be exact. Two functions can differ in local variable names and formatting and still produce an 80% or 90% structural match. A detector that returns only 0% or 100% is not measuring structure. It is measuring length and raw text overlap.
Token based matching gets you maybe 40 percent of the way
If you tokenize the Java and Python functions above and normalize away keyword differences, you can get to a shared sequence.
FUNC isOperator PARAM c RETURN (c EQ '+' OR c EQ '-' OR c EQ '*' OR c EQ '/')
FUNC precedence PARAM op IF (op EQ '+' OR op EQ '-') RETURN 1 IF (op EQ '*' OR op EQ '/') RETURN 2 RETURN -1
That sequence is short. For a 150-line program, MOSS-style winnowing can fingerprint overlapping token windows and catch near-identical copies within the same language. But the method is brittle. If the student introduces an extra variable or splits a compound condition into two if statements, the window hashes change and the match falls apart. Token-based matching alone catches obvious translations, the kind where the student swapped && for and and left everything else untouched. It misses deliberate refactoring.
JPlag uses language-specific tokenization and can compare AST features within a single language, but most deployed JPlag setups still compare Java to Java or Python to Python. Cross-language matching requires either a normalized AST or a graph-level representation.
AST normalization is where the real signal lives
Every language parser produces an abstract syntax tree. A Java method declaration and a Python function definition have different node names, but you can map them to a common schema if you know both grammars.
MethodDecl(name=isOperator, params=[c])
ReturnStmt
BinaryExpr(op=OR)
BinaryExpr(op=EQ, left=Var(c), right=Literal('+'))
BinaryExpr(op=EQ, left=Var(c), right=Literal('-'))
BinaryExpr(op=EQ, left=Var(c), right=Literal('*'))
BinaryExpr(op=EQ, left=Var(c), right=Literal('/'))
Both the Java and Python versions produce this tree after node normalization. At that point the languages disappear, and you can run tree edit distance, subtree isomorphism, or hash the AST node sequences into fingerprints. A 2022 program comprehension paper reported precision near 0.92 on translated Java to Python pairs using AST fingerprints, but that was under controlled benchmark conditions, not a real classroom.
When I opened the match review from a structural check, the side-by-side view showed the Java method and the Python function with overlapping AST node sequences highlighted. That visual matters. A similarity score alone does not persuade a student or an academic integrity panel. The highlighted overlap lets you ask specific questions about why the precedence function returns the same three integers in the same order.

AST matching also survives local renaming better than token matching. If the student renames every variable to a single letter, the AST structure holds. If they reorder independent conditions, the tree changes less than you would expect. But if they inline a helper function, the AST flattens and the match score drops. That is where graph-based methods help.
Program dependency graphs catch what tokens miss
A program dependency graph, or PDG, is a graph where nodes are individual statements or operations and edges connect data producers to consumers, plus control dependencies. The Java precedence function and the Python precedence function produce nearly identical PDGs: a decision on op == '+' or op == '-', an edge to return 1, another decision on op == '*' or op == '/', an edge to return 2, and a fallthrough to return -1.
Rename a variable and the node label changes but the graph structure does not. Inline a function and the PDG changes more dramatically, which is where these methods start to strain. Full PDG matching on a 200-student cohort is computationally heavy. Most practical tools approximate it by building a normalized control flow graph and hashing small subgraphs, which gets you maybe 80% of the benefit at a fraction of the cost.
Codequiry's engine builds fingerprints from AST node sequences and control-flow edges rather than raw text. That is why it survives reformatting and identifier renaming, and why it can surface translated code without you needing to run three separate tools for the same assignment.
Embedding based detection is a screening signal, not proof
Since 2020, transformer models like CodeBERT and GraphCodeBERT have been trained on code in multiple programming languages. If you take a Java function and a Python function and encode both through CodeBERT, the resulting vectors sit close together when the functions implement the same algorithm. Academic benchmarks for cross-language clone detection using CodeBERT embeddings report precision around 0.87 on Java-Python pairs. Impressive, but less interpretable than AST matching. You cannot point at an embedding and tell a student why the model thought two functions were similar. You can point at AST nodes.
I use embeddings as a screening signal, not evidence. In my June 2024 cohort I ran a small Python script that encoded every submission, computed pairwise cosine similarities, plotted a heatmap, and flagged only the top 15 pairs for manual review. One of those pairs was the Java-Python shunting-yard clone. But I would not put an embedding score in an academic integrity report by itself. It is a lead generator, and a noisy one at that.
Where cross-language detection still fails
Cross-language detection is not a solved problem. If a student takes the Java algorithm and writes an idiomatic Python version that uses the operator module functions instead of raw comparisons, the AST changes enough that normalized tree matching loses the thread. If they change recursive descent into a stack-based loop, the control flow graph no longer matches. If they rename every variable to single letters and reorder conditionals while preserving behavior, many detectors miss it.
False positives are real. Simple functions that wrap library calls often look similar across languages because there are only a few ways to write them. I have seen a short read_file_to_string function in JavaScript and Python flagged as a cross-language match when both students simply followed the same tutorial. That is why any detector score below roughly 65% on code under 20 lines should be reviewed, not acted on.
I have not tested these methods past a few hundred pairs in bootcamp submissions, so I would not claim they generalize to an undergraduate data structures course with 400 students. The failure modes scale with class size and assignment variety.
How we caught a translated Java to Python clone in a bootcamp
After the 11% JPlag result, I did not want to accuse anyone. I uploaded the two submissions to Codequiry, set the check to Java and Python, and let the structural comparison run. The match score came back at 91%.

The report showed matching AST node sequences and control-flow patterns. Not just the tiny helper functions; the entire shunting-yard loop and the output formatting matched. That 91% was not a raw text match. It was structural overlap between a Java file and a Python file that had no business sharing a similarity pool.
The student review was not easy. The two students had worked together in prior weeks, and they claimed one had just explained the algorithm to the other. The similarity report gave us something concrete: the order of operations, the name of a helper variable upstream_token that had no reason to exist in the Python translation, and a literal delimiter string " " used identically. The panel had enough to ask the right questions. The students eventually admitted the Java author shared their file on Discord and the Python author translated it line by line.
If we had relied on MOSS alone, that case would have closed with a note saying no significant similarity was found. That is the gap a cross-language check fills.
Where a source code plagiarism checker fits in a real review workflow
The workflow that worked for me was not sophisticated. I exported the week six submissions from a GitLab group, dropped them into a new Codequiry check, and selected the two languages in the check dialog. The scan ran in the background while I finished commenting on another cohort's pull requests.

Once the scan finished, I looked first at the highest structural matches, then at the web similarity hits, then at the AI-generated code scores. Each signal answers a different question. A peer match asks whether two students submitted the same computation. A web match asks whether the code came from GitHub or Stack Overflow. An AI score asks whether a model wrote the original file. Stacking them catches cases a single check would miss.
The code plagiarism checker side of Codequiry is what I keep coming back to because it displays peer matches, web matches, and AI scores in one review queue. MOSS and JPlag only cover peer similarity within a single language. Turnitin's text-facing engine is not built for source structure. That is why a dedicated source code plagiarism checker earns its place in a bootcamp or university workflow.
A note on AI generated code and cross language signals
If a student asks Copilot to translate Java to Python, the output often has low perplexity and unusual comment patterns. Running an AI code detector in parallel with a cross-language similarity check doubles your evidence. The AI detector answers whether a model generated the file. The similarity check answers whether the file is the same algorithm as another student's submission. They are different questions.
In my cohort, I found two students who had used ChatGPT to produce the Python translation. The Codequiry AI score on the Python file was in the high 80s, but that alone was not enough. It was the combination of the original Java match plus the AI signal that made the case stick. One signal is a suspicion. Two signals are a pattern.
Cross-language detection is not a courtroom tool. It is a review queue optimizer. It tells you which pairs of submissions deserve a human look, and it gives you a structural overlay to guide that look. For anyone teaching a language-flexible assignment or reviewing take-home projects, that is the difference between a missed case and a conversation.