Jamie Balfour

Welcome to my personal website.

Find out more about me, my personal projects, reviews, courses and much more here.

Official ZPE/YASS documentationZPE/YASS Developer Guide

This page describes the conventions used when developing ZPE/YASS itself, writing native Java functions, creating modules and objects, and building extension libraries. Following these conventions keeps extensions consistent with the ZPE runtime and makes them easier to maintain, document and debug.

Conventions

A convention describes the standard way in which a feature should be implemented. Code may technically work without following every convention, but code intended for inclusion in ZPE or distribution through ZULE should follow them consistently.

Native return values

All values crossing from native Java code into YASS should use the ZPE type system. Native functions and module methods should therefore return an implementation of ZPEType, rather than exposing an arbitrary Java object directly.

Common ZPE value types include:

  • String
  • Number
  • Boolean
  • List and array
  • Map and ordered map
  • Function
  • Object and structure
  • Record
  • Undefined

Use the appropriate ZPE implementation, such as ZPEString, ZPENumber, ZPEBoolean, ZPEList or ZPEMap.

A Java value that has no direct ZPE representation should be wrapped in a suitable ZPE object. For example, a Java BufferedImage should be held internally by an image object which exposes safe native methods for reading, modifying and saving it.

Do not expose implementation-specific Java objects directly to YASS code. Wrapping them preserves the ZPE type system, permission checks, object behaviour and compatibility with ZPEX.

Native parameters

Modern native module methods receive their arguments as an array of ZPEType values together with the active ZPERuntimeEnvironment. A native method should validate the number and types of its arguments before using them and should report invalid input through ZPE's exception system.

Native methods must also accurately declare their parameter requirements, return types, permission level, documentation and version information. This information is used by the manual, the compiler, ZIDE and other development tools.

Naming conventions

YASS uses lowercase names separated by underscores. Native functions, module methods, reference methods, keywords and public library functions should follow this convention.

For example, use:

YASS
file_get_contents
ZPE::list_zpe_modules
calculate_total

Avoid publicly exposing camel-case names such as getFileContents. Java implementation classes may follow normal Java naming conventions, but the names visible to YASS should use lowercase words separated by underscores.

This is a requirement for code intended for inclusion in ZPE or publication through ZULE. Consistent naming is particularly important because these names also appear in documentation, autocomplete suggestions and compiler diagnostics.

However, modules and objects may use camel case, and it's encouraged. So for example, the sequential file object is written as SequentialFile.

Creating native Java libraries

ZPE is written in Java and can be extended with native functions, objects and modules. A modern native library should implement ZPELibrary, which extends ZPEPluginSuperClass.

A library supplies:

  • Its name and version information
  • A map of native functions
  • A map of native object factories
  • A map of native modules
  • The operating systems it supports

Native libraries should be distributed as non-runnable JAR files. They should contain the library implementation and its required resources, but should not define an application entry point merely to launch the library.

Keep the public API small and return only ZPE-compatible values. Platform- specific functionality must be identified accurately using the operating system support methods supplied by the library interface.

Submitting libraries to ZULE

ZULE is the ZPE package repository for distributing approved extensions and libraries. Before submission, a library should compile cleanly, follow the conventions in this document and include accurate documentation for every public function, object and module.

Submitted code should not bypass ZPE permission checks, expose unsafe Java objects, depend on undocumented internal behaviour or include unnecessary executable entry points. Platform restrictions and external dependencies must be clearly documented.

Packages are reviewed before publication. The current ZULE submission process should be used rather than relying on older GUI submission workflows described by previous versions of this document.

Debugging YASS programs

ZPE includes a socket-based debugger for debugging YASS programs. This is the debugger used by ZIDE to provide breakpoints, variable inspection, stepping and runtime profiling.

The debugger consists of two parts:

  • The debugger interface, normally ZIDE, opens a server socket and waits for ZPE to connect.
  • ZPE runs the YASS program in debug mode and sends debugging events to the interface over the socket.

Starting a debugging session

ZPE can run a YASS file in debug mode using the -d ZAC:

Terminal
zpe -d program.yas

By default, ZPE connects to a debugger listening on localhost using port 45678. A different port can be specified when required:

Terminal
zpe -d program.yas -port 5000

The debugger server must be started before ZPE is launched. If no server is listening, ZPE will report that it could not connect to the selected debugging port. Debugging connections are restricted to the local computer.

ZIDE handles this process automatically. It opens the debugger server, starts ZPE as a separate process using the corresponding port and then waits for ZPE to establish the connection.

Example debugger integration

The following simplified code is based on ZIDE. It opens a local debugger server, launches ZPE with the same port and asks ZPEDebugger to process the incoming events:

Java
ServerSocket debugServer =
    new ServerSocket(0);
