Browse documentation
Docs/Language

Petri Nets

A NET consumes and produces several tokens in one semantic operation. One firing transforms one immutable marking; a definition does not run, and a query for enabled transitions reserves…

Petri nets

A NET consumes and produces several tokens in one semantic operation. One firing transforms one immutable marking; a definition does not run, and a query for enabled transitions reserves nothing.

assembly = NET
  PLACE parts INITIAL 12 CAPACITY 20
  PLACE machine INITIAL 1 CAPACITY 1
  PLACE completed INITIAL 0 CAPACITY 20

  TRANSITION assemble
    CONSUME parts: 2, machine: 1
    PRODUCE completed: 1, machine: 1
  END
END

initial = assembly.INITIAL()
attempt = assembly.FIRE(initial, :assemble)

After one successful firing, parts is 10, machine is 1 and completed is 1. Six consecutive firings succeed; the seventh is disabled for lack of parts and leaves the marking untouched. The machine token must be present before firing even though the same firing returns it.

NET assembly … END is the equivalent declaration spelling. A parameterized net, name = NET(tools) … END, evaluates its initial counts and capacities against the arguments of INITIAL. The definition binds INITIAL, FIRE, ENABLED, TOKENS and REPORT members: no clock, scheduler or implicit chooser.

Imported ordinary PT nets

A static IMPORT pnml"…" clause may replace the authored body. It compiles through the same native count NET owner; all existing operations below keep their semantics. The imported definition cannot mix parameters or authored clauses. The bounded import contract records accepted IDs, references, counts and refusal boundaries. See the imported delivery example.

Imported BPMN control flow

The explicit BPMN importer maps one bounded acyclic process with plain tasks and parallel gateways to ordinary NET places and transitions. It adds source-linked task begin/complete steps and explicit instance cancellation. It creates no task worker, scheduler or external action; wider BPMN event, condition and subprocess semantics remain separate profiles.

A separate DMN routing profile admits one exclusive split/merge pair with a frozen decision receipt and bounded position encoding.

Places and transitions

Declaration Meaning
PLACE name INITIAL expression [CAPACITY expression] Count place; exact nonnegative integer initial count and optional capacity
TRANSITION name … END One atomic consume/read/guard/produce contract
CONSUME place: count, … Tokens removed by the firing; duplicate entries add
READ place: count, … Availability thresholds that are not consumed; the strongest threshold applies
WHEN boolean Guard evaluated once after availability; an error is an error, not false
PRODUCE place: count, … Tokens added by the firing

Count expressions may use the definition parameters and marking.<place> counts from the pre-marking. Counts are exact integers in the safe numeric range; fractional, negative or nonfinite values are errors (NET_INVALID_MULTIPLICITY). Missing CAPACITY means no modeled bound.

Firing

FIRE(marking, :transition) evaluates, in order: multiplicities, availability (count >= MAX(consumed, read)), the guard, outputs, then post-capacity. It returns OK({next, transition, firing_id}) with the complete proposed marking, or ERR(reason) where reason.kind is :disabled (shortage, false guard, capacity) or :error (invalid multiplicity, failed expression, overflow, wrong definition, unknown transition). reason.phase names the step. No partial consumption or production survives rejection, and a same-firing output never satisfies an input shortage. A marking from another definition is refused (NET_DEFINITION_MISMATCH). Restored or edited markings are validated before any transition expression: record shapes, exact nonnegative counts, parameter names, declared capacities, occupancy and the firing counter must agree. Copying a definition ID does not validate a marking. Invalid contents return NET_INVALID_MARKING; exhausting the exact firing-counter range returns NET_COUNT_OVERFLOW.

ENABLED(marking, options) tries every transition against the same marking and returns OK({bindings, examined, complete, reason, cursor}); an optional {transition: :name} narrows the search. An expression error in any transition is reported, never hidden as deadlock. REPORT(marking, :transition) performs one firing evaluation and returns grid.net.report.v1 with status :fired, :disabled or :error and the same outcome FIRE would return. checks lists only the checks attempted, in execution order, with passed/failed status. proposed_changes contains validated partial consumption/production counts, labeled :proposed; a failed firing returns no committed marking. The changes list describes a successful proposed next marking. Neither list commits shared state.

