Build Applications

Run Lispex Rules

Create a complete rule package, call it from Topaz, and handle a resource limit.

Call a Lispex rule from a Topaz application and handle its result when the work limit runs out. You will create six files and run the same rule with two limits.

Install Topaz and follow First Application if packages are new to you. Topaz includes the evaluator, so you do not need a separate Lispex installation. This integration uses Lispex 1.20.0.

A rule cannot read files, use the system clock, or access the network. Topaz prepares it once and starts each evaluation with fresh memory, globals, resource counters, and an execution log.

Create the rule and its limits

Create a folder named lispex-first-call with rules and src folders inside it. Save the following six files there. Run every command below from lispex-first-call.

Save this rule as rules/approve.lspx.

LISPEX
(if (< 10 15) "allow" "deny")

The rule compares the constants 10 and 15 and returns "allow". It ignores the input value. Keeping the decision fixed lets you compare a completed evaluation with a resource limit.

Save the complete limits file as rules/approve.limits.json.

JSON
{
  "schema": "topaz.lispex-embed-limits/v1",
  "prepare": {
    "raw_source_bytes": 4096,
    "prepare_work": 1000000,
    "logical_allocation": 1000000,
    "syntax_depth": 64
  },
  "evaluate": {
    "canonical_input_bytes": 4096,
    "eval_work": 10000,
    "logical_allocation": 1000000,
    "semantic_frames": 1000,
    "traversal_depth": 256,
    "output_bytes": 4096,
    "diagnostic_bytes": 4096,
    "transcript_bytes": 4096,
    "transcript_events": 100,
    "result_bytes": 4096
  }
}

Save the second limits file as rules/semantic-limit-probe.limits.json.

JSON
{
  "schema": "topaz.lispex-embed-limits/v1",
  "prepare": {
    "raw_source_bytes": 4096,
    "prepare_work": 1000000,
    "logical_allocation": 1000000,
    "syntax_depth": 64
  },
  "evaluate": {
    "canonical_input_bytes": 4096,
    "eval_work": 1,
    "logical_allocation": 1000000,
    "semantic_frames": 1000,
    "traversal_depth": 256,
    "output_bytes": 4096,
    "diagnostic_bytes": 4096,
    "transcript_bytes": 4096,
    "transcript_events": 100,
    "result_bytes": 4096
  }
}

The second file sets eval_work to 1. That is too little work to finish this rule. Keep every field in both files. You can lower the values, but adding or omitting fields makes the document invalid.

prepare_work counts work while preparing the rule. eval_work counts work while evaluating it. These counters follow the evaluator's rules, so the same rule, input, and evaluator settings use the same work units. Elapsed time is controlled separately.

Set application quotas

Save this as rules/application.quotas.json.

JSON
{
  "schema": "topaz.lispex-application-quotas/v1",
  "concurrent_evaluations": 2,
  "queued_evaluations": 2,
  "total_evaluations": 64,
  "aggregate_input_bytes": 262144,
  "aggregate_result_bytes": 262144,
  "aggregate_output_bytes": 262144,
  "aggregate_transcript_bytes": 262144,
  "aggregate_safety_fuel": 64000000000,
  "prepared_bytes": 3000000,
  "wall_millis": 5000
}

Quotas apply across the application. This file allows two concurrent evaluations, two waiting evaluations, and 64 evaluations in total. wall_millis gives an evaluation 5,000 milliseconds. The byte fields cap the combined input, results, output, and execution logs.

Declare the package

Save this as topaz.toml.

TOML
[package]
name = "complete_lispex_decision_application"
version = "0.1.0"
language = "5.20"
entry = "src/main.tpz"

[build]
target = "native"
deterministic = true

[dependencies]
std = "5.20"

[lispex]
profile = "lispex/r7rs-rule-current-profile-bounded/1"
application = "topaz/lispex-decision-application/2"
application_quotas = "rules/application.quotas.json"

[[lispex.rule]]
name = "approve"
source = "rules/approve.lspx"
limits = "rules/approve.limits.json"

[[lispex.rule]]
name = "semantic_limit_probe"
source = "rules/approve.lspx"
limits = "rules/semantic-limit-probe.limits.json"

Both rule names use the same source. approve gets the normal limits, while semantic_limit_probe gets the work limit of 1.

Keep language and std at 5.20 for this application contract. Keep the profile and application identifiers exactly as shown. A profile names the supported language features and execution rules. Here, bounded is part of the profile identifier and refers to its resource limits.

Each [[lispex.rule]] entry supplies a name, source, and limits path. Topaz makes the declared names available as functions in std.lispex.rules, such as rules.approve().

Call the rule

Save this complete program as src/main.tpz.

TOPAZ
import std.lispex { canonicalBytes, defaultLimits, evaluate, valueFromCanonical }
import std.lispex.rules as rules