int debugPort =
    debugServer.getLocalPort();

ProcessBuilder processBuilder =
    new ProcessBuilder(
        "java",
        "-jar",
        zpeJarPath,
        "-d",
        yassFile.toAbsolutePath().toString(),
        "--silent",
        "--use_zpe_events",
        "-port",
        Integer.toString(debugPort)
    );

Process zpeProcess =
    processBuilder.start();

// These streams must be consumed so that the ZPE process cannot block
// when its output buffers become full.
readProcessOutput(zpeProcess.getInputStream());
readProcessErrors(zpeProcess.getErrorStream());

// Accept ZPE's connection and process its debugger events in the background.
ZPEDebugger.respond(debugServer);

Using port 0 asks the operating system to choose an available port. ZIDE then passes that selected port to ZPE. This avoids assuming that a fixed port is available.

ZIDE registers a breakpoint listener before starting the debugging session:

Java
ZPEDebugger.addBreakPointReachedListener(
    (breakpoint, variables) -> {
      Platform.runLater(() -> {
        currentBreakpoint = breakpoint;
        showVariables(variables);
        showDebugControls();
      });
    }
);

The supplied BreakPoint object represents the paused ZPE process. ZIDE's Continue, Step Over and Stop buttons call the corresponding methods on that object:

Java
private void continueDebugging() {
  if (currentBreakpoint != null) {
    currentBreakpoint.resume();
  }
}

private void stepOver() {
  if (currentBreakpoint != null) {
    currentBreakpoint.stepOver();
  }
}

private void stopDebugging() {
  if (currentBreakpoint != null) {
    currentBreakpoint.stopExecution();
  }
}

Internally, these methods send a newline-terminated command back to ZPE:

Incoming events travel in the opposite direction as newline-delimited JSON. For example, a breakpoint event contains the event name and the variables visible at the point where execution paused:

Debugger commands
continue
step
stop
JSON
{
  "event": "breakpoint_hit",
  "line": "0",
  "vars": {
    "$counter": 42,
    "$name": "Jamie"
  }
}

ZPEDebugger.respond accepts the socket connection, decodes these JSON messages and turns them into higher-level Java listeners and BreakPoint controls. An editor using ZPE as a dependency can therefore reuse this implementation rather than recreating the wire protocol.

Because ZIDE uses JavaFX while the debugger reads from a background thread, interface changes are passed to Platform.runLater. Other user interface toolkits should use their equivalent mechanism for transferring work onto the interface thread.

Breakpoints

Breakpoints allow execution to pause at a selected point in a YASS program. Internally, a breakpoint is represented using:.

YASS
#breakpoint#

When ZPE reaches this marker during a debugging session, it sends a breakpoint_hit event to the debugger. The event includes the variables available at that point in the program.

Developers should normally create and remove breakpoints through ZIDE. ZIDE manages the internal breakpoint markers and associates them with the appropriate source lines.

Continuing and stepping

After reaching a breakpoint, ZPE waits for an instruction from the debugger. The debugger can tell ZPE to continue normally or to step through subsequent YASS statements.

ZIDE provides controls for:

  • Continuing execution
  • Stepping to the next statement
  • Inspecting variables at a breakpoint
  • Stopping the running program

Debugger communication

ZPE and the debugger communicate using newline-delimited JSON messages over a local TCP socket. ZPE sends events such as:

  • connected
  • breakpoint_hit
  • failure
  • profile_sample
  • profile_complete
  • done

The debugger responds with commands that control execution. New debugger integrations should use this protocol rather than attempting to control the ZPE process through its standard output.

Profiling

A debugging session can also provide profiling information. ZPE reports elapsed runtime, memory usage, CPU usage, thread count and the YASS function executing when each sample was taken.

ZPEX samples more frequently because native YASS programs can finish extremely quickly. Profiling can affect runtime performance, so profiling measurements should be treated as diagnostic information rather than exact benchmark results.

Using the debugger outside ZIDE

Another development environment can attach to the YASS debugger by opening a server socket on the chosen local port and then launching ZPE with -d and the same -port value.

The external debugger must accept ZPE's connection, read its JSON events and respond to breakpoint and stepping events using the debugger protocol. ZPE remains the debugger client; the editor or debugging tool acts as the server.

Building native plugins

A native ZPE plugin is a Java library that implements ZPELibrary. The plugin can provide native functions, objects and modules to YASS.

The plugin JAR must contain a public class named Plugin in the default package. This class is the entry point loaded by ZPE and should implement ZPELibrary.

A plugin must provide:

  • getName(), which returns the plugin's unique name.
  • getVersionInfo(), which returns its version information.
  • getFunctions(), which returns its native YASS functions.
  • getObjects(), which returns factories for its native objects.
  • getModules(), which returns its native modules.
  • supportsWindows(), supportsMacOs() and supportsLinux(), which describe its platform support.