A marking is ordinary immutable data. Committing its effects to shared state is the job of an existing owner (a STATE cell, a rule, a durable table); FIRE itself has no network, storage or clock.

Typed occurrences and selection

A typed place retains distinct token occurrences, even when their payloads are equal. TOKENS(marking, :place) returns occurrence references containing the payload and its identity. Use those references to select exactly which job and part a firing consumes:

repair = NET(jobs, parts)
  PLACE waiting OF {id: string, sku: string} INITIAL jobs
  PLACE stock OF {id: string, sku: string} INITIAL parts
  PLACE active OF {job_id: string, part_id: string} INITIAL VECTOR()
  PLACE benches INITIAL 1 CAPACITY 1

  TRANSITION start
    TAKE job FROM waiting
    TAKE part FROM stock
    CONSUME benches: 1
    WHEN job.sku = part.sku
    PRODUCE active: {job_id: job.id, part_id: part.id}
  END
END

FIRE(marking, :start, {job: job_reference, part: part_reference}) checks all references against that marking before evaluating the guard. A copied payload is not an occurrence reference. Consumed references cannot be selected again; a stale selection or a reference to a different place/definition is refused. The payload names job and part are available only inside the transition.

TAKE items COUNT expression FROM place [MATCH pattern] selects a collection of distinct occurrences. The count may depend on an earlier binding, such as TAKE order FROM waiting followed by TAKE parts COUNT order.quantity FROM stock. READ items [COUNT expression] FROM place [MATCH pattern] binds without consuming; reads may share occurrences. Separate TAKE bindings may not consume the same occurrence. PRODUCE place: EACH values produces one occurrence per value; without EACH it produces one typed payload. Outputs must satisfy the place type and post-capacity before any change succeeds.

ENABLED returns proposed, snapshot-bound selections. limit bounds returned selections; work bounds candidate exploration. Pass its returned cursor to continue the same search on the same marking and transition scope. A bounded page with complete: FALSE is not proof of deadlock. Equal payloads still give different occurrence choices. Expression errors remain errors rather than being silently discarded as disabled candidates.

Subnets and live ownership

A subnet declares PORT INPUT, PORT OUTPUT or PORT SHARED places, with OF type for typed ports. INPUT allows consumption/read, OUTPUT allows production, and SHARED permits both. Instantiate it inside a parent with:

USE first = worker(1) PLACES {pool: benches, waiting: waiting, done: completed}

The mappings alias the parent's places; they do not copy inventories. Private places and transitions are qualified by the instance name. For example, FIRE(marking, "first.start", selection) selects the child's transition. A template with unmapped ports cannot be initialized as a standalone net.

Live applications explicitly select the Host NET owner. It freezes the compiled definition and initial arguments, then commits one proposed firing and its operation receipt in the existing model database transaction. A stale expected revision fails without consumption; an identical operation retry returns its committed result. Reopening reuses the committed marking. Changed definitions or admission arguments cannot silently reinterpret that state. Formula calls remain pure and never select this owner.

The repair-shop example selects one compatible job, part and bench. The kitting schedule uses an imported decision and maps accepted packed payloads to explicit simulation arrivals. Rejected firings produce no arrival, and the simulation owns its resource pool separately. Durable Frame publication remains an explicit owner operation.

Scope

Count and typed places share atomic firing, bounded search and reports. REPORT retains the actual attempt's checks and per-place deltas without evaluating the firing a second time. It retains at most 4,096 checks and 8,192 occurrence IDs; omission sets explanation_complete to false while preserving the complete firing outcome. Explanation materialization has an 8 MiB bound. Timing, stochastic firing, reachability proofs and automatic selection policies are separate contracts; they are not implied by a NET reference or a complete enabled-search page.