export function main(args: Array<string>, stdin: string) -> Result<int, string> {
    let bytes = Bytes.fromArray([0])?
    let input = match valueFromCanonical(bytes) {
        case Ok(value) => value
        case Err(_) => return Err("invalid input")
    }
    let scenario = if args.length == 0 { "approve" } else { args[0] }
    let rule = if scenario == "approve" {
        rules.approve()
    } else if scenario == "semantic-limit" {
        rules.semantic_limit_probe()
    } else {
        return Err("use approve or semantic-limit")
    }
    match evaluate(rule, input, defaultLimits(rule)) {
        case Ok(settlement) => match settlement {
            case Complete(value) => print("complete:{canonicalBytes(value).toHex()}")
            case LimitExhaustion(reason) => print("limit:{reason}")
            case SemanticFailure(reason) => return Err("rule failed: {reason}")
            case RequestRefusal(reason) => return Err("request refused: {reason}")
        }
        case Err(fault) => match fault {
            case DeadlineExceeded => return Err("application deadline exceeded")
            case TotalEvaluationsExceeded => return Err("application evaluation quota exceeded")
            case _ => return Err("operational fault")
        }
    }
    Ok(0)
}

Bytes.fromArray([0]) creates the one-byte encoding of the empty Lispex value, nil. valueFromCanonical checks that these bytes follow lispex.embed-value/v1, the shared value format. The program supplies that value as input.

defaultLimits reads the rule's declared evaluation limits. evaluate returns either a LispexSettlement or an operational fault. A settlement describes completion, a rule failure, a refused request, or a resource limit. The program handles the work limit as a result and reports application quotas and deadlines as errors.

Lock, check, and run

From the folder containing topaz.toml, run these commands.

BASH
topaz lock --root .
topaz check --root . --locked
topaz run --root . --locked

lock writes topaz.lock and prepares the declared rules under .topaz/lispex. check succeeds with exit code 0 and no output. run prints this line and exits with code 0.

Output
complete:090000000000000001080000000000000005616c6c6f77

The program prints the result's canonical bytes as hexadecimal. They encode a list containing the string "allow". Complete means the evaluator finished the rule.

Now select the rule with the smaller work limit.

BASH
topaz run --root . --locked -- semantic-limit
Output
limit:eval_work

The program exits with code 0 because it handled LimitExhaustion. The rule stopped at its eval_work limit. To let it finish, change that field in rules/semantic-limit-probe.limits.json from 1 to 10000, then repeat the lock, check, and run commands. The result becomes the complete line above.

For the remaining examples, restore eval_work to 1 in that file and run topaz lock --root . again. The lock file is updated, and semantic-limit prints limit:eval_work again.

Review topaz.lock before committing it. It records the evaluator, profile, encoding, limits, quotas, rule sources, prepared rules, and their digests. A digest identifies the exact bytes. Let topaz lock write these values.

After changing the manifest, rules, limits, quotas, evaluator, or profile, regenerate the lock file. Commands using --locked reject a mismatch. See Packages & Distribution for lock-file handling and Toolchain Status for installation details.

Save and check an evaluation record

Use evaluateWithEvidence when you also need a saved evaluation record. For eligible deterministic outcomes, it returns a consumer artifact, a record produced by the calling application.

FunctionUse
consumerArtifactBytesConvert the record to bytes for storage
consumerArtifactFromBytesRead stored bytes and validate their format
inspectConsumerArtifactRead metadata and identifiers without running the rule
verifyConsumerArtifactCheck structure, digests, and links to the evaluation parameters
portableCoreBytesExtract the included record in Lispex format, when present
freshReplayRun the recorded rule and input with the recorded limits in a fresh instance and compare the resulting artifact

The record and its Lispex-format core are unsigned, unauthenticated data created by the caller. They identify no issuer and grant no provider approval, signing authority, component approval, or permission to act on external systems. Verify the record's contents and authorize external actions separately.

Local request refusal, cancellation, a full queue, a protective stop, and an engine failure produce no portable core. freshReplay succeeds when the new artifact matches the saved one.

The maintained sample at docs/samples/guides/lispex-application/first-call contains the six files above. Its parent sample also exercises repeated evaluations, record storage, verification, replay, state isolation, and the total evaluation quota. Its all scenario stops after the 65th call is refused. The first-call program here accepts approve and semantic-limit.

Build the native product

With the Rust build tools installed, create a release build outside the package folder.

BASH
topaz build --root . --locked --release --out-dir ../complete-product

On Linux or macOS, run the generated program.

BASH
../complete-product/target/release/program
../complete-product/target/release/program semantic-limit

On Windows, use these commands.

POWERSHELL
..\complete-product\target\release\program.exe
..\complete-product\target\release\program.exe semantic-limit

The first command prints the complete line, and the second prints limit:eval_work. Both exit with code 0. Run the executable on the operating system and architecture it was built for.

Choose a supported runtime

RouteBehavior
interpreterRuns the locked application through topaz run
nativeRuns on the supported native release targets
raw-web and worker-webRefused before output files are written
no-capabilityRefused before execution or output files are written
mcp-empty-component-setRefused before execution

The native release targets are Windows x64, Linux x64 and arm64, and macOS x64 and arm64. An unsupported route fails without switching to another runtime.

Existing Topaz 5.18 packages use the embedded-core compatibility profile with its own evaluator, profile, contract, and prepared-rule identifiers. The application above uses lispex/r7rs-rule-current-profile-bounded/1. Selecting one profile never substitutes the other.