25 Interoperability
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 can target the JVM or JavaScript.
The important question is not whether interop exists, but where it should live in the design. This chapter is about that boundary.
25.1 Importing Platform Symbols
Nex supports import statements at the top level.
For Java:
import java.util.Scanner
For JavaScript modules:
import Math from './math.js'
These imports are primarily meaningful when compiling or translating Nex programs to the target platform. They tell the JVM compiler or JavaScript generator which host symbols the program expects to use.
25.2 import Versus intern
intern loads Nex classes from Nex files.
File resolution follows the current Nex loader:
- the loaded file’s directory
- the current working directory
~/.nex/deps
For path-qualified classes, Nex checks lib/<path>/... layouts and also accepts lowercase filenames such as tcp_socket.nex.
import names external Java or JavaScript symbols.
This distinction should remain sharp:
- use
internfor Nex-to-Nex modularity - use
importfor host-platform interop
Confusing 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.
25.3 Keeping the Boundary Small
The best interop style is usually not to scatter host calls everywhere. Instead:
- isolate host-specific code near the boundary
- keep the core logic in ordinary Nex classes
- wrap external behavior behind Nex routines with clear contracts
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.
25.4 Translation Targets
The repository supports JVM bytecode compilation and JavaScript translation.
From Clojure:
(require '[nex.compiler.jvm.file :as jvm])
(require '[nex.generator.javascript :as js])
(println (js/translate nex-code))And for files:
(jvm/compile-jar "input.nex" "build/")
(js/translate-file "input.nex" "output.js")This matters for design because the same Nex source may be aimed at different host environments. 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.
25.5 Development Builds and Production Builds
Contracts are included in normal translated output. For production translation, the JavaScript generator supports skip-contracts:
(js/translate nex-code {:skip-contracts true})Likewise for JavaScript file translation:
(js/translate-file "input.nex" "output.js" {:skip-contracts true})This is an important design point. Contracts remain in the source as specification and documentation, but the runtime checking overhead can be removed for production output when desired.
Use this option deliberately. Development builds should usually keep contracts enabled.
25.6 with "java" Blocks
On 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:
- keep Java calls inside
with "java"blocks - expose ordinary Nex routines outside those blocks
- if needed, store a Java-backed object in a field typed as
Any, and only manipulate it insidewith "java"
25.7 A Small Interop-Oriented Design
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.
25.8 Portability and Contracts
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:
- what arguments are valid
- what it guarantees on success
- what exceptions may still arise from the environment
This makes the platform boundary explicit and safer.
25.9 A Worked Example: Separating Core Logic from Host Access
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.propertyis the host boundaryrender_runtime_reportandlineare pure Nex logicRuntime_Report_Appassembles the two
This makes the program easier to test. The formatting routines can be exercised with ordinary strings, while only Java_Runtime_Info depends on JVM interop.
25.10 Summary
importbrings in host-platform symbols;internbrings in Nex classes- Keep interop code near system boundaries rather than scattering it through core logic
- Nex can target JVM bytecode and JavaScript
- Production translation can omit runtime contract checks with
skip-contracts - Contracts are especially valuable around interop boundaries
- Portable core logic is easier to test, reason about, and reuse
25.11 Exercises
1. 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 using {:skip-contracts true} is 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.