Class BytecodeExecutor

java.lang.Object
org.ek9lang.compiler.backend.jvm.BytecodeExecutor

public final class BytecodeExecutor extends Object
Executes generated EK9 bytecode within the current JVM. This avoids spawning external JVM processes for each execution, significantly improving performance.

The executor uses a URLClassLoader to load generated .class files from the compilation output directory. The ek9-lang runtime classes are loaded from the classpath (parent-first delegation).

Output capture is achieved using thread-local PrintStreams. Each thread captures its own stdout and stderr independently, enabling parallel-safe execution. EK9 programs use Stdout/Stderr which write to System.out/System.err, but we redirect both to thread-local delegating streams.

Used by REPL, bytecode tests, and the EK9 test runner.

  • Method Details

    • redirectUncaptured

      public static BytecodeExecutor.OutputRedirection redirectUncaptured(PrintStream out, PrintStream err)
      Route everything the per-thread capture does NOT take to the given streams, and return a handle that restores the previous routing.

      Call this instead of System.setOut/System.setErr around a run whose stray output must be captured — an embedding host such as the MCP server, whose stdout carries JSON-RPC. A plain setOut REPLACES the delegating stream installed below, and the static initialiser that installed it never fires again, so from the second run onward in a long-lived process every per-case capture silently returns nothing and every expected-output comparison sees empty actual output. That is invisible: the aggregate buffer still fills, so the run looks like it produced output while each case looks like it produced none.

      Re-installs the delegating streams if something has displaced them, so this is also the repair path, not only the polite one.

      Parameters:
      out - where uncaptured stdout should go
      err - where uncaptured stderr should go
      Returns:
      a handle whose close restores the previous fallbacks
    • execute

      public static String execute(Path bytecodeDir, String programClassName, String... args) throws Exception
      Executes a generated EK9 program and returns its captured stdout output.

      This is the backward-compatible method for REPL and simple bytecode tests. No env var overrides, no stdin fixtures, no stderr capture returned.

      Parameters:
      bytecodeDir - Directory containing generated .class files
      programClassName - Fully qualified class name (e.g., "bytecode.test.MyProgram")
      args - String arguments to pass to _main() method
      Returns:
      The captured stdout output from the program
      Throws:
      Exception - if execution fails
    • executeWithFixtures

      public static BytecodeExecutor.ExecutionResult executeWithFixtures(Path bytecodeDir, String programClassName, String[] args, Map<String,String> envVars, String stdinContent)
      Executes a generated EK9 program with full test isolation fixtures.

      Provides per-thread isolation for all four I/O channels:

      • stdout - captured via thread-local PrintStream
      • stderr - captured via thread-local PrintStream
      • env vars - thread-local override via EnvVars._setThreadEnv()
      • stdin - thread-local override via Stdin._setThreadInput()

      The env vars are an OVERLAY: names not in the map still resolve from the process environment - the dev/envvars.txt contract, and what the native leg's ProcessBuilder.environment().putAll does. For a run whose expected output asserts that a realistic name is ABSENT, use executeHermetic(Path, String, String[], Map, String).

      Parameters:
      bytecodeDir - Directory containing generated .class files
      programClassName - Fully qualified class name
      args - String arguments to pass to _main() method
      envVars - Thread-local env var overrides (null = no override)
      stdinContent - Content to provide as stdin (null = no override)
      Returns:
      Execution result with captured stdout, stderr, exit code, and duration
    • executeHermetic

      public static BytecodeExecutor.ExecutionResult executeHermetic(Path bytecodeDir, String programClassName, String[] args, Map<String,String> environment, String stdinContent)
      As executeWithFixtures(Path, String, String[], Map, String), but the given map IS the program's entire environment - nothing the JVM inherited from the process is visible through EnvVars or the Networking env gateways, so an empty map is an empty environment.

      This is for running EXAMPLES: the Q&A corpus reads realistic names (AWS_REGION, API_KEY, DB_HOST, GITHUB_TOKEN) and its expected output asserts them absent, which held on every developer machine and failed the first time CI ran on a box with AWS_REGION=eu-west-2 set. A fixture that must be visible (QA0271 demonstrates PATH/HOME being present) goes in the map.

      Parameters:
      bytecodeDir - Directory containing generated .class files
      programClassName - Fully qualified class name
      args - String arguments to pass to _main() method
      environment - The whole environment the program will see (never null)
      stdinContent - Content to provide as stdin (null = no override)
      Returns:
      Execution result with captured stdout, stderr, exit code, and duration