Native API Conventions

The shared design rules that give every native module one vocabulary for identifiers, return shapes, failure signals, byte interop, and lifecycle across the whole CLI surface.

Overview

The conventions on this page are followed by every native module: File, Dir, Process, Buffer, RegEx, JSON, Crypto, Random, Hash, System, Path, Time, Console, and the networking surface to come. Module pages stay focused on what each module does and refer here for how it returns values, signals failure, and exchanges bytes. When a native diverges from anything below, the divergence is documented on its own page.

Identifiers & Globals

All natives are registered as uppercase global identifiers at VM startup: File, Dir, Process, Buffer, RegEx, JSON, Crypto, Random, Hash, System, Path, Time, Console, and Pack, plus the standalone print(...) function.

Static (singleton) methods are reached as Module.method(...). Where the module has a create(...) factory, instance methods are reached as inst.method(...) after var inst = Module.create(...).

Path.isAbsolute(p)              // static (singleton) method on the global
var re = RegEx.create(pat)      // factory returns an instance
print(...)

Writes to stdout. The one standalone global function, registered alongside the module globals at VM startup.

Return Shapes

Natives use a small, deliberate vocabulary of return shapes. When more than one shape could fit a method's purpose, the method uses the one furthest down this list that fits. The records below are drawn from across the modules to anchor each shape; each links to its full documentation.

Plain Value

For methods that always succeed given valid arguments. The value comes back directly. There is no wrapper and no null case.

Path.dirname(p)

Returns a plain string. Failure is not part of the contract; the call always succeeds for valid arguments. Full documentation on the Path page.

Time.ticksMsec()

Returns a plain number. Full documentation on the Time page.

Boolean

For a yes/no question, or for “did the operation succeed?” when there is no further detail to report.

Path.isAbsolute(p)

Returns a boolean answering a yes/no question about its argument. Full documentation on the Path page.

Crypto.verify(...)

Returns a boolean: did the operation succeed, with no further detail to report. Full documentation on the Crypto page.

Dir.exists(p)

Returns a boolean answering a yes/no question. Full documentation on the Dir page.

Value or Null

For operations that may fail where the only useful information on failure is that it failed. null is the documented failure indicator; success returns the typed value.

File.open(...)

Returns a file handle on success, null on failure. Full documentation on the File page.

JSON.parse(text)

Returns the parsed value on success, null when the text does not parse. Full documentation on the JSON page.

RegEx.create(pat)

Returns a compiled regex instance, or null when compilation fails. Full documentation on the RegEx page.

var re = RegEx.create(pat)
if (re == null) {
    print("pattern did not compile")   // failure is data, not an exception
}

Status String

For operations with more than two outcomes that the caller is expected to branch on. The value is a short lowercase string drawn from the shared vocabulary below.

Process.spawn(...)

Returns a result map whose status field is a status string, such as "ok" or "spawn_failed". The Process page documents the exact subset it produces.

Result Map

For multiple values that travel together: data plus metadata.

Process.exec(...)

Returns { exitCode, stdout, stderr }, an exit code alongside the captured output. stdout and stderr are Buffers, so binary child output round-trips losslessly; call .toString() on either when text is wanted. Full documentation on the Process page.

Runtime Error

Reserved for calls that are structurally invalid: wrong argument types, or a call like Path.dirname(42). All natives raise typed runtime errors of the form Module.method(args) expects a <type>. See Errors: Raised vs. Returned.

Status-String Vocabulary

String statuses are used today by Process.spawn and reserved for the upcoming networking natives. Wherever a native returns one, the values are drawn from one shared vocabulary, so scripts can match against consistent strings across modules.

StringMeaning
"ok"The operation completed successfully.
"busy"The operation cannot make progress right now; retry is appropriate (non-blocking I/O).
"timeout"A bounded wait elapsed before the operation could complete.
"eof"A reader observed end-of-stream (peer closed cleanly, file finished).
"closed"The handle is no longer valid for the requested operation.
"error"Something went wrong that fits none of the other categories and cannot be retried.
"not_found"The named target does not exist (lookup miss, missing key, file, or host).
"denied"The operation is refused for a permissions or authorization reason.

A native that returns one of these strings documents which subset it actually produces. Scripts should match on string equality with switch and treat unknown values defensively.

Errors: Raised vs. Returned

Programming bugs, such as a wrong type, a missing argument, or a method called on the wrong kind of value, raise a Zym runtime error with a message of the form Module.method(args) .... A runtime error unwinds the stack and stops the script unless caught.

Recoverable, expected failures come back through one of the return shapes above: null, a status string, or a result map. That covers file not found, a parse error, network busy, and a peer closing the connection. They never raise.

Consequence: a script that handles null and status-string returns needs no exception handling around native calls; only programmer mistakes propagate as errors.

Bytes & String Interop

Buffer is the byte-interop currency between every native that talks about raw bytes: File, Process, Crypto, Hash, Buffer itself, and the networking natives to come.

File.read(n)

Returns a Buffer, not a string. Full documentation on the File page.

Crypto.sign(...)

Returns a Buffer. The script encodes it to hex or base64 itself when it needs an over-the-wire representation. Full documentation on the Crypto page.

Hash.digest(...)

Returns a Buffer. The script encodes it to hex or base64 itself when it needs an over-the-wire representation. Full documentation on the Hash page.

Buffer.compress(algo, level?)

