Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/petrinaut-lambda-place-counts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hashintel/petrinaut-core": patch
---

Allow transition Lambdas to read current place token counts through an optional third `places` argument.
4 changes: 2 additions & 2 deletions libs/@hashintel/petrinaut-core/src/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,10 +266,10 @@ When creating or revising a net:

Validate every code-writing change. After any tool call that writes code — lambda, transition kernel, dynamics, visualizer, metric, or scenario code-mode initial state — call ${getNetCompilationErrorsToolName} before continuing and fix any reported diagnostics before relying on the new code. Do not assume a code edit is correct just because the tool call succeeded; mutations only validate the schema, not the runtime contract.

Place names are part of the code surface: lambdas/kernels read \`input.PlaceName\`, metrics read \`state.places.PlaceName.count\`, and scenario code-mode initial state keys are place names. Renaming a place via \`updatePlace\` requires updating every dependent lambda, kernel, dynamics, metric, visualizer, and scenario in the same batch — otherwise you will silently break references.
Place names are part of the code surface: lambdas/kernels read \`input.PlaceName\`, lambdas may also read \`places.PlaceName.count\` through their optional third argument, metrics read \`state.places.PlaceName.count\`, and scenario code-mode initial state keys are place names. Renaming a place via \`updatePlace\` requires updating every dependent lambda, kernel, dynamics, metric, visualizer, and scenario in the same batch — otherwise you will silently break references.

Code-surface cheatsheet (exact shapes expected by the runtime):
- Transition lambda (\`transition.lambdaCode\`): \`export default Lambda((input, parameters) => …)\`. Available when stochasticity is enabled OR when colours are enabled and the transition has at least one standard or read input arc from a coloured place. \`input.PlaceName\` is a tuple sized to the input arc weight for coloured standard and read input arcs; token attributes are typed by the colour element: real/integer → number, boolean → boolean, uuid → bigint, string → string (plain JS strings everywhere, compared by value). Read arcs expose tokens in \`input\` but do not consume them when the transition fires. Inhibitor arcs and uncoloured input places are NOT in \`input\`. Predicate → boolean; stochastic → non-negative rate in firings per simulation second (0 disables, Infinity always fires). Must be deterministic. If unavailable or empty, the runtime uses true for predicate-style transitions and Infinity for stochastic-style transitions.
- Transition lambda (\`transition.lambdaCode\`): \`export default Lambda((input, parameters) => …)\`. It may take an optional third \`places\` argument; \`places.PlaceName.count\` is the current total token count for any place in the containing net, including uncoloured places. Available when stochasticity is enabled OR when colours are enabled and the transition has at least one standard or read input arc from a coloured place. \`input.PlaceName\` is a tuple sized to the input arc weight for coloured standard and read input arcs; token attributes are typed by the colour element: real/integer → number, boolean → boolean, uuid → bigint, string → string (plain JS strings everywhere, compared by value). Read arcs expose tokens in \`input\` but do not consume them when the transition fires. Inhibitor arcs and uncoloured input places are NOT in \`input\`. Predicate → boolean; stochastic → non-negative rate in firings per simulation second (0 disables, Infinity always fires). Must be deterministic. If unavailable or empty, the runtime uses true for predicate-style transitions and Infinity for stochastic-style transitions.
- Transition kernel (\`transition.transitionKernelCode\`): \`export default TransitionKernel((input, parameters) => …)\`. Available only for transitions with coloured output places. Return \`{ OutputPlaceName: [token, …] }\` sized to the output arc weight. Include only coloured output places; uncoloured output places are auto-populated. Output values must match element types: real/integer use numbers, boolean uses booleans, string uses plain strings (REQUIRED in the token type; a missing/undefined value becomes the empty string \`""\`, and non-string values are stringified via \`String(value)\`). uuid attributes are OPTIONAL in output tokens: omit them to auto-generate a fresh UUID deterministically from the seeded simulation RNG, use \`Uuid.generate()\` for an explicit fresh UUID, \`Uuid.from(value)\` to derive one from a number or arbitrary string, use a UUID string directly, or forward an input token's uuid bigint unchanged. Plain non-UUID values must be wrapped in \`Uuid.from(...)\`; supplying them directly is a type error. When stochasticity is enabled, real attributes may use \`Distribution.Gaussian(mean, sd)\` / \`Distribution.Uniform(min, max)\` / \`Distribution.Lognormal(mu, sigma)\` (never integer/boolean/uuid/string attributes), and chained \`.map(fn)\` on the same distribution shares one draw. When stochasticity is disabled, kernel outputs must use plain values only. Leave empty when no coloured outputs exist.
- Differential equation (\`differentialEquation.code\`): \`export default Dynamics((tokens, parameters) => …)\`. \`tokens\` is THIS place's tokens only. Return an array of the same length whose entries provide derivatives for real-valued elements only (i.e. dx/dt, not the new value); integer, boolean, uuid, and string elements are discrete and remain unchanged by dynamics (they can be read from input tokens but never written). The equation's \`colorId\` MUST match every referencing place's \`colorId\`.
- Place visualizer (\`place.visualizerCode\`): \`export default Visualization(({ tokens, parameters }) => <JSX/>)\`. Classic React runtime — do NOT import React, do NOT use \`<>…</>\` fragments, do NOT use hooks. Convention: return a sized \`<svg viewBox="0 0 W H">…</svg>\`.
Expand Down
13 changes: 11 additions & 2 deletions libs/@hashintel/petrinaut-core/src/hir/BUFFER_ABI.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ The emitter and engine use the same input slot order:
Runtime arguments:

```ts
(f64, u64, u8, placeBases, indices, placeCounts) => number | boolean;

