Chapter 24

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 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.

Importing Platform Symbols

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 intern

intern loads Nex classes from Nex files, and its file resolution follows the current Nex loader:

  1. the loaded file’s directory
  2. the current working directory
  3. ~/.nex/deps

For 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:

  • use intern for Nex-to-Nex modularity
  • use import for 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.

Keeping the Boundary Small

The best interop style is usually not to scatter host calls everywhere. Instead:

  1. isolate host-specific code near the boundary
  2. keep the core logic in ordinary Nex classes
  3. 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.

Compilation Target

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" 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 inside with "java"

Implementing a Java Interface

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.

Extending a Java Class

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.

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.

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.

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.property is the host boundary
  • render_runtime_report and line are pure Nex logic
  • Runtime_Report_App assembles 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.

Summary

  • import brings in host-platform symbols; intern brings in Nex classes
  • Keep interop code near system boundaries rather than scattering it through core logic
  • Nex compiles to JVM bytecode
  • inherit an imported Java interface to implement it, with methods named exactly as Java names them — works on both the interpreter and the compiled path
  • inherit 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 far
  • Contracts are especially valuable around interop boundaries
  • Portable core logic is easier to test, reason about, and reuse

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 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.