Skip to content

Java Library Adapter ​

Goal ​

A Norm Module can implement its public Norm API using a Java JAR and its runtime dependencies, then replace that implementation with pure Norm in a later version. Module names, exports, dependency declarations, and publication coordinates do not expose implementation provenance.

text
Norm API → optional JAR binding → Java dependency graph
Norm API → Norm Core

Both paths produce the same kind of Module artifact. Consumers depend only on the Module.

File identity ​

Every .norm file is Norm source. A top-level Module module() supplies the module declaration. A separate module.norm is the conventional multi-file layout; single-file applications can place it beside business declarations. Module declarations and auxiliary entry points use the same parsing, type checking, Core lowering, and execution pipeline. A JAR declaration is simply an ordinary Norm object returned by that function. Generated adapters also use only public Norm syntax, types, and functions. The compiler carries a set of generated origins that source cannot forge, and exposes frozen Binding intrinsics only to those documents. Content distinctions define language and module semantics, not host authorization.

Module boundaries ​

Module module() is the single declaration point for module identity, dependencies, and publication configuration. A working directory is not a dependency or publication unit, and there is no Project manifest.

A Module has at most one optional jarBinding, containing one root JAR. Its POM or local declaration can introduce transitive runtime dependencies, but the compiler generates callable declarations only for public classes physically owned by the root JAR. Objects from dependency JARs may cross signatures as constrained external types; calling their APIs requires the corresponding Norm Modules.

Explicit exports and jarBinding.api establish public Norm name mappings in declaration order. Java class names therefore do not determine Module API identity: jakarta.persistence.EntityManager, for example, can be exported stably as orm.Store. If exports is omitted, names are derived from jarType.name.

To combine several Java libraries, adapt each root JAR in its own Module and compose them through a pure Norm Module. Ordinary Norm source, generated declarations, and later pure-Norm replacements share one export table.

Declaration model ​

Java annotation adaptation projects the field-initialization responsibility of jakarta.inject.Inject onto the standard-library ManagedField contract. The frontend determines constructor parameters from resolved annotation contracts, not third-party short names. The DI container still injects existing objects.

Java annotations directly marked with io.micronaut.aop.Introduction project onto ManagedImplementation, retaining their original targets and retention. The contract identifies an external implementation provider. Recognizing it and linking method implementations are distinct stages; unlinked methods must not become empty implementations. See JavaAnnotationContract.

norm
Module module() {
  return module(
    name: "commons.lang",
    version: 1,
    dependencies: [],
    binding: jarBinding(
      target: mavenJar(
        group: "org.apache.commons",
        artifact: "commons-lang3",
        version: "3.20.0",
        resolution: sha256("...")
      ),
      api: [
        jarType(
          name: "StringUtils",
          members: ["isBlank", "isNotBlank", "reverse", "split", "trim"]
        )
      ]
    )
  )
}

JarType, JarBinding, and their factory functions are ordinary Norm declarations defined during bootstrap. Both binding and target are single values. Binding Module exports derive from the type names in api; a pure Norm version implements those same export names in ordinary source.

Local JARs use localJar(path, integrity). norm resolve resolves dependencies and atomically fills missing digests. A declared digest mismatch fails immediately; authors updating dependencies must first change their declarations. norm run, norm package, and CI verify declared content without accepting dependency drift. No separate lock file is used.

Initial usage ​

Place local JARs inside the Module directory, such as lib/tools.jar. A jarType name identifies one unique public class in the root JAR. members selects constructor, method, or field names and includes stable public overloads of each name; constructors use new. Root-JAR types appearing in signatures automatically form the minimal declaration closure. The compiler generates ordinary Norm declarations for selected APIs: for example, StringUtils.reverse becomes stringUtilsReverse.

norm
Module module() {
  return module(
    name: "example.tools",
    version: 1,
    binding: jarBinding(
      target: localJar(path: "lib/tools.jar"),
      api: [jarType(name: "Tools", members: ["new", "convert"])]
    )
  )
}

Initial resolution writes the digest back into the declaration:

text
norm resolve path/to/example/tools
norm run path/to/example/tools/Main.norm

Maven root artifacts use the declaration model above. Another Module declares an ordinary Norm dependency and imports generated functions:

norm
import commons.lang.stringUtilsReverse

Void main() {
  printLine(stringUtilsReverse("Norm") ?? "")
}

Before publication, run norm resolve, then generate artifacts in a directory that can serve directly as a Maven repository:

text
norm package path/to/commons/lang --output path/to/repository

Repository coordinates and artifact names derive from Module identity; see the package manager. Maven and Gradle can consume the generated NAR and POM. Another Norm project's dependency(repository, name, version?) resolves those same coordinates without a POM, Gradle file, or lock file.

