No practical language lives entirely by itself. Sooner or later a program needs a file system, a library, a host API, or an external service. Nex is designed for that reality. It can import host-platform symbols and it targets the JVM.
The important question is not whether interop exists, but where it should live in the design. This chapter is about that boundary.
Nex supports import statements at the top level. For Java:
import java.util.Scanner
These imports are primarily meaningful when compiling Nex programs. They tell the JVM compiler which host symbols the program expects to use.
import Versus internintern loads Nex classes from Nex files, and its file resolution follows the current Nex loader:
~/.nex/depsFor path-qualified classes, Nex checks lib/<path>/... layouts and also accepts lowercase filenames such as tcp_socket.nex.
import, by contrast, names external Java symbols. This distinction should remain sharp:
intern for Nex-to-Nex modularityimport for host-platform interopConfusing the two leads to confused architecture. A Nex class is part of your program’s design. An imported host symbol is part of the surrounding environment.
The best interop style is usually not to scatter host calls everywhere. Instead:
For example, rather than calling platform I/O throughout a program, define a small Nex service class whose job is “read configuration” or “write report.” The rest of the program then depends on that service’s contract, not on host details.
Nex compiles to JVM bytecode. From Clojure:
(require '[nex.compiler.jvm.file :as jvm])
(jvm/compile-jar "input.nex" "build/")
This matters for design because a routine that depends only on arrays, maps, strings, and user-defined classes is much easier to move, test, and trust than one tangled with host APIs at every step.
with "java" BlocksOn the JVM, Nex also supports:
with "java" do
...
end
This marks a block whose body may use Java interop directly. In practice, that means method calls and class names inside the block may resolve against imported Java classes and host objects rather than only against ordinary Nex classes.
For example:
import java.lang.System
with "java" do
print(System.getProperty("java.version"))
end
This form is JVM-specific. It is useful when a small part of the program genuinely needs host behavior, but the surrounding design should remain ordinary Nex.
At present, this is primarily a compiled-JVM feature. It works well in JVM REPL sessions and on the compiled JVM path, but it is not supported by the interpreter-based file runner used by:
nex some_file.nex
So if a file relies on with "java", do not assume it will run correctly through the interpreter path. Use the compiled JVM route instead.
In REPL-oriented wrapper classes, with "java" is often the most practical way to isolate the host boundary. A common pattern is:
with "java" blocksAny, and only manipulate it inside with "java"A Nex class can inherit an imported Java interface, not just another Nex class. The class must then define a method for every abstract member of the interface, spelled exactly as Java spells it — run, not Run or run_. Nex does not translate between its own naming convention and Java’s; the method name in the feature block is the interface’s method name, verbatim.
import java.lang.Runnable
import java.lang.Thread
class Countdown
inherit
Runnable
feature
run() do
print("liftoff")
end
end
with "java" do
let task: Countdown := create Countdown
let t: Thread := Thread.new(task)
t.start()
t.join()
end
Countdown is now a real java.lang.Runnable — not something that merely looks like one from the Nex side. Passing it to Thread.new hands the JVM an object it can call run() on directly, exactly as it would for a Runnable written in Java. This is what makes the class usable with any Java API that expects the interface: a Comparator can be handed to Collections.sort, an ActionListener can be registered on a Swing button, and so on — always by inheriting the interface and defining its methods under their Java names.
Unlike with "java" blocks themselves, implementing an interface works the same way whether the program runs through the interpreter or the compiled JVM path. A method call the interface didn’t declare — one the class defines only for its own purposes — remains an ordinary Nex method, callable the ordinary way, alongside the interface’s methods.
A Nex class can also inherit a concrete Java class, not only an interface. This is a heavier commitment than implementing an interface — the class becomes a real subclass at the bytecode level — so it comes with two restrictions worth knowing up front. First, it works only on the compiled JVM path; the interpreter reports a clear error rather than approximating it. Second, a class may extend at most one Java class, matching the JVM’s own single-inheritance rule (it may still additionally implement any number of Java interfaces, and inherit Nex classes, in the same inherit list).
Reaching the Java superclass’s constructor uses a reserved method name, new, as the first statement of the Nex constructor:
import java.lang.Thread
class Worker
inherit
Thread
feature
run() do
super.run()
print("working")
end
end
with "java" do
let w: Worker := create Worker
w.setName("background")
w.start()
w.join()
print(w.getName())
end
Worker declares no constructor of its own here, so nothing calls super.new(...) explicitly — that is fine as long as the Java class has a public no-argument constructor, which Thread does. Written out, the equivalent explicit form is super.new() or, naming the class directly, Thread.new(); either is accepted as the constructor’s first statement, and the typechecker rejects it anywhere else, since the JVM requires the superclass constructor to run before anything else touches the new object.
Three things are happening in run that are worth noticing separately. super.run() reaches Thread’s own implementation of run — the same real method a Java subclass’s super.run() would reach, not a recursive call back into this override. run itself is an override: because a real java.lang.Thread calls its own run() internally when start() spawns the new thread, and that call must land on this class’s version for the override to mean anything. And w.setName(...), w.start(), w.join(), and w.getName() are all methods Worker never defines — inherited, un-overridden members of Thread, callable on a Worker exactly as they would be on a plain Thread.
The one gap worth knowing about: super.new(...) and super.<method>(...) currently only support the zero-argument case. A Java superclass whose only usable constructor takes arguments — reading a title into a windowing-toolkit base class, say — cannot yet be reached this way; the compiler reports this plainly rather than silently dropping the arguments.
In JVM REPL sessions, a practical style is to import a Java class and wrap it in a small Nex class immediately. For example, suppose we want efficient string assembly. We could expose StringBuilder everywhere, but a better design keeps that host type inside one Nex wrapper:
import java.lang.StringBuilder
class Line_Buffer
create
make() do
with "java" do
this.builder := create StringBuilder
end
end
feature
builder: Any
append_line(s: String) do
with "java" do
builder.append(s)
builder.append("\n")
end
end
text(): String do
with "java" do
result := builder.toString()
end
end
end
class Greeting_Report
feature
render(name: String): String do
let buf := create Line_Buffer.make
buf.append_line("Hello, " + name)
buf.append_line("Welcome to Nex on the JVM.")
result := buf.text()
end
end
In a REPL session, this is convenient:
nex> import java.lang.StringBuilder
nex> class Line_Buffer
create
make() do
with "java" do
this.builder := create StringBuilder
end
end
feature
builder: Any
append_line(s: String) do
with "java" do
builder.append(s)
builder.append("\n")
end
end
text(): String do
with "java" do
result := builder.toString()
end
end
end
nex> let b := create Line_Buffer.make
nex> b.append_line("alpha")
nex> b.append_line("beta")
nex> print(b.text())
alpha
beta
The design point is the important part: StringBuilder is confined to Line_Buffer. The rest of the program depends on ordinary Nex routines such as append_line and text, not on Java library details.
Contracts are especially valuable around interop because host libraries often sit outside the type and contract discipline of the Nex core.
If a wrapper routine imports or calls external functionality, its contract should state:
This makes the platform boundary explicit and safer.
Here is a complete JVM-oriented example. Suppose we want to print a short report about the current Java runtime. Reading system properties is host access. Formatting the report is ordinary program logic. We should separate those two concerns:
import java.lang.System
function line(label, value: String): String
do
result := label + ": " + value
end
function render_runtime_report(version, vendor, home: String): String
do
result := line("Java version", version) + "\n"
result := result + line("Java vendor", vendor) + "\n"
result := result + line("Java home", home)
end
class Java_Runtime_Info
feature
property(name: String): String
require
valid_name: name /= ""
do
let value: ?String := nil
with "java" do
value := System.getProperty(name)
end
if value = nil then
result := "<missing>"
else
result := value
end
end
end
class Runtime_Report_App
feature
run(): String do
let info := create Java_Runtime_Info
let version := info.property("java.version")
let vendor := info.property("java.vendor")
let home := info.property("java.home")
result := render_runtime_report(version, vendor, home)
end
end
let app := create Runtime_Report_App
print(app.run())
The design is deliberate:
Java_Runtime_Info.property is the host boundaryrender_runtime_report and line are pure Nex logicRuntime_Report_App assembles the twoThis makes the program easier to test. The formatting routines can be exercised with ordinary strings, while only Java_Runtime_Info depends on JVM interop.
import brings in host-platform symbols; intern brings in Nex classesinherit an imported Java interface to implement it, with methods named exactly as Java names them — works on both the interpreter and the compiled pathinherit a concrete Java class to extend it, forwarding to its constructor with super.new(...) — compiled-JVM only, and only for a zero-argument constructor call so far1. Write a short example containing both an intern statement and an import statement. Explain the different role each plays.
2. Choose a small program idea and identify which parts should remain pure Nex logic and which parts belong to the host boundary.
3. Write a wrapper-class design for a clock or random-number service. State the contract of the main routine and explain what remains host-specific.
4. Explain when disabling runtime contract checks in a production build might be reasonable, and when it is risky.
5.* Take one earlier example, such as a report printer or configuration loader, and redesign it so that host-specific work is isolated in one class while the main computation remains platform-independent.