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.
(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.
{
"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.
{
"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.
{
"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.
[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.
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.
topaz lock --root .
topaz check --root . --locked
topaz run --root . --lockedlock 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.
complete:090000000000000001080000000000000005616c6c6f77The 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.
topaz run --root . --locked -- semantic-limitlimit:eval_workThe 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.
| Function | Use |
|---|---|
consumerArtifactBytes | Convert the record to bytes for storage |
consumerArtifactFromBytes | Read stored bytes and validate their format |
inspectConsumerArtifact | Read metadata and identifiers without running the rule |
verifyConsumerArtifact | Check structure, digests, and links to the evaluation parameters |
portableCoreBytes | Extract the included record in Lispex format, when present |
freshReplay | Run 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.
topaz build --root . --locked --release --out-dir ../complete-productOn Linux or macOS, run the generated program.
../complete-product/target/release/program
../complete-product/target/release/program semantic-limitOn Windows, use these commands.
..\complete-product\target\release\program.exe
..\complete-product\target\release\program.exe semantic-limitThe 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
| Route | Behavior |
|---|---|
interpreter | Runs the locked application through topaz run |
native | Runs on the supported native release targets |
raw-web and worker-web | Refused before output files are written |
no-capability | Refused before execution or output files are written |
mcp-empty-component-set | Refused 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.
Related documentation
- Lispex Evaluator and LIT for one CLI request without a package
- Interop Boundaries to choose how to connect an application