See the Apache Commons Lang example for a runnable directory.

Content identity ​

Paths and Maven coordinates locate content. The implementation derives these identities:

  • JarContentId: complete JAR bytes.
  • JavaApiId: normalized bindable public API.
  • ResolvedJarGraphId: artifact content and dependency edges.
  • BindingArtifactId: dependency graph, mapping policy, and Binding ABI.
  • ModuleApiId: public Norm declarations.
  • ModuleImplementationId: Norm Core and optional Binding implementation.

Identical content shares scanning and Binding caches. A changed JAR implementation requires relinking; consumer source remains valid when the public Norm API is unchanged.

Publication model ​

norm package produces a NAR and a POM derived from module.norm. ModuleArchiveFormat owns the archive version. Every Module stores its evaluated manifest, complete production sources, and resources; exports defines public APIs rather than selecting artifact files. Java Binding Modules also retain an API report and the stable binding artifact defined by PublishedJarBinding. Consumers verify binding ABI, artifact digests, module descriptors, pinned dependency graphs, public type ownership, and archived sources, then directly link published artifacts. Application-specific callbacks, annotation processing, and reachability pruning remain application-build responsibilities. NARs neither embed Java JARs nor execute remote module.norm source. Pure Norm and Java adapters share one package model. See ModulePackagerTest and CrossModuleJarBindingTest for archive and cross-module acceptance.

The binding ABI covers data structures, serialization, and runtime conventions. Changes to those contracts require updating PublishedJarBinding.ABI and republishing adapters; unrelated compiler implementation changes do not.

All published modules also carry the Core artifact defined by CompiledModule. The manifest validates its digest and ABI. The compiler architecture defines import rules and compilation-work acceptance. Changes to the payload's data structures or serialization require updating CompiledModule.ABI; Core and language-semantics versions follow their existing identity contracts.

POMs declare root Java artifacts and ordinary Maven dependencies. Resolving a Norm Module also obtains the required Java graph. Publishing a local JAR requires resolvable publication coordinates; the same publication produces a Java artifact and a dependent Norm artifact.

A pure-Norm implementation never replaces an existing Binding artifact in place. Implementation migration publishes a new version of the same Module.

Mandatory constraints ​

  • Modules have no Java-specific kind.
  • Each Module binds at most one root JAR.
  • Do not generate public callable APIs for transitive dependencies.
  • Do not expose arbitrary host-class lookup, reflective calls, or untyped host objects.
  • Java objects appear in Norm as opaque references with definite declaration identity.
  • Explicit bindings of different versions of the same Java artifact across Modules must fail. The shared classpath resolver selects transitive versions, preferring explicit roots and otherwise using Maven version ordering; only selected versions' dependency closures remain.
  • Different content at fixed Java coordinates, or an API fingerprint mismatch, must fail.
  • Unsupported signatures on the selected public surface produce deterministic diagnostics.
  • Remote artifacts carry compiled module descriptors; consumers do not execute publisher configuration source.
  • Maven POMs, digest manifests, and generated declarations are derived artifacts. Gradle consumes the same Maven metadata directly.

Current binding surface ​

JavaModulePath owns Java module identification and selection of root-JAR JPMS dependencies. JVM execution, prepared artifacts, and Native share module-root identity. JVM module resources and class loading use the same application execution domain; the application loader closes resource streams on exit. Entry points are JvmJarBindingRuntime, PreparedApplication, and NativeApplicationExecutable. JavaModuleLoadingTest verifies module selection, identity, resource access, and file release. ApplicationClassLoaderIsolationTest verifies isolation of compiler dependencies, service resources, and the JDK platform boundary. Native compatibility still requires actual application acceptance.

Current support includes static and instance methods, constructors, static and instance fields, primitive and boxed scalars, strings, Number, opaque objects, Object-bounded generics, and generic inheritance projections within a JAR. Java arrays with concrete component types become generated identity wrappers supporting fixed length, reads, in-place updates, and construction. Primitive and boxed arrays retain distinct nominal types rather than mapping to value-semantic Norm Array<T>. Java T[] and T... use reified arrays distinguished by erased component type; a varargs call takes one array argument.