Works Buffer → Buffer: the compressed bytes come back as a new Buffer. Full documentation on the Buffer page.

Buffer.decompress(algo, max)

Works Buffer → Buffer: the decompressed bytes come back as a new Buffer. Full documentation on the Buffer page.

Buffer.toString()

Decodes the buffer contents as UTF-8 and returns the string. Invalid sequences are replaced rather than raising.

Process.exec follows the same rule: stdout and stderr arrive as Buffers so binary child output round-trips losslessly, and the script calls .toString() when it wants text. The upcoming networking natives consume and produce Buffer for every payload; status and metadata travel as separate fields in the result map.

Numbers

Zym numbers are double-precision floats. Native methods that conceptually return integers, such as counts, timestamps, lengths, exit codes, and file sizes, still return them as number. Values up to 253 round-trip exactly; beyond that the usual IEEE-754 caveats apply (nanosecond timestamps near 2106 lose low bits).

Where a native exposes underlying 64-bit state that cannot round-trip through a double, such as the full PCG state or full PCRE2 group pointers, it documents the precision caveat on its own page; Random does this.

Strings

All string-returning methods produce UTF-8. Where the underlying platform call returns a wide-character string, as with Windows path APIs and the Win32 console, the native transcodes before returning.

All string-accepting methods interpret their input as UTF-8. ASCII inputs are a strict subset and always work.

Methods that compare strings do so byte-for-byte unless the doc explicitly says otherwise. RegEx's (?i) flag and System's case-insensitive systemDir name lookup are the documented exceptions.

Lifecycle & Aliasing

Natives that own an external resource expose a create / open / compile factory and a close (or an implicit GC finalizer) for cleanup. This applies to File handles, Process handles, Dir iterators, RegEx instances, Crypto* instances, and network sockets.

Multiple Zym variables bound to the same instance share the same underlying handle. Calling close() on one variable invalidates the other; subsequent calls on either return the documented “closed” shape.

Closed shape: after close(), value-returning methods return null and status-string methods return "closed". This holds through every alias of the handle.

A native that does not own external resources, such as Path, JSON, Hash digest one-shots, Time static methods, or System static methods, is purely functional and has no lifecycle.

Engine Errors

When a native delegates to engine code that itself prints to stderr on internal failure, those messages can appear in the form:

ERROR: <text>
   at: <function> (<file>:<line>)

Whether they appear is controlled by the build-time USE_ENGINE_ERRORS toggle (default ON). Building with USE_ENGINE_ERRORS=OFF silences them globally; the native's own return value is unchanged either way, whether that is null, a status string, or a result map.

What Is Not a Convention

What the native surface refuses to adopt is as deliberate as what it includes.

No exceptions. Native code never raises a typed exception; it either returns a value or runs to completion. Only programmer mistakes raise a runtime error that unwinds the script.

No callbacks from native into script. No native takes a Zym closure as a “called when done” argument. Zym is single-threaded and natives complete synchronously; long-running operations that need progress expose a polling or iteration interface instead: the Dir.list enumerator today, a socket poll() in the networking natives to come.

No promises or async values. Where blocking would be unwise, natives expose timeouts directly.

System.sleep(ms)

Sleeps for ms milliseconds. Timeouts and waits are expressed as direct arguments like this, never as promises or callbacks; a future socket.read(n, timeoutMs) follows the same pattern. Full documentation on the System page.

No global mutable state on the native side. Every native is either purely functional or owns its state behind a handle. The documented exceptions are the two setEnv methods below, which mutate the process environment by definition, and the global print(...), which writes to stdout.

System.setEnv(...)

Mutates the process environment. With Process.setEnv, it is one of the documented exceptions to the no-global-mutable-state rule. Full documentation on the System page.

Process.setEnv(...)

Mutates the process environment. With System.setEnv, it is one of the documented exceptions to the no-global-mutable-state rule. Full documentation on the Process page.

New-Native Checklist

Adding a new native? Follow these rules and it will match the existing surface.

  1. Register the global as a single uppercase identifier.
  2. Use plain values where the call always succeeds.
  3. Use null to signal an expected failure where the only useful information is that it failed.
  4. Use a status string from the shared vocabulary when the script must distinguish more than two outcomes.
  5. Use a result map for multi-value returns.
  6. Raise a runtime error only for argument-shape problems.
  7. Speak Buffer for bytes, UTF-8 strings for text.
  8. Document any deviation on the native's own page.

Examples

Branching Without Exceptions

func loadAndRun(text, cmd) {
    // Value-or-null: the only news is that parsing failed
    var config = JSON.parse(text)
    if (config == null) {
        print("config is not valid JSON")
        return false
    }

    // Status string: branch on the documented vocabulary
    var r = Process.spawn(cmd)
    switch (r.status) {
        case "ok":
            return true
        case "spawn_failed":
            print("could not start the child")
            return false
        default:
            print("unexpected status")   // treat unknown values defensively
            return false
    }
}

Bytes at the Boundary

func runTool(cmd) {
    var out = Process.exec(cmd)          // { exitCode, stdout, stderr }
    if (out.exitCode != 0) {
        print(out.stderr.toString())     // stderr is a Buffer too
        return null
    }
    return out.stdout                    // Buffer: decode only when text is wanted
}

var raw = runTool(cmd)
if (raw != null) {
    print(raw.toString())                // UTF-8; invalid sequences are replaced
}