A Java Virtual Machine interpreter implemented in Bend
Overview • Getting started • Usage • Testing • Architecture
BendJVM reads Java .class files, validates and decodes their bytecode, then executes them with an explicit JVM model written in Bend. JVM state stays in Bend: loaded classes, frames, locals, operand stacks, statics, heap entries, and interpreter status.
Warning
BendJVM is experimental. It targets a focused Java 8 compatibility slice, not full OpenJDK compatibility. Unsupported class-file features and opcodes fail explicitly.
The execution pipeline is:
Java source
│ javac --release 8
▼
.class files
│
▼
BendJVM
├─ binary reader and class-file parser
├─ constant-pool and descriptor validation
├─ bytecode decoder and branch validation
├─ stack-effect verifier
├─ class linker and bootstrap MiniJRE
└─ explicit Bend-owned interpreter state
│
▼
program output or a VM error
The project is designed around two boundaries:
-
Bend owns JVM semantics and state. Host C/JavaScript code is limited to launch I/O, file/TCP effects, and the generated-artifact adapter.
-
The reference JVM is the oracle. The differential test harness compiles the same Java fixture with
javac, runs it withjava, runs it with BendJVM, and compares observable output. -
Java 8 class files (major version 52)
-
ordered directory, JAR, and ZIP classpath sources
-
package-qualified class-name startup and standalone
.classstartup -
transitive application dependency resolution with first-match precedence
-
manifest
Main-Classand localClass-Pathexpansion for-jar -
classpath-root-relative resources through
ClassLoader.getSystemResourceAsStream -
bounded stored/deflated archive reads with origin-aware diagnostics
-
class loading, symbol interning, constant-pool linking, and descriptors
-
integer and float arithmetic, signed integer behavior, branches, loops, and recursion
-
static and virtual method calls,
invokeinterface, constructors, objects, fields, and inherited fields -
primitive and reference arrays, array bounds, null references, and heap limits
-
string constants, UTF-16 decoding,
String/StringBuilder/Integer/MathMiniJRE methods -
ArrayList/HashMap,invokeinterface, in-memory and file streams, and UTF-8 readers -
blocking TCP client/server sockets, try-with-resources, and typed exception unwinding
-
class initialization, typed Java exceptions, and
System.out.printlnfor supported primitive types -
instruction tracing, class dumps, disassembly, fuel limits, heap limits, and
--no-files/--no-net -
structured annotation attributes parsed and retained through linking, including visible/invisible declaration, parameter, type, default, and
Exceptionsmetadata -
ldcClass literals, cached mirrors for loaded classes/arrays, primitive and void mirrors,Object.getClass,Class.forName(String), andClassLoader.loadClass(String) -
bounded
Classintrospection, declaredField/Method/Constructordiscovery, annotation materialization/defaults/repeatable queries, field access, reflective invocation, and construction -
interface dynamic proxies backed by Bend-owned handler callbacks, plus ordered classpath resource streams/URLs and bounded
Propertiesparsing
Install the following tools:
- Bend 2
- Python 3.10 or newer
- JDK 9 or newer, providing
javac --release 8andjava - Bun, used to execute the JavaScript artifacts generated by Bend
From the project root, read the language guide and check the repository proof gate:
bend guide
bend PROOF.bendCompile an example to a temporary output directory:
mkdir -p /tmp/bendjvm-classes
javac --release 8 -encoding UTF-8 \
-d /tmp/bendjvm-classes \
examples/HelloWorld.javaRun a standalone class file:
python3 scripts/run.py /tmp/bendjvm-classes/HelloWorld.classRun a package-qualified class from an ordered directory/JAR/ZIP class path. After compiling the packaged example below:
python3 scripts/run.py \
-cp /tmp/bendjvm-app \
example.hello.Main BendLaunch an executable JAR. Its manifest supplies Main-Class and local
Class-Path dependencies:
python3 scripts/run.py -jar /tmp/example.jar application-argumentThe runner builds and caches Bend loader/runtime artifacts under
/tmp/bendjvm-cache by default. Override tools or cache location with
BEND, BUN, or BENDJVM_CACHE.
Application arguments preserve spaces and Unicode and become a Bend-owned
String[]. CLI usage errors exit 2; loading and VM failures exit 1.
python3 scripts/run.py [options] Main.class [application arguments...]
python3 scripts/run.py [options] -cp <entries> com.example.Main [arguments...]
python3 scripts/run.py [options] -jar app.jar [arguments...]-classpath and --class-path are aliases for -cp; entries use the host
platform path separator. Classpath sources are searched left to right, and
the first matching class or resource wins. Empty entries mean the current
directory. -jar uses the selected archive and its manifest dependencies,
ignoring ordinary classpath settings.
Useful options:
| Option | Purpose |
|---|---|
--fuel N |
Stop after at most N interpreter steps. Default: 1000000. |
--max-heap N |
Limit simulated heap entries. Default: 1024. |
--no-files |
Deny FileInputStream/FileOutputStream host effects. |
--no-net |
Deny Socket/ServerSocket host effects. |
--dump-class |
Print loaded class metadata without executing main. |
--disassemble |
Print class metadata and decoded instructions. |
--trace |
Print decoded instruction execution diagnostics to stderr. |
--prepare |
Build cached Bend loader/runtime artifacts without running a class. |
Archive sources accept stored and deflated entries. Reads are bounded at 64 MiB per entry and 512 MiB decompressed per archive. Unsafe names, duplicates, encrypted entries, unsupported compression, and corrupt payloads are rejected; archives are never extracted.
Example diagnostics:
python3 scripts/run.py --dump-class /tmp/bendjvm-classes/HelloWorld.class
python3 scripts/run.py --disassemble /tmp/bendjvm-classes/HelloWorld.class
python3 scripts/run.py --trace /tmp/bendjvm-classes/HelloWorld.class
python3 scripts/run.py --fuel 10000 /tmp/bendjvm-classes/HelloWorld.classStandalone programs in examples/ cover the MiniJRE slice:
| File | What it shows |
|---|---|
HelloWorld.java |
strings, integers, and String concatenation of arguments |
ArithmeticAndBranches.java |
arithmetic, for/while, and comparisons |
ObjectsAndDispatch.java |
constructors, fields, virtual dispatch, and identity equals |
ArraysAndStrings.java |
integer arrays, System.arraycopy, and substring |
Exceptions.java |
caught ArithmeticException and getMessage |
StaticInitialization.java |
static fields and <clinit> |
CollectionsAndText.java |
ArrayList/HashMap via interfaces, boxing cache, valueOf |
FilesAndTryWithResources.java |
files, UTF-8 readLine, and try-with-resources |
TcpLoopback.java |
blocking TCP client and single-connection server |
ReflectionAndConfiguration.java |
direct runtime-visible annotation presence, private-field access, exact overload lookup, parameter mirrors, and Properties |
Both example harnesses validate output as well as exit status:
python3 scripts/examples.py runs the standalone corpus with selectable
--only entries, while python3 examples/run.py also exercises the packaged
directory and executable-JAR paths. The standalone reflection example focuses
on field/method/configuration behavior; the differential fixture corpus also
covers runtime annotation objects and dynamic proxies.
examples/packaged/ is a small application: example.hello.Main depends on
example.lib.Answer, stores the argument and answer in an ArrayList, and
reads banner.txt from the classpath.
Run the standalone corpus with output assertions:
python3 scripts/examples.py
python3 scripts/examples.py --only ReflectionAndConfiguration --only TcpLoopbackCompile the standalone samples:
mkdir -p /tmp/bendjvm-classes
javac --release 8 -encoding UTF-8 -d /tmp/bendjvm-classes examples/*.java
python3 scripts/run.py /tmp/bendjvm-classes/HelloWorld.class
python3 scripts/run.py /tmp/bendjvm-classes/HelloWorld.class 'spaced arg'
python3 scripts/run.py /tmp/bendjvm-classes/CollectionsAndText.class
python3 scripts/run.py /tmp/bendjvm-classes/TcpLoopback.class
python3 scripts/run.py /tmp/bendjvm-classes/FilesAndTryWithResources.class /tmp/bendjvm-example.bin
python3 scripts/run.py /tmp/bendjvm-classes/ReflectionAndConfiguration.classCompile and run the packaged application from a directory:
mkdir -p /tmp/bendjvm-app
javac --release 8 -encoding UTF-8 -d /tmp/bendjvm-app \
examples/packaged/example/hello/Main.java \
examples/packaged/example/lib/Answer.java
cp examples/packaged/banner.txt /tmp/bendjvm-app/
python3 scripts/run.py -cp /tmp/bendjvm-app example.hello.Main BendExpected packaged output:
Bend
42
66
66 is the first byte of banner.txt (B). The same classes also run from a
JAR whose manifest names example.hello.Main.
Run every example through the practical runner:
python3 examples/run.pyThe broader differential corpus lives in bendjvm/tests/fixtures/ and includes integer/float operations, arrays, inheritance, strings, exceptions, class initialization, collections, files, TCP, try-with-resources, fuel limits, heap limits, classpaths, archives, manifests, and resources.
Run the fast differential suite:
python3 scripts/test.py --fastRun the complete fixture corpus, malformed class-file cases, CLI checks, and 24 generated integer programs:
python3 scripts/test.py --fullRun selected fixtures:
python3 scripts/test.py --only Arithmetic --only IntArrayThe harness requires Python 3.10+, a JDK, Bend 2, and Bun. It compiles sources and writes generated or mutated class files to temporary directories outside the repository. Override executables with --bend, --java, --javac, or the corresponding BEND, JAVA, and JAVAC environment variables.
| Area | Responsibility |
|---|---|
bendjvm/classfile/ |
Big-endian reader, Java 8 class parser, constant pool, attributes, descriptors, and modified UTF-8. |
bendjvm/bytecode/ |
Instruction decoding, operand normalization, decoded program counters, and branch-target checks. |
bendjvm/verifier/ |
Stack contracts, local bounds, descriptor checks, and instruction validation. |
bendjvm/loader/ |
Symbol interning, class catalog/linking, bootstrap classes, intrinsic registration, metadata retention, and VM construction. |
bendjvm/heap/ |
Object, primitive-array, reference-array, string, and resource-stream entries with bounded allocation. |
bendjvm/java/ |
MiniJRE intrinsics: objects/strings/collections, bounded class mirrors/member discovery, resources/URL/Properties, streams, files, TCP, throwables, and println. |
scripts/classpath.py |
Ordered directory/JAR/ZIP sources, manifest expansion, bounded reads, and resource lookup. |
scripts/run.py |
Builds cached Bend JavaScript artifacts and provides the structured practical command-line runner. |
scripts/test.py |
Differential, classpath/JAR, file/TCP, malformed-input, limit, debug-mode, and generated-program tests. |
LAWS.bend / PROOF.bend |
Bend law declarations and proofs checked by bend PROOF.bend. |
Supported methods are registered by exact owner, name, descriptor, and flags in
bendjvm/loader/bootstrap.bend. Unlisted overloads are not implied by a class
name; they fail with NoSuchMethodError before execution. Access flags are
public instance native (0x0101), public static native (0x0109), or
interface abstract (0x0401).
| Area | Classes and descriptors |
|---|---|
| Objects | Object.<init>()V, equals(Ljava/lang/Object;)Z, hashCode()I, toString()Ljava/lang/String;; Objects.requireNonNull(Ljava/lang/Object;)Ljava/lang/Object;, equals(Ljava/lang/Object;Ljava/lang/Object;)Z |
| Strings | String.length()I, isEmpty()Z, charAt(I)C, equals, hashCode, substring(I), substring(II), concat, toString, valueOf(I|Z|Ljava/lang/Object;); StringBuilder.<init>()V, <init>(Ljava/lang/String;)V, append(String|Object|I|C|Z), length, toString |
| Integers / Math | Integer.valueOf(I), intValue(), parseInt, equals, hashCode, toString; cached valueOf identities for -128..127; Math.abs/min/max on I; System.arraycopy |
| Collections | ArrayList/List/Collection/Iterable/Iterator and HashMap/Map as registered in bootstrap, including iterator hasNext/next/remove |
| Streams | InputStream.read()I, read([B)I, read([BII)I, close; OutputStream.write(I|[B|[BII), flush, close; ByteArrayInputStream([B), ByteArrayOutputStream toByteArray/size/reset |
| Files | File(String) exists/isFile/isDirectory/getPath; FileInputStream(String); FileOutputStream(String) and (String,Z) |
| Reflection / metadata | Object.getClass(), cached primitive/void/array/class mirrors and wrapper TYPE fields, Class name/superclass/modifier/array/interface/primitive/assignability/instance checks, annotation queries/materialization/defaults/repeatable expansion, cast, forName(String) and forName(String,boolean,ClassLoader), ClassLoader.loadClass(String), declared member arrays and exact name/parameter lookup, declaring-class/basic metadata accessors, per-handle AccessibleObject.setAccessible/isAccessible, Field.get/set, Method.invoke, and Constructor.newInstance; Proxy/InvocationHandler interface proxies, generated-class identity, category-one boxing, and handler lookup |
| Resources / configuration | Class/ClassLoader relative/root lookup, ordered getResources, origin-bound URL.openStream, local URL path/protocol/string accessors, Properties.load(InputStream) and bounded UTF-8 Properties.load(Reader) for memory-backed InputStreamReader, getProperty/default getter/setProperty; lookup is ordered and first-match |
| Text | Reader.read()I, read([CII)I, close; Writer.write(I|String|[CII), flush, close; InputStreamReader/OutputStreamWriter (stream, UTF-8); BufferedReader.readLine |
| TCP | InetSocketAddress(String,I); Socket()V, (String,I), connect(SocketAddress,I), getInputStream, getOutputStream, setSoTimeout, close; ServerSocket(I), getLocalPort, accept, setSoTimeout, close |
| Throwables | Throwable no-arg/message/cause/message+cause <init>, getMessage, getCause, initCause, addSuppressed, getSuppressed, toString; UndeclaredThrowableException cause access; IOException has the four Java 8 constructors |
The reflection slice is intentionally bounded. Class mirrors are cached for
loaded class and array identities. Declared members expose basic metadata;
Field.get/set, Method.invoke, and Constructor.newInstance cover the
supported access path, including primitive wrapper arguments and results.
Primitive/void mirrors and wrapper TYPE fields are supported. setAccessible
is tracked per reflective handle for bounded private-member access. Runtime
annotations materialize supported scalar/reference values, raw-word boxed
Long/Double values, defaults, parameter metadata, defensive array results,
and repeatable containers.
Class.forName(name, false, loader) leaves a loaded class uninitialized;
the true form initializes the superclass chain before the direct <clinit>,
and the supported application namespace is loader-aware. Interface proxies
generate a public InvocationHandler constructor, validate interface
visibility/conflicts/returns, preserve declared checked exceptions, cache by
loader and ordered interfaces, and wrap undeclared checked throwables.
Method, field, and class Signature attributes expose generic return,
parameter, exception, field, and superclass types, including parameterized
type arguments and type-variable names.
Class.forName(name, initialize, null) resolves bootstrap classes through the
bootstrap loader. Caller package edges, default-method proxy dispatch,
category-two bytecode execution, and complete OpenJDK formatting remain
outside this bounded slice.
Resources are root-relative for ClassLoader and package-relative for Class;
relative class names normalize ./ and .., while single lookups preserve
first-match classpath order. getResources enumerates every matching origin in
order. Returned URLs retain the selected origin, reopen it independently, and
expose local file:/jar:file: protocol and UTF-8 percent-escaped string
forms for supported local paths.
Properties.load(InputStream) handles ISO-8859-1 bytes, Unicode escapes,
comments, separators, continuation lines, duplicate-key last-write behavior,
default getters, and malformed Unicode escape rejection. load(Reader) is
implemented for the bounded UTF-8 InputStreamReader over memory resources;
other reader backends remain unsupported.
The class-file metadata parser validates declaration, parameter, type, and
nested Code type-annotation attributes and links them into method metadata.
Runtime-visible annotations materialize supported values on demand; CLASS
retention remains absent from runtime query results. Wide annotation constants
are represented losslessly as two raw U32 words inside boxed Long/Double
reflection results; they do not imply general long/double execution.
Host file and TCP effects use opaque handles scoped to one VM run. --no-files
and --no-net deny those effects with IOException. --max-heap counts heap
entries. Relative files resolve against the launch working directory. The host
closes remaining handles on every process exit, including fuel exhaustion and
uncaught exceptions. This is a capability control, not a sandbox.
BendJVM accepts Java 8 class files and ordered directory/JAR/ZIP classpaths, but intentionally does not provide full JVM or OpenJDK compatibility. It does not yet promise:
- full reflection access-edge compatibility (caller package cases), parameter-name reflection, or complete annotation formatting
- modules, JNI, agents, custom class-loader subclasses, multiple application namespaces, or runtime class publication
longanddoublebytecode execution, category-two field/call conversion, or complete floating-point edge-case compatibility; annotation reflection only exposes boxed raw-word wide valuesinvokedynamic, method handles, default-method special proxy invocation, or full verifier compatibility- Java threads, monitors,
synchronized,volatile, garbage collection, JIT compilation, or Spring Boot nested-JAR layouts
BendJVM is not a sandbox-grade security boundary. Java code only reaches host behavior through registered intrinsics, but untrusted bytecode should still be treated as untrusted input.
The project specification is documented in bendjvm-spec.md. Its guiding principles are:
- Decode class files once and execute compact linked instructions.
- Resolve names into numeric IDs before instruction execution.
- Represent the JVM as explicit state transitions rather than relying on Bend call-stack state.
- Reject unsupported semantics instead of approximating them.
- Prove small interpreter invariants in Bend before optimizing representation or dispatch.
Classpath, JAR/ZIP, manifest, MiniJRE streams, files, blocking TCP, bounded reflection, runtime annotation materialization, and interface proxies are in place. The next milestones are broader bytecode coverage, stronger edge-case compatibility, and concurrency — not Spring Boot until those exist.