Java Throwable, Exception, and RuntimeException map to catchable Norm Exception values that can be passed back to Java. Throwables from binding calls enter Norm throw/catch. Exported AutoCloseable or java.io.Closeable types implement std.io.Resource; the execution resource domain handles explicit closure and exit cleanup. Java Object maps to Any?, Path/File to std.filesystem.Path, URI/URL to std.http.Uri, and CharSequence/Charset to Norm strings. java.io.InputStream and java.io.OutputStream map to std.io.InputStream and std.io.OutputStream, implementing standard byte and resource protocols. One type table drives these platform mappings. Public root-JAR types referenced by entry signatures join the generated closure. Explicitly exposed nested Java types receive stable top-level names from their complete enclosing type chain: Request.Builder, for example, becomes RequestBuilder.

Public interfaces in the root JAR become ordinary Norm interfaces, retaining projectable generic inheritance. Generated concrete classes implement their corresponding interfaces. Interface methods are ordinary Norm methods; private binding carriers preserve JVM identity for objects returned through interfaces. The shared type relation drives this mapping, while user source uses only Norm interfaces, classes, and method calls.

Public interfaces inherited through package-private Java parents are restored in generated declarations, substituting generic arguments along the full hierarchy. Java unbounded wildcards project to the Norm existential type ?, allowing Iterable<String> to pass safely to Iterable<?> parameters.

Member selection considers the complete public inherited surface, substituting parent type variables in the exported class. Calls retain publicly linkable declaration owners; package-private declarations are linked through the exported class. The census records real declarations without duplicating inherited views. Public dependency-JAR types participate in inheritance and SAM identification, but published adapter surfaces can still select only root-JAR types.

Java Class<T> maps to Norm Class<T>?. The generator derives JVM descriptors for public wrapper declarations and array wrappers. Runtime resolution uses declaration identity to map real java.lang.Class values in both directions; when a return value has multiple valid erased views, the call site's Class<T> disambiguates them. Ordinary Norm types without Binding mappings cannot be resolved through string class names or host reflection.

Java java.time.Duration and std.time.Duration convert in both directions through seconds and nanoseconds. The standard-library ABI generates Duration's field layout, rather than individual adapters copying it. Dependent Java generic bounds such as <U extends T> remain ordinary Norm generic constraints.

Exact Class<T> arguments enter bindings only when the Norm mapping preserves JVM class identity. Platform facades and Optional mappings that collapse host identity do not pretend to be a different class token. Raw Class, Class<?>, and class tokens expressed through type parameters continue to use runtime declaration identity.

Root-JAR enums become closed Norm enum declarations whose public constants are payload-free variants. Java static methods become ordinary functions; instance methods become functions with the enum value as their first argument. Parameters, results, and enum-array elements convert through declaration and constant identities in both directions. For Java identifiers outside Norm's identifier set, the generator preserves stable reversible variant mappings.

Java Optional<T> uses Norm nullability for absence, while OptionalInt, OptionalLong, and OptionalDouble use corresponding nullable scalars. Java Collection<T>, List<T>, Set<T>, and Map<K,V> map to reference classes in std.collections to preserve shared identity. List and Set share MutableCollection<T>; platform carriers use IterableView<T> and IteratorView<T>. Root-JAR types implementing Java Iterable<T> implement ordinary Norm std.core.Iterable<T> through their generic ancestor. Their iterator() returns std.core.Iterator<T> and works directly with for. These types remain distinct from value-semantic collections. In-place mutation on either side and host identity across repeated transfers are observable.

Java standard functional interfaces and public root-JAR SAM interfaces map to native Norm Function<R(P...)>. Projection resolves standard ? super inputs and ? extends outputs; root-JAR SAM generic arguments are substituted at their use sites. Runtime adaptation creates real Java interface instances from Norm lambdas, captured closures, and function references. Host callbacks enter Norm on their calling thread, preserving thread-local contexts such as transactions during nested host calls. Norm releases exclusive execution ownership while host code runs and reacquires it on return. Nested host calls blocking for callbacks from another thread follow the same boundary. Synchronous, asynchronous, and internally awaited Java callbacks share argument, result, and exception propagation rules. See GuestCallbackScheduler.

Java Future<T>, CompletionStage<T>, and CompletableFuture<T> map to std.concurrent.Task<T>. await() preserves the element type and sends Java failures through Norm throw/catch; cancel() and completed() expose defined state operations. Task implements Resource: explicit closure and execution-domain exit cancel unfinished work. Passing a Task back to Java restores its original host object. java.lang.Void maps to nullable std.core.Unit. Reactive Streams Publisher<T> maps to std.concurrent.Publisher<T>; subscription callbacks, completion, failure, cancellation, and execution-domain cleanup use the same scheduling and resource boundaries.

Each package's binding/java-api.json is the complete machine-readable declaration/adaptation census. jar.api in module.json is the machine-readable contract for the published public surface. Publication requires generating that entire selected surface and passing behavioral tests.