placeBases: Int32Array; // one byte offset per colored input arc
indices: Int32Array; // one selected token index per input token slot
placeCounts: Uint32Array; // dense counts for every place in the frame
```

An attribute read compiles to:
Expand All @@ -51,6 +54,12 @@ read view at base + fieldByteOffset
Dynamic indexes into transition input tokens are rejected. Static `.map(...)`
over transition token tuples is unrolled.

The optional third Lambda parameter exposes count-only place states. A read
such as `places.Queue.count` compiles directly to
`placeCounts[__places[ordinal]]`; `__places` is resolved once when the
artifact is instantiated. `HirLambdaArtifact.placeIds` stores the referenced
net-local place IDs in ordinal order, including for subnet instances.

## Transition outputs

Kernel staging is a reusable `Uint8Array`.
Expand Down Expand Up @@ -115,13 +124,13 @@ compile to loops over `placeCounts` and `placeOffsets`.

## Artifact validation

Artifacts are `version: 4` and carry a fingerprint of the sanitized SDCPN and
Artifacts are `version: 5` and carry a fingerprint of the sanitized SDCPN and
extension settings used for compilation.

The engine rejects stale artifacts by checking:

- artifact version and compilation-input fingerprint;
- lambda `inputSlotCount`;
- lambda `inputSlotCount` and resolvable `placeIds`;
- kernel `inputSlotCount`;
- kernel `outputByteCount`;
- metric `placeNames` resolution.
Expand Down
9 changes: 7 additions & 2 deletions libs/@hashintel/petrinaut-core/src/hir/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,14 @@ outside the supported HIR subset is a blocking diagnostic.

```ts
{
version: 4,
version: 5,
fingerprint: string,
dynamics: Record<string, { source: string }>,
lambdas: Record<string, { source: string; inputSlotCount: number }>,
lambdas: Record<string, {
source: string;
inputSlotCount: number;
placeIds: string[];
}>,
kernels: Record<string, {
source: string;
inputSlotCount: number;
Expand All @@ -102,6 +106,7 @@ The emitted programs use shared views over packed token bytes:

- `f64`, `u64`, `u8` token-region views;
- `placeBases` and `indices` for transition input selections;
- `placeCounts` for direct count reads in transition Lambdas;
- output staging bytes for kernels;
- `placeCounts` and `placeOffsets` for metrics;
- a per-run string pool;
Expand Down
20 changes: 14 additions & 6 deletions libs/@hashintel/petrinaut-core/src/hir/artifacts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ function packToken(token: TokenRecord, pool: StringPool) {
}

describe("compileHirArtifacts", () => {
it("emits stable v4 artifact shapes", () => {
it("emits stable v5 artifact shapes", () => {
const artifacts = compile();

expect({
Expand Down Expand Up @@ -187,7 +187,8 @@ describe("compileHirArtifacts", () => {
},
"lambda": {
"inputSlotCount": 1,
"source": "(f64, u64, u8, placeBases, indices) => {
"placeIds": [],
"source": "(f64, u64, u8, placeBases, indices, placeCounts) => {
return ((u8[placeBases[0] + indices[0] * 24 + 16] !== 0) && (f64[(placeBases[0] + indices[0] * 24) >> 3] >= __params["threshold"]));
}",
},
Expand All @@ -205,7 +206,7 @@ describe("compileHirArtifacts", () => {
return count;
}",
},
"version": 4,
"version": 5,
}
`);
});
Expand All @@ -229,9 +230,16 @@ describe("compileHirArtifacts", () => {
{ x: 4, active: false, status: "queued" },
]) {
const views = packToken(token, pool);
expect(buffer(views.f64, views.u64, views.u8, placeBases, indices)).toBe(
reference({ Source: [token] }, parameters),
);
expect(
buffer(
views.f64,
views.u64,
views.u8,
placeBases,
indices,
new Uint32Array(),
),
).toBe(reference({ Source: [token] }, parameters));
}
});
});
3 changes: 2 additions & 1 deletion libs/@hashintel/petrinaut-core/src/hir/compile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe("compileHirArtifacts on example models", () => {
(_name, sdcpn) => {
const { artifacts, failures } = compileHirArtifacts(sdcpn);
expect(failures).toEqual([]);
expect(artifacts.version).toBe(4);
expect(artifacts.version).toBe(5);
expect(artifacts.fingerprint).toMatch(/^[0-9a-f]{16}$/);

const colorById = new Map(
Expand Down Expand Up @@ -113,6 +113,7 @@ describe("compileHirArtifacts on example models", () => {
for (const artifact of Object.values(artifacts.lambdas)) {
expect(typeof artifact.source).toBe("string");
expect(typeof artifact.inputSlotCount).toBe("number");
expect(Array.isArray(artifact.placeIds)).toBe(true);
}
for (const artifact of Object.values(artifacts.kernels)) {
expect(typeof artifact.source).toBe("string");
Expand Down
3 changes: 2 additions & 1 deletion libs/@hashintel/petrinaut-core/src/hir/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export function compileHirArtifacts(
): HirCompileResult {
const sanitized = sanitizeSDCPNForExtensions(sdcpn, extensions);
const artifacts: HirArtifacts = {
version: 4,
version: 5,
fingerprint: fingerprintHirCompilationInput(sanitized, extensions),
dynamics: {},
lambdas: {},
Expand Down Expand Up @@ -199,6 +199,7 @@ export function compileHirArtifacts(
artifacts.lambdas[transition.id] = {
source: program.source,
inputSlotCount: program.inputSlotCount,
placeIds: program.placeIds,
};
}
}
Expand Down
81 changes: 75 additions & 6 deletions libs/@hashintel/petrinaut-core/src/hir/emit-buffer-js.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ const lambdaContext: HirLambdaContext = {
],
inputPlaces: [(({ slotStart: _s, ...binding }) => binding)(poolSlot)],
inputSlots: [poolSlot],
places: [
{ id: "pool", name: "Pool" },
{ id: "buffer", name: "Buffer" },
],
lambdaType: "stochastic",
};

Expand Down Expand Up @@ -111,16 +115,54 @@ function compileLambda(code: string, pool: StringPool, parameters = {}) {
}

describe("emitBufferLambdaJs (token format v2)", () => {
it("reads place counts through the optional third parameter", () => {
const pool = new StringPool();
const program = emitBufferLambdaJs(
lower(
`export default Lambda((input, parameters, places) => places.Pool.count + places.Buffer.count);`,
"lambda",
),
lambdaContext,
);
expect(program).not.toBeNull();
expect(program!.placeIds).toEqual(["pool", "buffer"]);
const fn = instantiateHirBufferLambda(
program!.source,
{},
pool,
new Int32Array([1, 0]),
);
const { views, placeBases, indices } = makeRegion(pool);

expect(
fn(
views.f64,
views.u64,
views.u8,
placeBases,
indices,
new Uint32Array([4, 7]),
),
).toBe(11);
});

it("reads real/boolean attributes at packed byte offsets", () => {
const pool = new StringPool();
const { views, placeBases, indices } = makeRegion(pool);
const fn = compileLambda(
`export default Lambda((input, parameters) => input.Pool[0].alive ? input.Pool[0].x + input.Pool[1].v : 0);`,
pool,
);
expect(fn(views.f64, views.u64, views.u8, placeBases, indices)).toBe(
1.5 + 8,
);
expect(
fn(
views.f64,
views.u64,
views.u8,
placeBases,
indices,
new Uint32Array(),
),
).toBe(1.5 + 8);
});

it("resolves interned strings through the pool", () => {
Expand All @@ -130,7 +172,16 @@ describe("emitBufferLambdaJs (token format v2)", () => {
`export default Lambda((input, parameters) => input.Pool[0].status === "shipped" && input.Pool[1].status.startsWith("q"));`,
pool,
);
expect(fn(views.f64, views.u64, views.u8, placeBases, indices)).toBe(true);
expect(
fn(
views.f64,
views.u64,
views.u8,
placeBases,
indices,
new Uint32Array(),
),
).toBe(true);
});

it("assembles uuid attributes as bigints from the two u64 lanes", () => {
Expand All @@ -140,7 +191,16 @@ describe("emitBufferLambdaJs (token format v2)", () => {
`export default Lambda((input, parameters) => input.Pool[0].id === input.Pool[1].id ? 1 : 0.5);`,
pool,
);
expect(fn(views.f64, views.u64, views.u8, placeBases, indices)).toBe(0.5);
expect(
fn(
views.f64,
views.u64,
views.u8,
placeBases,
indices,
new Uint32Array(),
),
).toBe(0.5);
void uuid;
});

Expand All @@ -158,7 +218,16 @@ describe("emitBufferLambdaJs (token format v2)", () => {
pool,
{ rate: 2, threshold: 1 },
);
expect(fn(views.f64, views.u64, views.u8, placeBases, indices)).toBe(3);
expect(
fn(
views.f64,
views.u64,
views.u8,
placeBases,
indices,
new Uint32Array(),
),
).toBe(3);
});
});

Expand Down
Loading
Loading