The code generation tooling complements the runtime adapter by turning supported guest-language contracts into Java source files.
It is build-time tooling, not runtime execution:
- it does not create GraalVM contexts
- it does not execute guest-language code
- it does not depend on the adapter runtime implementation
Note Code generation is optional. The runtime adapter can be used with handwritten Java interfaces or with generated ones.
This module contains the generation pipeline:
ContractGeneratorandDefaultContractGenerator- parser dispatch by
SupportedLanguage - Python contract parsing
- Java type rendering
- Java interface generation
- a CLI entry point
This module integrates the generator with Maven. It:
- scans an input directory
- filters supported script files by extension
- invokes the generator
- writes Java output
- registers the generated sources directory with Maven
The implemented pipeline is:
- walk the configured input directory
- detect the language from the file extension
- create a
ScriptDescriptor - parse the source into a
ContractModel - render one Java interface per discovered contract
- write the result under the configured Java package
The generator is deterministic:
- imports are collected and sorted
- interface source is rendered from the shared model
- generated files include a checksum header based on the rendered interface body
Language parsers convert source syntax into the adapter’s language-neutral contract model.
PythonContractParser is the only fully implemented parser today. It:
- looks for
polyglot.export_value(...) - detects whether the export points to a class or a dictionary-style function map
- extracts method signatures
- respects
CodegenConfig.onlyIncludedMethods() - skips private methods by default
- maps Python type hints into
PolyType
The Python parser is intentionally lightweight and convention-oriented. It is designed for adapter contracts, not for full Python semantic analysis.
The parser matches class definitions with or without base classes:
class ForecastService: # no base class
class ForecastService(Protocol): # single base class
class ForecastService(BaseA, BaseB): # multiple base classes
class ForecastService (Base): # space before parenthesesOnly the class name is captured; base class names are ignored by the parser.
A single Python file may contain multiple polyglot.export_value(...) declarations. Each
declaration produces one ContractClass in the resulting ContractModel, and the Maven plugin
generates one Java interface file per class:
polyglot.export_value("OrderService", OrderService)
polyglot.export_value("InventoryService", InventoryService)Contracts are returned in source order (the order polyglot.export_value(...) lines appear in
the file).
If two or more polyglot.export_value(...) declarations in the same file use the same API name,
parse(...) fails immediately with an IllegalStateException that includes the duplicate name.
This check runs in all modes, not only in strictMode.
# This will fail at parse time:
polyglot.export_value("Api", ClassA)
polyglot.export_value("Api", ClassB) # duplicate name → IllegalStateExceptionSupported subset today:
polyglot.export_value(...)with a class export or dictionary-style export- multiple
polyglot.export_value(...)declarations per file - class definitions with optional base classes
- top-level Python functions used in exported dictionaries
- class methods and basic type hints aligned with the repository type model
Not promised today:
- full Python parsing fidelity
- semantic import resolution
- runtime-equivalent understanding of arbitrary Python constructs
- JavaScript contract generation
JsContractParser exists as an extension point only. It currently throws UnsupportedOperationException, so JavaScript code generation is not supported in the current version.
The type model used by code generation is intentionally small:
PolyPrimitivePolyListPolyMapPolyObjectPolyUnionPolyUnknown
Current Java rendering behavior is conservative:
- primitives map to boxed Java types
- lists map to
List<T> - maps map to
Map<K, V> - unsupported or richer types fall back to
Object
The Maven plugin goals are:
polyglot:generate
polyglot:check
polyglot:doctor
polyglot:validate-contracts
Generation defaults:
- phase:
generate-sources - input directory:
${project.basedir}/src/main/resources - output directory:
${project.build.directory}/generated-sources/polyglot - base package fallback:
${project.groupId}.polyglot
The plugin adds the output directory as a compile source root, so generated interfaces are compiled automatically in the same build.
Additional Maven plugin options:
onlyIncludedMethods(default:false)
When enabled, only methods marked with@adapter_includeare generated.strictMode(default:false)
Fails generation if unresolved/unknown types are detected in parsed contracts. This prevents silently propagatingObject-like contracts into generated APIs.failOnNoContracts(default:false)
Fails the build if scanning finishes with zero generated contracts.skipUnchanged(default:true)
Skips rewriting generated files when rendered content is unchanged.failOnContractDrift(default:false)
Inpolyglot:generate, switches to drift-check behavior and fails when generated files differ.
DX workflow pattern:
- local update:
./mvnw polyglot:generate - CI verification:
./mvnw polyglot:check - local diagnostics alias:
./mvnw polyglot:doctor(same behavior ascheck)
Recommended policy:
- use
strictMode=truefor contracts that are intended to become stable Java-facing APIs - use
polyglot:checkin CI to detect drift without rewriting files - treat
polyglot:doctoras a local diagnostic convenience, not as a separate behavior contract
For parser limitations, drift failures, and common check/doctor symptoms, see
troubleshooting.md.
polyglot:validate-contracts checks explicitly selected Java interface sources against
Python sources without creating a GraalVM context, importing modules, or executing Python.
It complements check/doctor, which compare generated text rather than existing Java bindings.
The goal is disabled by default, including when configured as an execution.
<plugin>
<groupId>io.github.ih0r-d</groupId>
<artifactId>polyglot-codegen-maven-plugin</artifactId>
<version>0.5.0-SNAPSHOT</version>
<configuration>
<inputDirectory>${project.basedir}/src/main/resources/python</inputDirectory>
<contracts>
<contract>${project.basedir}/src/main/java/example/GreetingApi.java</contract>
</contracts>
</configuration>
<executions>
<execution>
<id>validate-python-contracts</id>
<goals><goal>validate-contracts</goal></goals>
</execution>
</executions>
</plugin>Enable validation for this build:
./mvnw -Dpolyglot.codegen.validateContracts=true verifyAlternatively, set <validateContracts>true</validateContracts> in an opt-in Maven profile.
The goal's default phase is verify; it writes no files and registers no source roots.
contracts must contain at least one existing Java source when enabled, unless annotation
discovery below is enabled. Generated interfaces
can be selected by their generated-source paths after the generate execution has run.
For interface GreetingApi { String hello(); }, the expected source is
inputDirectory/greeting_api.py, following the runtime's naming convention:
import polyglot
class GreetingApi:
def hello(self):
return "hello"
polyglot.export_value("GreetingApi", GreetingApi)Success reports Python contract validation passed: 1 Java source(s). Removing hello
fails Maven with a diagnostic such as:
GreetingApi.java:1: module 'greeting_api', symbol 'GreetingApi.hello': Missing Python method in .../greeting_api.py
A missing module reports the expected Python file and Java contract. Unsupported Python constructs report a Python file/line and fail, rather than silently validating an uncertain binding.
This first version supports one top-level Java interface per selected file without inheritance,
and the DEFAULT/BY_INTERFACE_EXPORT class-export convention. An optional @PolyglotClient
annotation must explicitly select SupportedLanguage.PYTHON; auto language selection and
BY_METHOD_NAME are not inferred. Selection in contracts is sufficient for plain interfaces;
Spring scanning is not involved.
Python validation reuses PythonContractParser behind a conservative static-subset check:
plain local classes, non-underscore-prefixed methods with single-line signatures, and literal top-level
polyglot.export_value("Name", ClassName) calls after the class declaration. Only
import polyglot is accepted at module level. Inheritance, decorators, special methods
(including __init__), dictionary/factory exports, conditional declarations, rebinding,
nested declarations, tabs, multiline strings and escaped line continuations are unsupported.
Method bodies are inspected as source only and never executed.
For each Java method, validation also checks its declared parameter count against the Python
instance method's accepted positional range: required parameters through total parameters,
excluding the implicitly bound first receiver (self, or another name). Python defaults make
parameters optional. Every Java overload is checked independently. Java arrays and Java varargs
each count as one declared parameter; the runtime does not flatten that array into guest arguments.
For example, def hello(self, name, count=2): accepts one or two Java arguments. A zero-argument
Java declaration fails with Java and Python source locations:
GreetingApi.java:2: module 'greeting_api', symbol 'GreetingApi.hello': Java supplies 0 positional argument(s); Python accepts 1..2 (receiver excluded), declared at .../greeting_api.py:2
Single-line positional signatures support annotations, trailing commas, and defaults containing
quoted strings or balanced containers. Default expressions are inspected only to establish
whether a parameter is optional; their values are never evaluated. *args, **kwargs, keyword-only
parameters (*), positional-only markers (/), lambda defaults, call expressions and formatted string expressions
in signatures, and parenthesized return annotations are unsupported and fail validation. Missing receivers, duplicate parameters,
unbalanced signatures and required parameters after defaults also fail conservatively.
This checks declared symbol existence and positional arity, not Python syntax validity, import
availability, argument/return types, default-expression success, constructor success, or full runtime
equivalence. It is not full Python type safety. Dynamic contracts should retain runtime validateBinding(...) or Spring fail-fast
validation. Existing generation, drift checks, runtime and Spring defaults are unchanged.
Existing @PolyglotClient declarations can replace the manual <contracts> list. For example,
keep the application's existing contract:
import io.github.ih0rd.polyglot.SupportedLanguage;
import io.github.ih0rd.polyglot.annotations.PolyglotClient;
@PolyglotClient(languages = SupportedLanguage.PYTHON)
public interface GreetingApi {
String hello();
}Configure the same plugin execution as above, but omit <contracts>. Then enable both flags:
./mvnw -Dpolyglot.codegen.validateContracts=true -Dpolyglot.codegen.discoverContracts=true verifyAlternatively, put <validateContracts>true</validateContracts> and
<discoverContracts>true</discoverContracts> in the plugin configuration of an opt-in Maven
profile. Before discovery, adding a client required another <contract> entry; with discovery,
adding its existing Java annotation is sufficient.
Discovery scans .java files recursively under sourceDirectory (default:
${project.build.sourceDirectory}, normally src/main/java) in sorted order. inputDirectory
continues to select the Python root. A nonempty explicit <contracts> list takes precedence,
even with discovery enabled, and retains its existing validation behavior. Discovery itself and
validation are both disabled by default; enabling discovery alone does not validate anything.
An enabled discovery run with no annotated contracts fails, so an incorrect source root cannot
silently pass as a successful validation.
The plugin identifies the exact framework-neutral io.github.ih0rd.polyglot.annotations.PolyglotClient
annotation through an explicit Java import or its fully qualified name. Unannotated interfaces
and explicitly imported foreign annotations with the same simple name are ignored. Wildcard-only
or locally shadowed annotation resolution fails as unsupported; use an explicit import or fully
qualified annotation instead. Java parsing does not require compiled application classes.
Discovered contracts go through the same module, export, method and positional-arity checks as
explicit contracts. They must explicitly select SupportedLanguage.PYTHON and use
DEFAULT/BY_INTERFACE_EXPORT. AUTO/multiple-language selection, JS, inherited/nested interfaces
and other unsupported forms fail rather than being counted as successfully validated.
This is source discovery, not Spring bean discovery: no ApplicationContext, GraalVM Context,
application class loading or Python execution is involved. It does not interpret @EnablePolyglotClients,
Spring profiles, conditions, properties, or dependency JARs. Only the configured source root is
scanned; tests and additional generated-source roots are not implicitly included. Set
sourceDirectory to a narrower source directory when needed. All Python subset restrictions above
continue to apply.
The maintained Spring sample already uses explicit Python @PolyglotClient annotations, but its
Python implementations use imports, constructors and docstrings outside the current static subset.
This feature does not enable validation in that sample or change its runtime behavior. Spring
startup fail-fast remains the runtime check for such contracts; discovery does not widen the
Python parser's capabilities.
The packaged-plugin integration fixtures use the Apache Maven Invoker Plugin only in the
plugin-it profile, with an isolated repository and cloned fixtures:
./mvnw -B -ntp -Pplugin-it -pl build-tools/polyglot-codegen-maven-plugin -am verifyThe code generation layer and the runtime adapter are separate by design, but they are intended to be used together.
Generated interfaces can be bound at runtime through:
PyExecutorJsExecutor- the Spring Boot starter
That is why the build tools follow the same naming conventions and contract model expected by the runtime adapter.
Generated Java interfaces are plain interfaces with no framework annotations. To use a generated
interface with the Spring Boot starter, you must annotate it with @PolyglotClient and enable
client scanning:
@PolyglotClient(languages = {SupportedLanguage.PYTHON})
public interface ForecastService {
// generated methods
}@EnablePolyglotClients
@SpringBootApplication
public class MyApplication { ... }The code generator does not add @PolyglotClient automatically. That annotation must be present
on the interface (either hand-written or added after generation) before the Spring starter can
register the corresponding bean.