JuLC Compiler Troubleshooting Guide
This guide maps every compiler, validation, configuration, and runtime error to its cause and solution. All error messages shown here are taken directly from the JuLC source code.
1. Compilation Errors (PirGenerator)
Section titled “1. Compilation Errors (PirGenerator)”These errors are emitted during PIR (Plutus Intermediate Representation) generation, when the compiler translates Java AST nodes into UPLC-compatible terms.
Conditional update to loop-body local '<name>' is not supported
Section titled “Conditional update to loop-body local '<name>' is not supported”An if/else inside a loop updates a local declared in the loop body outside
that branch. The branch carries only loop accumulators out; the old lowering
silently lost this local’s update. Even constant conditions and unused updates
are rejected.
// Rejected inside a loop:BigInteger step = BigInteger.ZERO;if (positive) { step = BigInteger.ONE; }acc = acc.add(step);
// Use a conditional initializer:BigInteger step = positive ? BigInteger.ONE : BigInteger.ZERO;acc = acc.add(step);Alternatively, declare the variable before the loop as an accumulator (reset it each iteration when needed), or declare it inside the branch if its value is only used there. Existing break-aware and native-accumulator restrictions still apply; see the full rules.
Switch-expression arm cannot update enclosing variable '<name>'
Section titled “Switch-expression arm cannot update enclosing variable '<name>'”A switch exports only its yielded result, not assignments to enclosing variables.
A nested loop inside an arm does not change this rule. It applies to outer
accumulators as well as loop-body locals, with or without an enclosing if.
// Correct: the accumulator belongs to the arm and its result is explicitly yielded.BigInteger step = switch (action) { case Only o -> { BigInteger local = BigInteger.ZERO; for (var y : xs) { local = local.add(y); } yield local; }};Do not declare local outside the arm and expect the loop to update it there.
Loops inside an if at method level or in a switch arm now preserve their
accumulator updates after the branch (#161). If using an older compiler affected
by this bug, upgrade and recompile; script hashes and budgets may change.
This includes a switch expression inside an outer loop body: declare the
accumulator inside the arm, update it with the guarded loop, and yield its result.
Older versions may conservatively reject this composition as a conditional
loop-body local update. Updates to variables declared outside the arm, or to
actual loop-body locals across an if, remain rejected.
Reassignment of switch case-pattern variable '<name>' is not supported
Section titled “Reassignment of switch case-pattern variable '<name>' is not supported”Updating a case-pattern binding could leave field projections pointing at its original record (#162). Reassignment is now rejected, including when only the record is yielded. Copy the binding to a fresh arm-local accumulator:
case Only p -> { Only local = p; for (var y : xs) { local = new Only(y); } yield local.value();}For [1, 2, 3], this yields 3; assigning to p instead is a compile-time error.
Otherwise-supported instanceof binding reassignment is unaffected.
1.1 Method must have a body: <name>
Section titled “1.1 Method must have a body: <name>”Cause: An abstract or interface method was encountered where the compiler expects a concrete method body.
Fix: Ensure every method in your validator class has a full body. On-chain code cannot contain abstract methods.
// WRONG: abstract method@SpendingValidatorpublic class MyValidator { @Entrypoint public abstract boolean validate(Data redeemer, ScriptContext ctx);}
// CORRECT: method with body@SpendingValidatorpublic class MyValidator { @Entrypoint public static boolean validate(Data redeemer, ScriptContext ctx) { return true; }}1.2 Variable must be initialized: <name>
Section titled “1.2 Variable must be initialized: <name>”Full message: Variable must be initialized: <name>. Hint: On-chain variables need initial values, e.g. var <name> = BigInteger.ZERO;
Cause: A local variable was declared without an initializer. On-chain code requires all variables to be initialized at declaration because UPLC has no concept of uninitialized memory.
Fix: Always assign an initial value.
// WRONGvar total;
// CORRECTvar total = BigInteger.ZERO;1.3 break statement outside of a loop
Section titled “1.3 break statement outside of a loop”Full message: break statement outside of a loop
Hint: break can only be used inside for-each or while loops.
Cause: A break statement appeared outside of a for-each or while loop scope.
Fix: Move the break inside a loop body, or restructure your logic to use
return instead.
// WRONGif (someCondition) { break; // not inside a loop}
// CORRECTfor (var item : items) { if (someCondition) { break; // inside a for-each loop }}1.4 Unsupported statement: <StatementType>
Section titled “1.4 Unsupported statement: <StatementType>”Full message: Unsupported statement: <StatementType>
Hint: Only variable declarations, if/else, for-each, while, return, and expression statements are supported on-chain.
Cause: A Java statement type that has no on-chain equivalent was used.
Unsupported statements include switch statements (as opposed to switch
expressions), try/catch, do/while, labeled statements, and others.
Fix: Rewrite using the supported statement forms: variable declarations, if/else, for-each, while, return, and expression statements.
// WRONG: switch statementswitch (value) { case 1: doSomething(); break;}
// CORRECT: switch expression (for sealed interface pattern matching)var result = switch (action) { case Bid b -> b.amount(); case Cancel c -> BigInteger.ZERO;};1.5 Unsupported statement in break-aware loop body: <StatementType>
Section titled “1.5 Unsupported statement in break-aware loop body: <StatementType>”Hint: Inside loops with break, only variable declarations, assignments, if/else, and break are supported.
Cause: A loop body that contains break used an unsupported statement type.
When a loop body contains break, the compiler uses a specialized translation
path that supports a narrower set of statements.
Fix: Simplify the loop body to contain only variable declarations, accumulator assignments, if/else, and break.
// WRONG: nested for-each inside a break-aware loop bodyfor (var item : items) { for (var sub : item.children()) { // not supported inside break-aware body if (condition) break; }}
// CORRECT: nested for-each without break is supportedfor (var item : items) { for (var sub : item.children()) { // OK — no break in outer loop sum = sum + sub.amount(); }}
// CORRECT: use a helper method for the break patternvar found = false;for (var item : items) { if (checkCondition(item)) { found = true; break; }}Note: Nested loops (while-in-while, for-each-in-for-each, mixed) are fully supported. The restriction is specifically about nesting inside a break-aware loop body — when the outer loop uses break, the inner statements must be limited to declarations, assignments, if/else, and break.
1.6 Unsupported in multi-acc loop body: <StatementType>
Section titled “1.6 Unsupported in multi-acc loop body: <StatementType>”Hint: Inside multi-accumulator loops, only variable declarations, assignments, and if/else are supported.
Cause: A for-each or while loop that updates multiple accumulators used an unsupported statement. Multi-accumulator loops are translated by packing all accumulators into a Data list tuple, which restricts the supported constructs.
Fix: Keep the loop body to variable declarations, accumulator assignments, and if/else.
// CORRECT: multi-accumulator loopvar count = BigInteger.ZERO;var total = BigInteger.ZERO;for (var item : items) { count = count + BigInteger.ONE; total = total + item.value();}1.7 Unsupported in multi-acc break-aware body: <StatementType>
Section titled “1.7 Unsupported in multi-acc break-aware body: <StatementType>”Hint: Inside multi-accumulator loops with break, only variable declarations, assignments, if/else, and break are supported.
Cause: A multi-accumulator loop that also contains break used an unsupported
statement. This is the most restrictive loop compilation mode.
Fix: Use only variable declarations, assignments, if/else, and break in the loop body.
1.8 Unsupported BigInteger field: <fieldName>
Section titled “1.8 Unsupported BigInteger field: <fieldName>”Full message: Unsupported BigInteger field: <fieldName>
Hint: Supported BigInteger fields: ZERO, ONE, TWO, TEN. Use new BigInteger("value") for other constants.
Cause: A BigInteger static field other than ZERO, ONE, TWO, or TEN
was referenced.
Fix: Use new BigInteger("value") for arbitrary constants.
// WRONGvar x = BigInteger.valueOf(42); // valueOf is fine for runtime, but...var y = BigInteger.NEGATIVE_ONE; // this field is not supported
// CORRECTvar x = BigInteger.valueOf(42); // valueOf IS supportedvar y = new BigInteger("-1"); // use string constructor for other values1.9 new BigInteger() requires a string literal argument
Section titled “1.9 new BigInteger() requires a string literal argument”Hint: Use new BigInteger("12345") or BigInteger.valueOf(n) for integer constants.
Cause: new BigInteger(expr) was called with a non-string-literal argument
(e.g., a variable).
Fix: Pass a string literal directly to the BigInteger constructor.
// WRONGvar s = "100";var amount = new BigInteger(s); // variable, not a string literal
// CORRECTvar amount = new BigInteger("100"); // string literalvar small = BigInteger.valueOf(42); // also works for int-range values1.10 Cannot construct non-record type: <typeName>
Section titled “1.10 Cannot construct non-record type: <typeName>”Hint: Only record types can be constructed on-chain. Define <typeName> as a record.
Cause: A new ClassName(...) expression was used with a type that is not a
Java record or a variant of a sealed interface.
Fix: Define your data types as records.
// WRONG: regular classpublic class Bid { public BigInteger amount; public Bid(BigInteger amount) { this.amount = amount; }}
// CORRECT: recordpublic record Bid(BigInteger amount) {}1.11 switch expression requires a sealed interface type, got: <type>
Section titled “1.11 switch expression requires a sealed interface type, got: <type>”Hint: Ensure the switch variable's type is a sealed interface with record variants.
Cause: A switch expression was used on a variable that is not typed as a
sealed interface with record variants.
Fix: Ensure the switched-on variable is a sealed interface with record implementations.
// Define the sum typepublic sealed interface Action permits Bid, Cancel {}public record Bid(BigInteger amount) implements Action {}public record Cancel() implements Action {}
// CORRECT usagevar result = switch (action) { case Bid b -> b.amount(); case Cancel c -> BigInteger.ZERO;};1.12 Unknown variant in switch: <typeName>
Section titled “1.12 Unknown variant in switch: <typeName>”Hint: Available variants: <list>
Cause: A case label in a switch expression referenced a type name that is
not a known variant (constructor) of the sealed interface.
Fix: Check spelling and ensure the type is listed as a permits variant of
the sealed interface.
// WRONG: "Offer" is not a variant of Actionvar result = switch (action) { case Offer o -> o.price(); // Offer not in permits list case Cancel c -> BigInteger.ZERO;};
// CORRECTvar result = switch (action) { case Bid b -> b.amount(); // Bid is a permitted variant case Cancel c -> BigInteger.ZERO;};1.13 Unsupported instanceof pattern: <expression>
Section titled “1.13 Unsupported instanceof pattern: <expression>”Hint: instanceof is supported only with sealed interface variants: if (x instanceof Variant v) { ... }
Cause: instanceof was used with a type that is not a variant of a sealed
interface, or the pattern form is not supported.
Fix: Use instanceof only for sealed interface variant matching.
// WRONG: instanceof with a non-variant typeif (obj instanceof String s) { ... }
// CORRECT: instanceof with sealed interface variantif (action instanceof Bid b) { var amount = b.amount();}1.14 Non-exhaustive switch on sealed interface: missing cases [X, Y]
Section titled “1.14 Non-exhaustive switch on sealed interface: missing cases [X, Y]”Cause: A switch expression on a sealed interface does not cover all variants. The compiler checks exhaustiveness before generating DataMatch.
Fix: Add explicit cases for all variants of the sealed interface, or use default -> as a catch-all for uncovered variants.
sealed interface Action permits Bid, Cancel, Update {}record Bid(BigInteger amount) implements Action {}record Cancel() implements Action {}record Update(BigInteger newPrice) implements Action {}
// WRONG: missing Update casevar result = switch (action) { case Bid b -> b.amount(); case Cancel c -> BigInteger.ZERO;};// Error: Non-exhaustive switch on sealed interface: missing cases [Update]
// CORRECT: all cases coveredvar result = switch (action) { case Bid b -> b.amount(); case Cancel c -> BigInteger.ZERO; case Update u -> u.newPrice();};1.15 Method does not return on all execution paths
Section titled “1.15 Method does not return on all execution paths”Cause: A method has code paths that do not end with a return statement. The compiler analyzes if/else completeness, fallthrough returns, and loop-as-return patterns.
Fix: Ensure every code path returns a value.
// WRONG: missing return on else pathstatic boolean check(BigInteger x) { if (x > 0) { return true; } // no return here}
// CORRECTstatic boolean check(BigInteger x) { if (x > 0) { return true; } else { return false; }}1.16 @NewType requires exactly one field
Section titled “1.16 @NewType requires exactly one field”Cause: A record annotated with @NewType has zero or more than one field.
Fix: @NewType records must have exactly one field of a supported primitive type (byte[], BigInteger, String, boolean).
// WRONG: two fields@NewTyperecord AssetId(byte[] policy, byte[] name) {}
// CORRECT: single field@NewTyperecord PolicyId(byte[] hash) {}1.17 Unsupported lambda body: <BodyType>
Section titled “1.17 Unsupported lambda body: <BodyType>”Hint: Lambda bodies must be a single expression or a block statement.
Cause: A lambda expression has an unexpected body form.
Fix: Use either a single expression or a block body.
// CORRECT: expression bodyvar doubled = ListsLib.map(items, item -> item * 2);
// CORRECT: block bodyvar filtered = ListsLib.filter(items, item -> { var threshold = new BigInteger("100"); return item > threshold;});1.15 Unknown method: <methodName>
Section titled “1.15 Unknown method: <methodName>”Hint (fuzzy match): Did you mean '<ClassName.methodName>()'?
Hint (no match): Library methods require class qualification (e.g., MathLib.<methodName>(...)).
Cause: A method call could not be resolved. The method is not a local helper, a stdlib method, a record accessor, or a known type method.
Fix: Qualify static library calls with their class name. Check spelling.
// WRONG: unqualified stdlib callvar len = length(myList);
// CORRECT: qualified stdlib callvar len = ListsLib.length(myList);
// WRONG: typovar total = MathLib.abss(value);
// CORRECT: compiler will suggest "Did you mean 'MathLib.abs()'?"var total = MathLib.abs(value);1.16 Unsupported operator: <operator>
Section titled “1.16 Unsupported operator: <operator>”Hint: Supported operators: +, -, *, /, %, ==, !=, <, <=, >, >=, &&, ||
Cause: A binary operator that has no UPLC equivalent was used (e.g., bitwise
operators &, |, ^, <<, >>).
Fix: Use the supported operators. For bitwise operations, use BitwiseLib.
// WRONGvar result = a & b;var shifted = x << 2;
// CORRECTvar result = BitwiseLib.andByteString(false, a, b);var shifted = BitwiseLib.shiftByteString(x, 2);1.17 Unsupported unary operator: <operator>
Section titled “1.17 Unsupported unary operator: <operator>”Hint: Supported unary operators: ! (logical NOT) and - (negation)
Cause: A unary operator other than ! or - was used (e.g., ~, ++, --).
Fix: Use ! for boolean negation and - for arithmetic negation.
// WRONGvar next = count++;var flipped = ~bits;
// CORRECTvar negated = !condition;var negative = -amount;1.18 Unsupported expression: AssignExpr
Section titled “1.18 Unsupported expression: AssignExpr”Hint: Variables are immutable on-chain. Use the accumulator pattern in for-each/while loops for mutable state.
Cause: An assignment expression was used outside of the accumulator pattern in a loop. On-chain UPLC does not support mutation.
Fix: Variables are immutable. Use the accumulator pattern in for-each or while loops.
// WRONG: assignment outside loop accumulatorvar x = BigInteger.ZERO;x = BigInteger.ONE; // not inside a loop accumulator context
// CORRECT: accumulator pattern in for-eachvar total = BigInteger.ZERO;for (var item : items) { total = total + item.value(); // accumulator assignment in loop}1.19 Unsupported expression: <ExpressionType>
Section titled “1.19 Unsupported expression: <ExpressionType>”Hint: This expression type is not supported in on-chain code.
Cause: A Java expression type that has no on-chain compilation path was used.
Fix: Rewrite using the supported expression forms: literals, variables, binary and unary expressions, method calls, field access, record construction, ternary expressions, lambdas, switch expressions, instanceof, and casts.
1.20 Undefined variable: <name>
Section titled “1.20 Undefined variable: <name>”Cause: A variable name was used that is not in scope. This is thrown by
SymbolTable.require() when no matching variable definition is found.
Fix: Ensure the variable is declared before use, or check for typos.
// WRONG: typovar totl = BigInteger.ZERO;return total; // "total" not defined, "totl" is
// CORRECTvar total = BigInteger.ZERO;return total;An instanceof pattern variable is only in scope when the pattern is the whole
condition of an if statement outside a loop. Used anywhere else, for example
if (x instanceof Bid b && b.amount() > 0) or inside a loop body, it is reported as
undefined until #204. Use a switch on the sealed type there, which binds case
variables in every position.
2. Validation Errors (SubsetValidator)
Section titled “2. Validation Errors (SubsetValidator)”These errors are emitted during the subset validation pass, before PIR generation begins. They reject Java constructs that have no on-chain equivalent. Each error includes the file name, line number, and column.
2.1 try/catch is not supported on-chain
Section titled “2.1 try/catch is not supported on-chain”Suggestion: Use if/else checks instead of exception handling
Cause: A try/catch/finally block was used. UPLC has no exception handling
mechanism.
Fix: Replace with explicit if/else validation logic.
// WRONGtry { riskyOperation();} catch (Exception e) { return false;}
// CORRECTif (!isValid(input)) { return false;}2.2 throw is not supported on-chain
Section titled “2.2 throw is not supported on-chain”Suggestion: Return false from the validator to reject a transaction
Cause: A throw statement was used. On-chain validators reject transactions
by returning false, not by throwing exceptions.
Fix: Return false to reject a transaction.
// WRONGif (!authorized) { throw new RuntimeException("Unauthorized");}
// CORRECTif (!authorized) { return false;}2.3 synchronized is not supported on-chain
Section titled “2.3 synchronized is not supported on-chain”Suggestion: On-chain code is single-threaded; remove synchronized blocks
Cause: A synchronized block or method was used. On-chain execution is
single-threaded and deterministic.
Fix: Remove the synchronized keyword.
2.4 C-style for loops are not supported on-chain
Section titled “2.4 C-style for loops are not supported on-chain”Suggestion: Use for-each over a list or while loops instead
Cause: A traditional for (init; cond; update) loop was used.
Fix: Use for-each over a list or a while loop with an accumulator.
// WRONGfor (int i = 0; i < 10; i++) { // ...}
// CORRECTfor (var item : items) { // process item}
// CORRECT: while loop with accumulatorvar i = BigInteger.ZERO;while (i < BigInteger.TEN) { // process i = i + BigInteger.ONE;}2.5 break is only supported inside for-each or while loops on-chain
Section titled “2.5 break is only supported inside for-each or while loops on-chain”Suggestion: Use for-each or while with an accumulator and break to exit early
Cause: A break statement appeared outside of a for-each or while loop.
Fix: Place break inside a for-each or while loop.
2.6 do-while loops are not supported on-chain
Section titled “2.6 do-while loops are not supported on-chain”Suggestion: Use while loops or for-each instead
Cause: A do { ... } while (cond) loop was used.
Fix: Rewrite as a while loop.
// WRONGdo { process();} while (hasMore);
// CORRECTwhile (hasMore) { process();}2.7 null is not supported on-chain
Section titled “2.7 null is not supported on-chain”Suggestion: Use Optional<T> to represent absence of a value
Cause: The null literal was used. UPLC does not have a null concept.
Fix: Use Optional<T> or sentinel values.
// WRONGData result = null;
// CORRECTvar result = Optional.empty();2.8 'this' is not supported on-chain
Section titled “2.8 'this' is not supported on-chain”Suggestion: On-chain validators are stateless; use static methods instead
Cause: The this keyword was used. On-chain validators are purely functional
and stateless.
Fix: Use static methods and pass all data as parameters.
// WRONGpublic boolean validate() { return this.checkSignature();}
// CORRECTpublic static boolean validate(ScriptContext ctx) { return checkSignature(ctx);}2.9 'super' is not supported on-chain
Section titled “2.9 'super' is not supported on-chain”Suggestion: Use sealed interfaces and pattern matching instead of inheritance
Cause: The super keyword was used. Inheritance is not supported on-chain.
Fix: Use sealed interfaces with record variants and pattern matching.
2.10 arrays are not supported on-chain
Section titled “2.10 arrays are not supported on-chain”Suggestion: Use List<T> instead of arrays
Cause: An array creation expression (e.g., new int[10]) was used.
Fix: Use List<T> instead.
// WRONGvar data = new byte[32];
// CORRECT// Use ByteString for byte arrays, or List<T> for element listsPV11 Note: From protocol version 11,
JulcArray<T>provides O(1) random-access arrays via CIP-138 builtins. Uselist.toArray()to convert a list.
2.11 array access is not supported on-chain
Section titled “2.11 array access is not supported on-chain”Suggestion: Use List<T> with list operations instead
Cause: Array indexing (e.g., arr[i]) was used.
Fix: Use list operations from ListsLib.
// WRONGvar first = items[0];
// CORRECTvar first = Builtins.headList(items);// orvar nth = ListsLib.nth(items, 2);PV11 Note: From protocol version 11,
JulcArray<T>provides O(1) random-access arrays via CIP-138 builtins. Uselist.toArray()to convert a list, thenarr.get(index)for O(1) access.
2.12 floating point types (float/double) are not supported on-chain
Section titled “2.12 floating point types (float/double) are not supported on-chain”Suggestion: Use BigInteger for integer arithmetic or Rational for fractions
Cause: A float or double type was used. UPLC only supports exact integer
arithmetic.
Fix: Use BigInteger for integer math. For fractions, use a rational number
pattern (numerator/denominator as BigInteger).
// WRONGdouble price = 1.5;float rate = 0.03f;
// CORRECTvar priceNumerator = new BigInteger("3");var priceDenominator = new BigInteger("2"); // represents 1.52.13 class inheritance is not supported on-chain
Section titled “2.13 class inheritance is not supported on-chain”Suggestion: Use sealed interfaces with record variants instead
Cause: A class extends another class (other than Object).
Fix: Use sealed interfaces with record variants for polymorphism.
// WRONGpublic class SpecialBid extends Bid { ... }
// CORRECTpublic sealed interface Action permits Bid, Cancel {}public record Bid(BigInteger amount) implements Action {}public record Cancel() implements Action {}2.14 JULC0052 Compound assignment operator <op> is not supported on-chain
Section titled “2.14 JULC0052 Compound assignment operator <op> is not supported on-chain”Cause: A bitwise, shift or boolean compound assignment (&=, |=, ^=, <<=, >>=,
>>>=) was used. Only +=, -=, *=, /= and %= on integers, and += on strings, are
lowered (as x = x op y). Java evaluates the right-hand side of &= and |= even when the
result is already known; lowering them as && or || would skip it, so they are rejected
(ADR-060).
Fix: Evaluate the check first, then combine it.
// WRONGfor (var x : xs) { ok &= check(x);}
// CORRECTfor (var x : xs) { boolean c = check(x); ok = ok && c;}2.15 JULC0053 Declare one variable per statement: <declaration>
Section titled “2.15 JULC0053 Declare one variable per statement: <declaration>”Cause: A local declaration declared several variables (BigInteger a = x, b = y;). Only the
first one would be bound, so a later reference to another could read an outer declaration of the
same name. Fields may still declare several variables.
Fix: Split the declaration into one statement per variable.
2.16 JULC0056 Assignment to field '<field>' in a loop is not supported
Section titled “2.16 JULC0056 Assignment to field '<field>' in a loop is not supported”Cause: A loop updates a static field or @Param. On-chain fields are not shared state: the
update rebinds the name only in the rest of the method’s current run. Every other method, and
every other run of the same method, sees the field’s initial value. The update is accepted only
where that gives Java’s result, and the message says which condition failed:
the field is final'<method>' is not an entry method: only a validator entrypoint, a multi-validator handler or thecompileMethodtarget may update a field. A helper or a library method can be called more than once.'<method>' calls itselfor'<caller>' calls '<method>': the entry method would run more than once.the loop is in a lambda: the lambda can run more than once.a static field initializer cannot update a field'<method>' reads it: another method would see the initial value. A method whose local, parameter or lambda variable has the field’s name does not read the field.
Fix: Copy the field into a local accumulator before the loop, update the local, and pass it to the methods that need it.
// WRONG: current() still sees the original countfor (var x : xs) { count += 1;}return current();
// CORRECTlong seen = count;for (var x : xs) { seen += 1;}return seen;2.17 JULC0058 Reassigning loop variable '<name>' is not supported in a loop that updates no variable declared before the loop
Section titled “2.17 JULC0058 Reassigning loop variable '<name>' is not supported in a loop that updates no variable declared before the loop”Cause: A for-each loop reassigns its loop variable, or a local declared in its body, and updates
no variable declared before the loop. Such a loop compiles its body as plain statements, which
cannot reassign a variable. A loop that also updates an earlier variable (an accumulator) can
reassign its loop variable and body locals. The message says local variable for a body local.
Fix: Declare a new local for the new value instead of reassigning.
// WRONGfor (var amount : amounts) { amount = amount.subtract(fee); if (amount.signum() < 0) Builtins.error();}
// CORRECTfor (var amount : amounts) { var net = amount.subtract(fee); if (net.signum() < 0) Builtins.error();}2.18 JULC0059 Pattern variable '<name>' has the same name as a field of <Class>
Section titled “2.18 JULC0059 Pattern variable '<name>' has the same name as a field of <Class>”Cause: An instanceof pattern variable has the same name as a field of the class,
including a @Param field. JuLC binds a pattern variable only when the pattern is the
whole condition of an if statement outside a loop. In any other position (a loop body,
an && condition, a negated guard, a ternary, a lambda) the name resolved to the field
instead, so the code silently read or updated the field (#204). The name is rejected in
every position, including the one where the variable is bound correctly.
Fix: Rename the pattern variable. switch case patterns are not affected.
static Bid best = new Bid(BigInteger.ZERO);
// WRONGif (action instanceof Bid best) { return best.amount(); }
// CORRECTif (action instanceof Bid bid) { return bid.amount(); }3. Configuration Errors (JulcCompiler)
Section titled “3. Configuration Errors (JulcCompiler)”These errors are raised during compiler setup and pipeline orchestration.
3.1 Failed to parse <label> source: <details>
Section titled “3.1 Failed to parse <label> source: <details>”Cause: The Java source code has syntax errors that JavaParser cannot parse.
<label> is either "validator" or "library[n]".
Fix: Ensure the source compiles with javac first. Common causes: missing
semicolons, unmatched braces, invalid Java 21 syntax.
3.2 No validator annotation found (e.g. @SpendingValidator, @MintingValidator)
Section titled “3.2 No validator annotation found (e.g. @SpendingValidator, @MintingValidator)”Cause: The validator source file does not contain any class annotated with a validator annotation.
Fix: Annotate your validator class with one of the recognized annotations.
// WRONG: no annotationpublic class MyValidator { public static boolean validate(Data redeemer, ScriptContext ctx) { return true; }}
// CORRECT@SpendingValidatorpublic class MyValidator { @Entrypoint public static boolean validate(Data datum, Data redeemer, ScriptContext ctx) { return true; }}Recognized annotations: @SpendingValidator, @MintingValidator, @WithdrawValidator, @CertifyingValidator,
@VotingValidator, @ProposingValidator
3.3 No @Entrypoint method found in <ClassName>
Section titled “3.3 No @Entrypoint method found in <ClassName>”Cause: The annotated validator class does not contain a method annotated with
@Entrypoint.
Fix: Add @Entrypoint to the validator’s main method.
@SpendingValidatorpublic class MyValidator { // WRONG: missing @Entrypoint public static boolean validate(Data datum, Data redeemer, ScriptContext ctx) { return true; }}
@SpendingValidatorpublic class MyValidator { @Entrypoint // CORRECT public static boolean validate(Data datum, Data redeemer, ScriptContext ctx) { return true; }}3.4 @SpendingValidator entrypoint must have 2 or 3 parameters
Section titled “3.4 @SpendingValidator entrypoint must have 2 or 3 parameters”Full message: @SpendingValidator entrypoint must have 2 or 3 parameters (datum, redeemer, scriptContext), found <n> in <Class>.<method>()
Cause: A spending validator entrypoint has the wrong number of parameters. Spending validators accept 2 parameters (redeemer, scriptContext) or 3 parameters (datum, redeemer, scriptContext).
Fix: Adjust the parameter count.
// WRONG: 1 parameter@Entrypointpublic static boolean validate(ScriptContext ctx) { ... }
// CORRECT: 2 parameters (datum in ScriptContext)@Entrypointpublic static boolean validate(Data redeemer, ScriptContext ctx) { ... }
// CORRECT: 3 parameters (explicit datum)@Entrypointpublic static boolean validate(Data datum, Data redeemer, ScriptContext ctx) { ... }3.5 @MintingValidator entrypoint must have 2 parameters
Section titled “3.5 @MintingValidator entrypoint must have 2 parameters”Full message: <annotation> entrypoint must have 2 parameters (redeemer, scriptContext), found <n> in <Class>.<method>(). Did you mean @SpendingValidator?
Cause: A non-spending validator entrypoint does not have exactly 2 parameters.
If you specified 3 parameters, the compiler asks if you meant to use
@SpendingValidator.
Fix: Use 2 parameters for non-spending validators, or switch to
@SpendingValidator if you need a datum parameter.
// WRONG: 3 params on a minting validator@MintingValidatorpublic class MyPolicy { @Entrypoint public static boolean validate(Data datum, Data redeemer, ScriptContext ctx) { ... } // Error: "Did you mean @SpendingValidator?"}
// CORRECT@MintingValidatorpublic class MyPolicy { @Entrypoint public static boolean validate(Data redeemer, ScriptContext ctx) { ... }}3.6 Library source must not contain @<Annotation>: <ClassName>
Section titled “3.6 Library source must not contain @<Annotation>: <ClassName>”Cause: A source file passed as a library (not the validator source) contains a validator annotation. Library files must not be validators.
Fix: Remove the validator annotation from library classes, or pass the file as the validator source instead.
// WRONG: library file with validator annotation@OnchainLibrary@MintingValidator // this is not allowed on a librarypublic class HelperLib { ... }
// CORRECT@OnchainLibrarypublic class HelperLib { ... }3.7 JULC0054 Method <name> is declared more than once in <class> with different on-chain behaviour
Section titled “3.7 JULC0054 Method <name> is declared more than once in <class> with different on-chain behaviour”Cause: On-chain methods are bound by name, so every call to an overloaded name would run one
of its declarations. Declarations that compile to identical code and types (such as the stdlib’s
integerToByteString(boolean, long, long) and (boolean, long, BigInteger)) are accepted; any
other overload is rejected. A helper may not reuse an entrypoint’s name.
Fix: Give each method a distinct name.
3.8 JULC0057 Static field '<field>' calls method <name>
Section titled “3.8 JULC0057 Static field '<field>' calls method <name>”Cause: Static field initializers are evaluated outside the class’s methods, so they cannot call them.
Fix: Call the method where the value is needed, or initialise the field with an expression that does not call a method of the same class.
4. Type Resolution Errors
Section titled “4. Type Resolution Errors”These errors are raised by the TypeResolver and TypeRegistrar during type
registration and resolution.
4.1 Unknown type: <name>
Section titled “4.1 Unknown type: <name>”Cause: A type name was used that the compiler does not recognize. It is not a primitive type, a known ledger type, a registered record, or a sealed interface.
Fix: Define the type as a record or sealed interface, or ensure it is imported.
// WRONG: MyData is not definedpublic static boolean validate(MyData data, ScriptContext ctx) { ... }
// CORRECT: define it as a recordpublic record MyData(BigInteger amount, byte[] hash) {}4.2 Unsupported primitive type: <type>
Section titled “4.2 Unsupported primitive type: <type>”Cause: A primitive type other than boolean, int, or long was used
(e.g., char, short, byte outside of byte[]).
Fix: Use BigInteger for integers and boolean for booleans.
4.3 Arrays are not supported; use List. Got: <type>
Section titled “4.3 Arrays are not supported; use List. Got: <type>”Cause: An array type other than byte[] was used. Only byte[] is supported
(mapped to ByteString).
Fix: Use List<T> instead of arrays.
PV11 Note: From protocol version 11,
JulcArray<T>provides O(1) random-access arrays via CIP-138 builtins. Uselist.toArray()to convert a list.
4.4 Cannot resolve type: <type>
Section titled “4.4 Cannot resolve type: <type>”Cause: A type expression could not be resolved to any known PIR type.
Fix: Check the type name for typos and ensure it is defined or imported.
4.5 Duplicate record type '<name>' found across compilation units
Section titled “4.5 Duplicate record type '<name>' found across compilation units”Cause: Two or more compilation units define a record with the same name.
Fix: Rename one of the records to eliminate the conflict.
4.6 Duplicate sealed interface '<name>' found across compilation units
Section titled “4.6 Duplicate sealed interface '<name>' found across compilation units”Cause: Two or more compilation units define a sealed interface with the same name.
Fix: Rename one of the sealed interfaces.
4.7 Circular type dependency detected among: <types>
Section titled “4.7 Circular type dependency detected among: <types>”Cause: The type registrar found a circular dependency when topologically sorting types. Type A depends on type B, which depends on type A.
Fix: Break the circular dependency by restructuring your types. Use Data as
an opaque type to break cycles if needed.
4.8 JULC0055 Cannot resolve <member> on a value whose type is not known here
Section titled “4.8 JULC0055 Cannot resolve <member> on a value whose type is not known here”Cause: x.name() or x.name was used on a value whose record, ledger or library type the
compiler does not know, for example a PlutusData or an untyped var. Before ADR-060 such a call
was bound to whatever variable or helper method happened to be named name, which is not Java’s
meaning.
Fix: Give the receiver its record or ledger type, or call a static helper as helper(x).
// WRONG: PlutusData has no amount()static BigInteger amount(PlutusData d) { return Builtins.unIData(d); }return d.amount();
// CORRECTreturn amount(d);5. Runtime Errors
Section titled “5. Runtime Errors”These errors occur when evaluating compiled UPLC programs on the Plutus VM.
5.1 No JulcVmProvider found. Add julc-vm-scalus or julc-vm-java to your classpath.
Section titled “5.1 No JulcVmProvider found. Add julc-vm-scalus or julc-vm-java to your classpath.”Cause: The JulcVm.create() method could not find any VM provider via
ServiceLoader. No VM backend JAR is on the classpath.
Fix: Add a VM backend dependency to your project.
dependencies { implementation 'org.julclang:julc-vm-scalus:<version>'}5.2 EvalResult.Failure
Section titled “5.2 EvalResult.Failure”Cause: The UPLC program evaluated to an error term. This typically means the
validator returned false or hit a Builtins.error() call. The error field
contains a description of the failure.
Fix: Check the trace messages via result.traces() for debugging output.
Ensure your validator logic returns true for valid transactions. Add
Builtins.trace("message", ...) calls to narrow down the failure point.
5.3 EvalResult.BudgetExhausted
Section titled “5.3 EvalResult.BudgetExhausted”Cause: The UPLC program exceeded the allowed execution budget (CPU steps and/or memory units). This happens when the script is too expensive.
Fix:
- Simplify your validator logic
- Reduce the number of loop iterations
- Use
breakin for-each loops to exit early - Avoid deeply nested data structure traversals
- Check
result.budgetConsumed()to understand where the budget went - Consider splitting logic across multiple transactions
5.4 Script too large for on-chain submission
Section titled “5.4 Script too large for on-chain submission”Cause: The compiled UPLC program serialized to CBOR exceeds the protocol
parameter maxTxSize (currently ~16 KB for the script itself). This is not a
compiler error but a chain-level constraint.
Fix:
- Remove unused helper methods (they are included in the script)
- Use stdlib methods from
julc-stdlib(compiled once, shared via reference) - Simplify logic, reduce the number of record types and sealed interfaces
- Use
@Paramfields to move configuration data off-chain (applied at deployment)
6. Common Mistakes
Section titled “6. Common Mistakes”Q: Why does my validator always return false?
Section titled “Q: Why does my validator always return false?”A: The most common cause is a missing return true at the end of the
entrypoint method. In UPLC, if your code falls through without returning true,
the default result is unit/void, which is treated as failure.
@Entrypointpublic static boolean validate(Data redeemer, ScriptContext ctx) { // Do some checks... var txInfo = ContextsLib.getTxInfo(ctx); // WRONG: forgot to return true}
@Entrypointpublic static boolean validate(Data redeemer, ScriptContext ctx) { var txInfo = ContextsLib.getTxInfo(ctx); // checks... return true; // CORRECT: explicit success}Q: Why do I get “Undefined variable” for a variable I just declared?
Section titled “Q: Why do I get “Undefined variable” for a variable I just declared?”A: Variables are scoped to the block they are declared in. If you declare a
variable inside an if block, it is not visible outside.
if (condition) { var value = BigInteger.ONE;}return value; // ERROR: "Undefined variable: value"
// CORRECT: declare before the ifvar value = BigInteger.ZERO;if (condition) { // use value, but cannot reassign outside of a loop accumulator}return value;Q: Why can I not reassign a variable?
Section titled “Q: Why can I not reassign a variable?”A: On-chain variables are immutable. Reassignment is only supported inside for-each and while loops as the accumulator pattern. Outside of loops, create a new variable with a different name.
// WRONGvar x = BigInteger.ONE;x = BigInteger.TWO; // CompilerException: Unsupported expression: AssignExpr
// CORRECT: use a new variablevar x = BigInteger.ONE;var y = BigInteger.TWO;Q: Why does BigInteger.valueOf(42) not work?
Section titled “Q: Why does BigInteger.valueOf(42) not work?”A: BigInteger.valueOf(n) is actually supported by the compiler. It translates
the argument directly to a UPLC integer. If you see an error, check that the
argument is a literal, not a variable.
var x = BigInteger.valueOf(42); // WORKSvar y = BigInteger.valueOf(someVar); // WORKS (someVar must be in scope)var z = new BigInteger("12345"); // WORKS (string literal required)var w = new BigInteger(someString); // ERROR: requires a string literalQ: How do I use List.of(...) on-chain?
Section titled “Q: How do I use List.of(...) on-chain?”A: List.of() is not directly supported. Use Builtins.mkNilData() to create
an empty list and Builtins.mkCons() to prepend elements.
// Create an empty listvar empty = Builtins.mkNilData();
// Prepend an elementvar list = Builtins.mkCons(Builtins.iData(BigInteger.ONE), empty);Q: Why does my library class get “Unknown method” errors?
Section titled “Q: Why does my library class get “Unknown method” errors?”A: Library methods must be called with their class name qualifier.
// WRONG (inside a different class)var result = myHelper(arg);
// CORRECTvar result = MyLib.myHelper(arg);If the library is a separate file, ensure it is annotated with @OnchainLibrary
so it can be auto-discovered from the classpath via
META-INF/plutus-sources/.
Q: How do I debug a failing validator?
Section titled “Q: How do I debug a failing validator?”A: Use Builtins.trace() to emit messages during evaluation. Trace messages
are collected in EvalResult.traces().
@Entrypointpublic static boolean validate(Data redeemer, ScriptContext ctx) { var txInfo = ContextsLib.getTxInfo(ctx); Builtins.trace("got txInfo");
if (!checkSomething(txInfo)) { Builtins.trace("checkSomething failed"); return false; }
Builtins.trace("all checks passed"); return true;}Q: Why do I get “Circular type dependency detected”?
Section titled “Q: Why do I get “Circular type dependency detected”?”A: Records that reference each other cannot be topologically sorted. Break the
cycle by using Data as an intermediate opaque type.
// WRONG: circularpublic record A(B other) {}public record B(A other) {}
// CORRECT: break cycle with Datapublic record A(Data other) {} // other is opaque Data, cast as neededpublic record B(A parent) {}Q: Why does my switch expression fail with a compiler error about missing cases?
Section titled “Q: Why does my switch expression fail with a compiler error about missing cases?”A: The compiler enforces exhaustiveness for switch expressions on sealed interfaces. The default -> branch acts as a catch-all for any uncovered variants — its body is compiled and used for all variants not explicitly listed. However, for clarity and safety, prefer explicit cases for ALL variants rather than relying on default.
// OK but not recommended: default catches unlisted variantsvar result = switch (action) { case Bid b -> b.amount(); default -> BigInteger.ZERO; // compiled — covers Cancel and any other variants};
// RECOMMENDED: explicit cases for all variantsvar result = switch (action) { case Bid b -> b.amount(); case Cancel c -> BigInteger.ZERO;};If you omit both a default branch and an explicit case, the compiler emits an error listing the missing variants.
Q: Why does pk.hash().hash() crash with a DeserializationError?
Section titled “Q: Why does pk.hash().hash() crash with a DeserializationError?”A: Types like PubKeyHash, TxId, ScriptHash, and DatumHash map to ByteStringType. The field access pk.hash() already generates UnBData(HeadList(...)) to extract the raw ByteString. Calling .hash() again applies a second UnBData on the already-unwrapped ByteString, causing UnBData(ByteString) to fail.
// WRONG: double unwrapbyte[] h = pk.hash().hash();
// CORRECT: PlutusData.cast() (recommended)byte[] h = PlutusData.cast(pk.hash(), byte[].class);
// CORRECT: double-cast (also works)byte[] h2 = (byte[])(Object) pk.hash();
// CORRECT for TxId:byte[] txHash = PlutusData.cast(ref.txId(), byte[].class);Q: Why can’t I use switch on a Tuple2 or Tuple3?
Section titled “Q: Why can’t I use switch on a Tuple2 or Tuple3?”A: Tuple2 and Tuple3 are registered as RecordType, but switch requires SumType (sealed interface). Use field access instead:
// WRONG: Tuple2 is not switchablevar result = switch (tuple) { ... };
// CORRECT: use field accessBigInteger first = tuple.first();BigInteger second = tuple.second();Q: Why can’t I access typed elements from a list.map(...) result?
Section titled “Q: Why can’t I access typed elements from a list.map(...) result?”A: The map HOF wraps each lambda result to Data, so the returned list is
JulcList<PlutusData> regardless of input element type. Use Builtins.unIData()
or Builtins.unBData() to extract the typed value:
var mapped = amounts.map(x -> x.multiply(BigInteger.TWO));// WRONG: mapped.head() returns PlutusData, not BigInteger// CORRECT:BigInteger first = Builtins.unIData(mapped.head());Q: Can I call foldl as an instance method on a list?
Section titled “Q: Can I call foldl as an instance method on a list?”A: No. foldl is only available as a static call: ListsLib.foldl(f, init, list).
It is not registered as an instance method because it takes two lambda parameters
plus an initial value, which makes instance-call syntax ambiguous.
Q: Why does Value.assetOf(policyId, tokenName) fail?
Section titled “Q: Why does Value.assetOf(policyId, tokenName) fail?”A: Value.assetOf() uses EqualsData internally. If policyId/tokenName are byte[] (ByteStringType), you must wrap with Builtins.bData() before passing. Otherwise EqualsData(BData(...), ByteString(...)) fails.
// WRONG: passing raw byte[] to assetOfbyte[] policy = ...;byte[] token = ...;BigInteger amount = value.assetOf(policy, token); // DeserializationError
// CORRECT: wrap with bDataBigInteger amount = value.assetOf(Builtins.bData(policy), Builtins.bData(token));Q: Why does my cross-library call with BytesData variables fail?
Section titled “Q: Why does my cross-library call with BytesData variables fail?”A: When calling a stdlib library method that takes BytesData/MapData typed parameters, if the caller also has a variable of the same type, the compiler sees matching types and skips conversion. But compiled libraries expect raw Data args at the UPLC boundary.
// WRONG: BytesData variable matches library parameter typePlutusData.BytesData myPolicy = ...;long amount = ValuesLib.assetOf(value, myPolicy, tokenName); // type mismatch at UPLC level
// CORRECT: use PlutusData typed variablesPlutusData myPolicy = ...;long amount = ValuesLib.assetOf(value, myPolicy, tokenName);Q: Why does @Param PlutusData.BytesData cause issues?
Section titled “Q: Why does @Param PlutusData.BytesData cause issues?”A: @Param values are ALWAYS raw Data at runtime, regardless of the declared type. Using PlutusData.BytesData tells the compiler the value is already a ByteString, causing double-wrapping and incorrect cross-library calls.
// WRONG@Param PlutusData.BytesData myPolicyId;
// CORRECT — always use PlutusData for @Param@Param PlutusData myPolicyId;Q: What annotations are available and when should I use each?
Section titled “Q: What annotations are available and when should I use each?”| Annotation | Purpose | Entrypoint params |
|---|---|---|
@SpendingValidator |
Validates spending from a script address | 2 (redeemer, ctx) or 3 (datum, redeemer, ctx) |
@MintingValidator |
Validates minting/burning tokens | 2 (redeemer, ctx) |
@WithdrawValidator |
Validates reward withdrawals | 2 (redeemer, ctx) |
@CertifyingValidator |
Validates certificate actions | 2 (redeemer, ctx) |
@VotingValidator |
Validates governance votes | 2 (redeemer, ctx) |
@ProposingValidator |
Validates governance proposals | 2 (redeemer, ctx) |
@MultiValidator |
Multi-purpose validator (multiple script purposes) | Multiple @Entrypoint methods with Purpose, or single DEFAULT |
@Entrypoint |
Marks the main validator method | - |
@Entrypoint(purpose = Purpose.MINT) |
Purpose-specific entrypoint in @MultiValidator |
- |
@Param |
Marks a parameterized field (applied at deployment) | - |
@OnchainLibrary |
Marks a reusable library class (auto-discovered) | - |
Q: Duplicate purpose in @MultiValidator
Section titled “Q: Duplicate purpose in @MultiValidator”A: Two @Entrypoint methods in a @MultiValidator class have the same Purpose value. Each purpose can only have one handler.
// WRONG: duplicate MINT@MultiValidatorpublic class BadValidator { @Entrypoint(purpose = Purpose.MINT) static boolean mint1(PlutusData redeemer, ScriptContext ctx) { return true; }
@Entrypoint(purpose = Purpose.MINT) // ERROR: duplicate purpose MINT static boolean mint2(PlutusData redeemer, ScriptContext ctx) { return true; }}
// CORRECT: one handler per purpose@MultiValidatorpublic class GoodValidator { @Entrypoint(purpose = Purpose.MINT) static boolean mint(PlutusData redeemer, ScriptContext ctx) { return true; }
@Entrypoint(purpose = Purpose.SPEND) static boolean spend(PlutusData redeemer, ScriptContext ctx) { return true; }}Q: Cannot mix DEFAULT and explicit purposes in @MultiValidator
Section titled “Q: Cannot mix DEFAULT and explicit purposes in @MultiValidator”A: You are combining Purpose.DEFAULT (manual dispatch) with explicit purposes like Purpose.MINT (auto-dispatch). Choose one mode.
// WRONG: mixing DEFAULT and MINT@MultiValidatorpublic class BadValidator { @Entrypoint // DEFAULT static boolean validate(PlutusData redeemer, ScriptContext ctx) { return true; }
@Entrypoint(purpose = Purpose.MINT) // ERROR: cannot mix static boolean mint(PlutusData redeemer, ScriptContext ctx) { return true; }}
// CORRECT: all explicit@MultiValidatorpublic class GoodValidator { @Entrypoint(purpose = Purpose.MINT) static boolean mint(PlutusData redeemer, ScriptContext ctx) { return true; }
@Entrypoint(purpose = Purpose.SPEND) static boolean spend(PlutusData redeemer, ScriptContext ctx) { return true; }}Q: Multiple validator annotations on the same class
Section titled “Q: Multiple validator annotations on the same class”A: A class cannot have both @MultiValidator and a single-purpose annotation (like @SpendingValidator). Use one or the other.
// WRONG@SpendingValidator@MultiValidatorpublic class BadValidator { ... }
// CORRECT: use @MultiValidator for multi-purpose@MultiValidatorpublic class GoodValidator { @Entrypoint(purpose = Purpose.SPEND) static boolean spend(PlutusData redeemer, ScriptContext ctx) { return true; }
@Entrypoint(purpose = Purpose.MINT) static boolean mint(PlutusData redeemer, ScriptContext ctx) { return true; }}Q: Invalid parameter count for multi-validator entrypoint
Section titled “Q: Invalid parameter count for multi-validator entrypoint”A: In a @MultiValidator, SPEND entrypoints accept 2 or 3 parameters. All other purposes accept exactly 2 parameters.
// WRONG: MINT with 3 params@Entrypoint(purpose = Purpose.MINT)static boolean mint(PlutusData datum, PlutusData redeemer, ScriptContext ctx) { ... }// ERROR: MINT entrypoint must have 2 parameters
// CORRECT: MINT with 2 params@Entrypoint(purpose = Purpose.MINT)static boolean mint(PlutusData redeemer, ScriptContext ctx) { ... }
// CORRECT: SPEND with 3 params (datum)@Entrypoint(purpose = Purpose.SPEND)static boolean spend(Optional<PlutusData> datum, PlutusData redeemer, ScriptContext ctx) { ... }