Java annotations become ordinary typed Norm annotations. At JVM application boundaries, annotations on Norm applications become real Java annotations. Modules needing compile-time processing declare official JSR 269 processors as ordinary dependencies; application builds generate isolated Java inputs and run the processors automatically. Generated application types retain Norm generic inheritance and provide managed instance allocation through JVM application facades. Framework-created entities or components associate with the same Norm objects. Processing includes the entry Module and pure Norm dependencies containing framework-support source, but excludes generated Binding declarations. Norm exceptions and enums retain their language semantics across Java proxies such as DI and transaction boundaries. See the Micronaut BBS for real-framework acceptance.

Module resources can supply annotation-processor compilation inputs. ApplicationCompiler prepares resources; AnnotationProcessorResourcesTest verifies template updates and deletion.

When every implicit-construction input has a default, the Java application facade offers a preferred no-argument constructor that runs Norm initialization. The full-argument entry remains available. JavaAnnotationBindingIntegrationTest verifies construction and private state across languages.

Core conformance witnesses connect interface default implementations to Java default methods, letting multiple implementing classes share one entry point. Method bodies still execute through the Norm runtime. Standard-interface parents without a Java representation do not generate extends Object. The same cross-language tests verify default-method inheritance and receiver dispatch.

Java application facades map zero-, one-, and two-argument functions to JDK functional interfaces according to arity and Void returns. Generic arguments recursively follow the same type projection. JavaFunctionShape unifies declarations and runtime adaptation. Cross-language tests cover bidirectional calls, function identity, boxing, and exception propagation. Java-supplied functions have no Norm source-declaration metadata.

Java method indexes retain declaration identity separately from execution implementation, allowing distinct methods to share normalized bodies. Cross-language calls verify shared default lifecycles.

Managed class method signatures project to Java abstract methods, with class abstractness determined by inherited dispatch targets. Real javac and reflection validate method/parameter annotations, generic return types, and parameter names. See JavaManagedMethodProjectionTest. Abstract declarations enter the host method index, not the local execution-entry set.

Norm-originated host calls retain concrete receiver and method type arguments; inherited views reuse CoreTypeRelations. When Java facades call ordinary methods on generic parents, the associated Norm object supplies the receiver type. JavaApplicationDispatch owns conversion. Direct Java calls to Norm methods with method type parameters, and direct construction of uninstantiated generic classes, remain unsupported.

Value-semantic List<T> in a Norm application facade projects to Java List<T>. The host boundary converts elements recursively according to their declared types, exposing immutable snapshots to Java. JavaAnnotationBindingIntegrationTest verifies nested lists, nullable elements, and Chinese text round trips.

Nullability on fields, parameters, results, and generic arguments projects through standard JSpecify Nullable type annotations. Java annotation-processing environments explicitly include JSpecify, so frameworks need not infer nullability from boxed types. JavaManagedMethodProjectionTest verifies reflection on nested collections and nullable type variables.

Acceptance ​

  • Module source trees contain no lock.norm, handwritten POM, or Gradle configuration.
  • The same module(...) factory describes pure Norm and JAR-backed Modules.
  • The type structure cannot declare two root JARs for one Module.
  • Maven and local JARs use the same Binding pipeline.
  • Replacing a JAR at the same path triggers a digest mismatch.
  • Identical JAR content can reuse a Binding artifact across paths.
  • A pinned Commons Lang version can be resolved from a Maven repository and called from Norm.
  • A packaged Module works as an ordinary dependency in another project and resolves its Java dependencies.
  • Removing Binding and providing source with the same Norm exports preserves consumer imports and call syntax.

Application-level Java resources register JavaApplicationResource through standard ServiceLoader. JvmJarBindingRuntime owns their lifecycle and closes them before releasing the application class loader. Closing a child resource such as a window does not terminate the application runtime.

JarResourceScope retains cached JAR resource URL handles until the last runtime releases them, including URLs reconstructed from strings. JvmJarBindingRuntimeTest verifies stream closure, shared runtimes, and file release.

JPA jakarta.persistence.Id and jakarta.persistence.EmbeddedId map to std.annotation.IdentityField while retaining their Java annotation identities. JavaAnnotationContract owns the mapping; see declaration references for reflection queries and field identity.

Generated declarations preserve public Java superclass relationships, substituting generic arguments across package-private intermediates. CrossModuleJarBindingTest verifies cross-module source and published artifacts. The standard-library ABI supplies binding-construction tokens centrally.

Projecting the same Norm function to the same SAM type preserves host-object identity. Weak-reference caches are isolated by application execution domain. JarBindingConcurrencyIntegrationTest verifies identity across calls and callback execution.

Norm 0.25