An optimizing PureScript-to-Java compiler, written in PureScript. Node.js runs the compiler; the generated application runs on the JVM.
Javapurs consumes the enriched TAST / tcorefn from our
PureScript fork,
optimizes it through the Java branch of PBO,
and emits Java source. The compiler and Java library ports are experimental.
For contributors: start here, then use the code map and test matrix. The FFI port matrix describes library coverage and limits.
- Type-guided ADT classes, immutable typed records and generic fallbacks.
- Tail-call loops, integer loops, direct local calls and specialized
Int -> Intfunctions. - Scope-aware extraction of large expressions into bounded Java helpers.
- Java FFI snippets, effects, references, Aff and Promise ports.
- Manifest-based publication and recovery of generated Java sources.
The compiler guide explains the pipeline; representations and specialized passes define the conditions and fallbacks of each optimization.
Use the documented Java baselines in altbak.pub and their methodology when evaluating performance. Those WIP results measure sequential workloads; they do not establish Aff support or multicore scaling.
- Node.js, Bash and Spago 1.0.3. The source recipe was exercised with Node 24.8.0; the backend pins registry package set 77.10.1 and spago.lock.
- A complete JDK 17+, including
javac,javaandjar. - The TAST-capable PureScript fork. An upstream
purswith the same version number does not provide the required metadata. - Git and Stack for a source installation. The pinned fork uses lts-23.18 / GHC 9.8.4; see the native prerequisites.
The installer fetches the exact revisions in tools/source-lock.json, builds the fork and backend, and runs Hello/Refs as standalone JARs:
git clone https://github.com/0x000000000000000000001/javapurs.git javapurs-source
cd javapurs-source
export JAVAPURS_WORKSPACE="$(dirname "$PWD")/javapurs-workspace"
node tools/install-source.mjs "$JAVAPURS_WORKSPACE"
export PATH="$JAVAPURS_WORKSPACE/bin:$PATH"
javapurs --helpThe destination must be new. Logs and partial work remain on failure; replay into
another new directory. Keep installation.json, which records revisions, source
hashes and the actual tools. See the installation and delivery contract.
In an existing checkout, provide the local packages from spago.yaml
and put the fork's binary directory first on PATH, then run:
command -v purs
./bin/build
export PATH="$PWD/bin:$PATH"bin/build locates its checkout, prints the selected tools and compiles a probe
requiring dataDecls, classDecls and typeTable before Spago.
purs --version alone cannot verify TAST capability. bin/javapurs loads
output/Main/index.js; rebuild after changing the backend or PBO.
The launcher defaults to a 16,384 MiB Node heap, configurable with JAVAPURS_HEAP.
The checked-in Hello example uses the following files.
Create an application directory with this spago.yaml:
package:
name: hello-java
dependencies:
- prelude
- effect
workspace:
packageSet:
registry: 77.10.1
backend:
cmd: javapursCreate src/Main.purs:
module Main where
import Prelude (Unit)
import Effect (Effect)
foreign import logLine :: String -> Effect Unit
main :: Effect Unit
main = logLine "Hello from javapurs"Supply the foreign binding in src/Main.java:
public static final Object logLine =
(java.util.function.Function<Object, Object>) message ->
(java.util.function.Supplier<Object>) () -> {
System.out.println((String) message);
return null;
};From the application root, with the built backend, fork and JDK on PATH:
spago build
javac --release 17 -d output/java/classes -sourcepath output/java output/java/MainRun.java
java -cp output/java/classes MainRunIt prints Hello from javapurs. Spago supplies enriched output/<Module>/corefn.json;
the backend writes Java under <input>/java, or the explicit --java-output.
Modules become __M$App_Main.java for App.Main; the executable launcher is
MainRun.java. Quote literal filenames containing $ in shell commands.
To regenerate Java from existing TAST, run javapurs --main Main. Keep compiled
classes in a separate directory and recreate that directory after module removals
or renames. For an application without external JAR dependencies:
jar --create --file app.jar --main-class MainRun -C output/java/classes .
java -jar app.jarThe runtime delivery is app.jar and a compatible JVM. For Java libraries,
add their JARs to both compilation and execution classpaths, for example
javac --release 17 -cp "lib/*" … and
java -cp "output/java/classes:lib/*" MainRun on macOS/Linux.
Dependency management and packaging belong to the application.
The Refs example selects Java ports for its executed
foreign imports, including transitive dependencies. Its paths describe the
installer's workspace/apps/refs layout. Larger applications must likewise
select every required implementation through workspace.extraPackages; see the
runner workspace and FFI contracts.
Supply arguments directly to javapurs, or in Spago's
workspace.backend.args, for example ["--main", "App.Main"].
| Option | Description |
|---|---|
--help |
Print usage and exit without reading TAST or writing files. |
--input <DIR> |
Read enriched CoreFn/TAST from this directory; defaults to output. |
--output <DIR> |
Alias of --input, matching the argument Spago appends for its own output option. |
--java-output <DIR> |
Override the Java destination; defaults to <input>/java (output/java). |
--main <Module> |
Select a module defining and exporting a local main; defaults to Main. MainRun forces a Supplier or applies a Function to null. All loaded modules are generated. |
--no-main |
Generate a library without MainRun.java. An empty input directory is accepted in this mode. |
--records=maps |
Compare Map records with the default typed representation on the same TAST. |
--loop-invariants=off |
Disable caching of eligible pure, closed Int computations within loops. |
--direct-calls=off |
Disable private static methods for known, fully applied local-module functions. Public curried functions remain available in both modes. |
--int-functions=off |
Disable the specialized Int -> Int closure/call representation. |
--ownership=off |
Disable consuming workers selected by the ownership analysis. |
--no-chunk |
Disable extraction into __chunk$N helpers; useful for focused comparisons of the same input. |
Value options accept --name VALUE or --name=VALUE. Paths are relative to the
caller. Repeated options, conflicting aliases, unknown arguments and missing
values fail with exit code 2; compilation/I/O failures return 1; success
and help return 0. Each option may occur once; --main and --no-main are
mutually exclusive. The selected main must be local and exported.
Spago 1.0.3 appends its TAST destination as --output; --java-output overrides
the Java destination. Existing java_output/ files are not migrated automatically.
Use a fresh destination, or select the old layout explicitly with
--java-output java_output. The compiler guide
details input, entrypoint and path validation; the
specialized-pass guide describes optimization switches.
.javapurs-manifest.json records generated Java names, hashes and the selected
FFI. Successful generations update that inventory and remove obsolete owned
sources. Edited generated files and collisions are reported; unrelated files
are preserved. .javapurs-work/ holds staging, locking and recovery metadata.
Consume output only after a successful compiler exit. Follow the
output lifecycle contract
before intervening in an interrupted publication.
Place Java members in a .java file beside the corresponding .purs source,
as in Main.java above. The fragment is inserted into the generated module class:
use static bindings, fully qualified Java names, and no enclosing class, package
or import declarations. Private helpers may contain ordinary Java implementation.
Curried arguments use Function<Object, Object>; an Effect a returns a Supplier
so effects run when forced. Access records through Map<String, Object> and copy
before mutation; typed records are immutable. Arrays use Object[], and generic
calls use boxed primitives. The runtime guide defines name
escaping, calling conventions, async values, resolution order and transitive ports.
A missing or empty Java fragment produces binding-specific stubs that throw when called. A provided fragment is inserted verbatim; successful compilation does not prove that all foreign paths are implemented.
The manifest's ffi.modules identifies selected fragments, origins, hashes and
bindings; status distinguishes provided, empty, missing and not-required.
verification: "not-checked" records that supplying a fragment is not proof of
executed coverage. To list missing or empty fragments:
node -e 'const {ffi} = JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8")); console.log(JSON.stringify(ffi.modules.filter(m => m.status === "missing" || m.status === "empty"), null, 2))' output/java/.javapurs-manifest.jsonIf a binding resolves to a registry package without Java, select its Java port
through extraPackages and rebuild the TAST and Java. The
report contract describes each field.
After a failed generation, the manifest still describes the last successful one.
Follow Démarrer et valider to select the fork and tools. After compiler-source changes, rebuild, then choose suites by responsibility:
./bin/build
node test/typed-records.mjs
node tools/check-all.mjs --quick--quick checks docs, port configurations, FFI inventory and generated outputs;
it rebuilds the backend to verify output stability. --full includes those
checks and adds test-tools, child-process, Spec and Node FS. Logs go to
logs/check-all/. These profiles do not cover every compiler suite or port.
The test matrix covers all 30 scripts,
including their prerequisites and optional benchmark inputs. For a named
PureScript fixture, use ./bin/test NAME; inspect selections with --list.
The 48 port launchers use the common isolated runner. Failed temporary workspaces
are retained and their paths printed; see the runner recipes.
For documentation alone, node tools/check-docs.mjs uses Node and Bash without
building. Maintenance describes tracked/generated files;
validation history preserves dated campaigns.
tools/java-tools.mjs resolves the JDK for test runners:
| Setting | Meaning |
|---|---|
JAVA_HOME or JAVAC/JAVA |
Build JDK; explicit executables take priority. JAVA remains the build JDK's sibling runtime. |
JAVAPURS_JAVA_RELEASE |
Decimal target version, 17 by default, minimum accepted target 17. Passed as javac --release N. |
JAVAPURS_JAVA_RUNTIME |
Optional executable used to run the classes; defaults to the build JDK's java. |
Explicit JAVAC/JAVA take priority, then JAVA_HOME, then javac on PATH
with a Homebrew fallback. The build pair must belong to the same real JDK bin
directory. node tools/java-tools.mjs prints selected paths, versions and target.
A system launcher such as macOS /usr/bin/javac may require selecting an installed
JDK explicitly. Raw javac commands need their own --release and classpath.
See Java configuration and cross-JDK checks
for the replayable matrix and its measured limits.
Main → Config → Driver loads and validates TAST, then invokes PBO.
For each optimized module, Pipeline runs CodeGen → Rename → Chunk.
Printer renders the Java AST; Emit assembles modules and runtime templates;
Output publishes them through a recoverable file transaction.
The code map assigns each responsibility to its module. The compiler, AST, chunking and printing guides define the detailed contracts.
Library coverage is package-specific: consult the FFI matrix. Aff uses platform daemon threads with cooperative cancellation; Promise reactions are eager rather than queued as JavaScript microtasks. Virtual-thread scheduling, Valhalla value types, broader platform coverage and distribution modes remain future work. The runtime contracts and maintenance guide describe these limits.
MIT, as declared by this project. A standalone license file has not yet been added.