A minimal plugin entry point has the following form:

Java
public class Plugin
    implements ZPELibrary {

  @Override
  public String getName() {
    return "example";
  }

  @Override
  public String getVersionInfo() {
    return "1.0";
  }

  @Override
  public
  Map<String, ZPECustomFunction>
      getFunctions() {
    return new HashMap<>();
  }

  @Override
  public
  Map<String,
      BiFunction<ZPERuntimeEnvironment,
          ZPEPropertyWrapper, ZPEObject>>
      getObjects() {
    return new HashMap<>();
  }

  @Override
  public
  Map<String, ZPEModule>
      getModules() {
    return new HashMap<>();
  }

  @Override
  public boolean supportsWindows() {
    return true;
  }

  @Override
  public boolean supportsMacOs() {
    return true;
  }

  @Override
  public boolean supportsLinux() {
    return true;
  }
}

Compile the project against the current ZPE JAR and package the compiled classes and resources into a non-runnable JAR. Plugin filenames should begin with zpe.lib. and end with .jar.

Install the JAR in the native-plugins directory inside ZPE's application-data directory. The active ZPE installation path can be found using:

Terminal
zpe -h config

Plugins should be tested against the same ZPE version used to compile them. They should not depend unnecessarily on package-private runtime behaviour. Native Java plugins are supported by JVM-based ZPE but are not dynamically loaded by ZPEX.

Building transpilers

A ZPE transpiler converts the compiled YASS syntax tree into another programming language. A transpiler should implement ZPESyntaxTranspiler.

Java
public interface
    ZPESyntaxTranspiler {

  String transpilerName();

  String transpile(
      IAST code,
      String className
  );

  String getLanguageName();

  String getFileExtension();
}

The transpile method receives the compiled IAST, not the original YASS source. The transpiler should traverse this tree and generate equivalent source code in its target language.

External transpilers use the package:

Java
jamiebalfour.zpe.transpilers

Their class and JAR names must identify the target language. For example, a Python transpiler should use the JAR file name zpe.transpiler.python.jar and the Java package:

Java
jamiebalfour.zpe.transpilers.ZPEPythonTranspiler

The transpiler JAR should be placed in the transpilers directory inside ZPE's application-data directory. It can then be used from ZIDE or from the command line:

Terminal
//Python is defined in the plugin as the language name
zpe -e program.yas -o program.py -t Python -n Program

getLanguageName() supplies the language identifier shown by ZPE and ZIDE, while getFileExtension() supplies the default output extension. transpilerName() should identify the transpiler implementation itself.

Understanding the IAST

Plugin developers usually work with ZPE values and runtime APIs. Transpiler developers, compiler contributors and language-tool developers must also understand the IAST, or inline abstract syntax tree.

An IAST node contains a byte-code type, an optional identifier or value, source positions and a number of links to other nodes:

  • next normally links statements in sequence.
  • left and middle contain structural or expression branches whose meaning depends on the node's byte code.
  • value may contain a literal value, metadata or another IAST branch.
  • id identifies variables, functions, modules and other named elements where applicable.
  • programStart and programEnd retain the node's relationship with the original source.

The meaning of these links is not identical for every byte code. A transpiler must interpret each node according to its specific structure rather than assuming, for example, that left always represents the same kind of child.

Before implementing a new node, observe how the same construct is represented in ZPE's AST whitepapers. The AST_WP documentation contains diagrams showing the shapes produced for YASS statements and expressions.

It is also useful to compile small, isolated examples and inspect their trees using ZPE's tree-view mode:

Terminal
zpe -t example.yas

Compare the resulting tree with the relevant AST_WP diagram before adding a transpilation rule. Test braced and non-braced forms where YASS supports both, as different surface syntaxes may deliberately compile into the same IAST representation.

Transpilers should operate on byte-code constants from YASSByteCodes rather than hard-coded numeric values. They should also handle unknown nodes explicitly so that a newly introduced YASS feature does not silently generate incorrect target code.

Comments

There are no comments on this page.

New comment

Comments are welcome and encouraged, including disagreement and critique. However, this is not a space for abuse. Disagreement is welcome; personal attacks, harassment, or hate will be removed instantly. This site reflects personal opinions, not universal truths. If you can’t distinguish between the two, this probably isn’t the place for you. The system temporarily stores IP addresses and browser user agents for the purposes of spam prevention, moderation, and safeguarding. This data is automatically removed after fourteen days. Your email address is stored so that replies can be sent to your email address.

Comments powered by BalfComment

Feedback 👍
Comments are sent via email to me.