Skip to content
Open
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
6 changes: 6 additions & 0 deletions .changeset/fe-1121-uuid-token-dimension-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@hashintel/petrinaut": minor
"@hashintel/petrinaut-core": minor
---

Add the `uuid` token dimension type: 128-bit RFC 4122 identifiers represented as `bigint` at runtime, stored as two little-endian 64-bit lanes in frame buffers, and as canonical lowercase strings at rest. Kernel outputs may omit uuid fields (auto-generated deterministically from the seeded simulation RNG) or use the new `Uuid.generate()` / `Uuid.from(value)` helpers; non-UUID inputs convert deterministically via UUIDv5. The initial-state and scenario spreadsheets gain UUID columns.
11 changes: 6 additions & 5 deletions libs/@hashintel/petrinaut-core/docs/architecture/engine.html
Original file line number Diff line number Diff line change
Expand Up @@ -325,11 +325,12 @@ <h2>Format v2 β€” packed token structs (current)</h2>
element type instead of changing <code>integer</code>.</td></tr>
<tr><td><code>boolean</code></td><td><code>u8</code> β€” 1 B</td>
<td>Not bit-packed (would break byte-granular copies for marginal savings).</td></tr>
<tr><td><code>uuid</code> / 128-bit (FE-1121, future)</td><td><code>u64 Γ— 2</code> β€” 16 B</td>
<td>Not implemented yet. Will read via a shared <code>BigUint64Array</code>
(~28 ns combine to one <code>bigint</code>; lane-compare without combining
for equality, ~4Γ— faster). Never route the lanes through
<code>number</code> (NaN-payload hazard).</td></tr>
<tr><td><code>uuid</code> (128-bit, FE-1121)</td><td><code>u64 Γ— 2</code> β€” 16 B</td>
<td>Two little-endian lanes read via the shared <code>BigUint64Array</code>
view, combined to one <code>bigint</code> at the boundary (~28 ns;
lane-compare without combining for equality, ~4Γ— faster). Never routed
through <code>number</code> (NaN-payload hazard). Kernel outputs are
optional β€” omitted values auto-generate from the seeded RNG.</td></tr>
</tbody>
</table>
<ul>
Expand Down
8 changes: 4 additions & 4 deletions libs/@hashintel/petrinaut-core/src/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,12 +269,12 @@ Validate every code-writing change. After any tool call that writes code β€” lam
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.

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. 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. When stochasticity is enabled, real attributes may use \`Distribution.Gaussian(mean, sd)\` / \`Distribution.Uniform(min, max)\` / \`Distribution.Lognormal(mu, sigma)\` (never integer/boolean 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 and boolean elements are discrete and remain unchanged by dynamics. The equation's \`colorId\` MUST match every referencing place's \`colorId\`.
- 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. 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. 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 any value, or forward an input token's uuid bigint unchanged; plain non-UUID values (numbers, arbitrary strings) are converted deterministically via UUIDv5. When stochasticity is enabled, real attributes may use \`Distribution.Gaussian(mean, sd)\` / \`Distribution.Uniform(min, max)\` / \`Distribution.Lognormal(mu, sigma)\` (never integer/boolean/uuid 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, and uuid elements are discrete and remain unchanged by dynamics. 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>\`.
- Metric (\`metric.code\`): a plain function body β€” NOT a module, no \`export default\`, no wrapper. The only variable in scope is \`state\`. Must \`return\` a finite number. Example: \`return state.places.Infected.count / (state.places.Susceptible.count + state.places.Infected.count + state.places.Recovered.count);\`. \`parameters\` and \`scenario\` are NOT available inside metrics.
- Scenario per_place initial state: \`content\` keys are place IDs; uncoloured values are expressions with \`parameters\` and \`scenario\` in scope; coloured values are row arrays in colour element order using numbers and booleans.
- Scenario per_place initial state: \`content\` keys are place IDs; uncoloured values are expressions with \`parameters\` and \`scenario\` in scope; coloured values are row arrays in colour element order using numbers and booleans; uuid columns accept UUID strings (any other text converts deterministically to a UUID via UUIDv5).
- Scenario code-mode initial state: function body returning \`{ PlaceName: tokens }\` keyed by NAME (asymmetric with per_place IDs); unknown names are silently dropped.
- Parameter access in any code surface: use \`parameters.<variableName>\` where \`<variableName>\` is the parameter's lower_snake_case \`variableName\` value (e.g. \`parameters.crash_threshold\`, never \`parameters.crashThreshold\`).

Expand Down
2 changes: 2 additions & 0 deletions libs/@hashintel/petrinaut-core/src/default-codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const defaultTokenAttributeSource = (
case "integer":
case "real":
return "0";
case "uuid":
return "Uuid.generate()";
}
};

Expand Down
8 changes: 8 additions & 0 deletions libs/@hashintel/petrinaut-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,8 +345,16 @@ export {
readTokenRecord,
type PhysicalKind,
type TokenLayoutField,
type TokenRegionViews,
type TokenSlotLayout,
} from "./simulation/engine/token-layout";
export {
formatUuid,
NIL_UUID,
parseUuid,
PETRINAUT_UUID_NAMESPACE,
toUuid,
} from "./simulation/engine/uuid";
export { compileUserCode } from "./simulation/authoring/user-code/compile-user-code";
export {
displayNameSchema,
Expand Down
125 changes: 125 additions & 0 deletions libs/@hashintel/petrinaut-core/src/lsp/lib/checker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -991,6 +991,131 @@ describe("checkSDCPN", () => {
});
});

describe("UUID elements", () => {
const uuidTypes = [
{
id: "color1",
elements: [
{ name: "id", type: "uuid" as const },
{ name: "x", type: "real" as const },
],
},
];
const uuidPlaces = [
{ id: "place1", name: "Source", colorId: "color1" },
{ id: "place2", name: "Target", colorId: "color1" },
];
const uuidKernel = (body: string) =>
createSDCPN({
types: uuidTypes,
places: uuidPlaces,
transitions: [
{
id: "t1",
inputArcs: [{ placeId: "place1", weight: 1, type: "standard" }],
outputArcs: [{ placeId: "place2", weight: 1 }],
transitionKernelCode: `export default TransitionKernel((input, parameters) => {
return { Target: [${body}] };
});`,
},
],
});

it("types input uuid attributes as bigint (=== comparison is valid)", () => {
const sdcpn = createSDCPN({
types: uuidTypes,
places: uuidPlaces,
transitions: [
{
id: "t1",
lambdaType: "predicate",
inputArcs: [{ placeId: "place1", weight: 2, type: "standard" }],
outputArcs: [{ placeId: "place2", weight: 1 }],
lambdaCode: `export default Lambda((input, parameters) => {
return input.Source[0].id === input.Source[1].id;
});`,
transitionKernelCode: `export default TransitionKernel((input, parameters) => {
return { Target: [input.Source[0]] };
});`,
},
],
});

const result = check(sdcpn);

expect(result.isValid).toBe(true);
expect(result.itemDiagnostics).toHaveLength(0);
});

it("rejects arithmetic mixing an input uuid bigint with a number", () => {
const sdcpn = createSDCPN({
types: uuidTypes,
places: uuidPlaces,
transitions: [
{
id: "t1",
lambdaType: "predicate",
inputArcs: [{ placeId: "place1", weight: 1, type: "standard" }],
outputArcs: [{ placeId: "place2", weight: 1 }],
lambdaCode: `export default Lambda((input, parameters) => {
return input.Source[0].id * 2 > 0;
});`,
transitionKernelCode: `export default TransitionKernel((input, parameters) => {
return { Target: [input.Source[0]] };
});`,
},
],
});

const result = check(sdcpn);

expect(result.isValid).toBe(false);
expect(result.itemDiagnostics[0]?.itemType).toBe("transition-lambda");
});

it("accepts kernel outputs with omitted uuid, Uuid.generate(), and strings", () => {
const omitted = check(uuidKernel(`{ x: 1 }`));
expect(omitted.isValid).toBe(true);
expect(omitted.itemDiagnostics).toHaveLength(0);

const generated = check(uuidKernel(`{ id: Uuid.generate(), x: 1 }`));
expect(generated.isValid).toBe(true);
expect(generated.itemDiagnostics).toHaveLength(0);

const fromString = check(uuidKernel(`{ id: "order-1", x: 1 }`));
expect(fromString.isValid).toBe(true);
expect(fromString.itemDiagnostics).toHaveLength(0);

const forwarded = check(
uuidKernel(`{ id: input.Source[0].id, x: input.Source[0].x }`),
);
expect(forwarded.isValid).toBe(true);
expect(forwarded.itemDiagnostics).toHaveLength(0);
});

it("accepts Uuid helpers even when stochasticity is disabled", () => {
const result = check(uuidKernel(`{ id: Uuid.generate(), x: 1 }`), {
colors: true,
stochasticity: false,
dynamics: true,
parameters: true,
subnets: true,
});

expect(result.isValid).toBe(true);
expect(result.itemDiagnostics).toHaveLength(0);
});

it("rejects Distribution values on uuid attributes", () => {
const result = check(
uuidKernel(`{ id: Distribution.Uniform(0, 1), x: 1 }`),
);

expect(result.isValid).toBe(false);
expect(result.itemDiagnostics[0]?.itemType).toBe("transition-kernel");
});
});

describe("Multiple errors", () => {
it("reports errors from multiple items", () => {
// GIVEN - SDCPN with errors in both a differential equation and a transition
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ function toTsType(type: ColorElementType | "ratio"): string {
if (type === "boolean") {
return "boolean";
}
if (type === "uuid") {
return "bigint";
}
return "number";
}

Expand All @@ -59,22 +62,31 @@ function toDynamicsDerivativeType(color: Color): string {
}

/**
* Kernel output token type when stochasticity is enabled. Only continuous
* (`real`) attributes may be produced as a `Distribution`; discrete
* attributes (`integer`, `boolean`) must be plain values β€” the generic
* `Probabilistic<T>` mapped type could not distinguish integer from real
* (both are `number`), so the type is generated per element instead.
* Kernel output token type, generated per element:
*
* - `real` attributes may additionally be produced as a `Distribution` when
* stochasticity is enabled β€” the generic `Probabilistic<T>` mapped type
* could not distinguish integer from real (both are `number`), so the type
* is generated per element instead.
* - `uuid` attributes are OPTIONAL (omitted values are auto-generated from
* the seeded simulation RNG) and also accept UUID strings and the
* `Uuid.generate()` / `Uuid.from(value)` sentinels.
* - Other discrete attributes (`integer`, `boolean`) must be plain values.
*/
function toStochasticOutputTokenType(color: Color): string {
function toKernelOutputTokenType(
color: Color,
stochasticityEnabled: boolean,
): string {
const properties = color.elements
.map(
(element) =>
` ${element.name}: ${
element.type === "real"
? "number | Distribution"
: toTsType(element.type)
};`,
)
.map((element) => {
if (element.type === "uuid") {
return ` ${element.name}?: bigint | string | PetrinautUuid;`;
}
if (element.type === "real" && stochasticityEnabled) {
return ` ${element.name}: number | Distribution;`;
}
return ` ${element.name}: ${toTsType(element.type)};`;
})
.join("\n");

return properties.length > 0
Expand Down Expand Up @@ -132,18 +144,27 @@ export function generateVirtualFiles(
): Map<string, VirtualFile> {
const files = new Map<string, VirtualFile>();

// Generate global SDCPN library definitions
// Generate global SDCPN library definitions. The Uuid helper is always
// available (uuid token attributes exist regardless of stochasticity);
// Distribution declarations stay gated on the stochasticity extension.
files.set(getItemFilePath("sdcpn-lib-defs"), {
content: extensions.stochasticity
? [
`type Distribution = { map(fn: (value: number) => number): Distribution };`,
`declare namespace Distribution {`,
` function Gaussian(mean: number, deviation: number): Distribution;`,
` function Uniform(min: number, max: number): Distribution;`,
` function Lognormal(mu: number, sigma: number): Distribution;`,
`}`,
].join("\n")
: "",
content: [
...(extensions.stochasticity
? [
`type Distribution = { map(fn: (value: number) => number): Distribution };`,
`declare namespace Distribution {`,
` function Gaussian(mean: number, deviation: number): Distribution;`,
` function Uniform(min: number, max: number): Distribution;`,
` function Lognormal(mu: number, sigma: number): Distribution;`,
`}`,
]
: []),
`type PetrinautUuid = { readonly __petrinautUuid: "generate" | "from" };`,
`declare namespace Uuid {`,
` function generate(): PetrinautUuid;`,
` function from(value: string | number | bigint): PetrinautUuid;`,
`}`,
].join("\n"),
});

// Build lookup maps for places and types.
Expand Down Expand Up @@ -293,7 +314,8 @@ export function generateVirtualFiles(
// Build output type: { [placeName]: [Token, Token, ...] } based on output arcs.
const outputTypeImports: string[] = [];
const outputTypeProperties: string[] = [];
// Per-colour stochastic output token types (Distribution on real only).
// Per-colour kernel output token types (optional uuid; Distribution on
// real only when stochasticity is enabled).
const outputTokenTypeAliases = new Map<string, string>();

for (const arc of transition.outputArcs) {
Expand All @@ -319,19 +341,15 @@ export function generateVirtualFiles(
outputTypeImports.push(importStatement);
}
const tokenTuple = Array.from({ length: arc.weight })
.fill(
extensions.stochasticity
? `Output_${sanitizedColorId}`
: `Color_${sanitizedColorId}`,
)
.fill(`Output_${sanitizedColorId}`)
.join(", ");
if (
extensions.stochasticity &&
!outputTokenTypeAliases.has(sanitizedColorId)
) {
if (!outputTokenTypeAliases.has(sanitizedColorId)) {
outputTokenTypeAliases.set(
sanitizedColorId,
`type Output_${sanitizedColorId} = ${toStochasticOutputTokenType(color)};`,
`type Output_${sanitizedColorId} = ${toKernelOutputTokenType(
color,
extensions.stochasticity,
)};`,
);
}
const placeDisplayName = getPlaceDisplayNameForArc(arc, sdcpn);
Expand Down Expand Up @@ -606,10 +624,10 @@ export function generateMetricSessionFiles(
}
tokensType = `Color_${sanitized}[]`;
} else {
tokensType = "Record<string, number | boolean>[]";
tokensType = "Record<string, number | boolean | bigint>[]";
}
} else {
tokensType = "Record<string, number | boolean>[]";
tokensType = "Record<string, number | boolean | bigint>[]";
}
placeStateProperties.push(
` "${place.name}": { count: number; tokens: ${tokensType} };`,
Expand All @@ -619,7 +637,7 @@ export function generateMetricSessionFiles(
const placesType =
placeStateProperties.length > 0
? `{\n${placeStateProperties.join("\n")}\n}`
: "Record<string, { count: number; tokens: Record<string, number | boolean>[] }>";
: "Record<string, { count: number; tokens: Record<string, number | boolean | bigint>[] }>";

// defs file (kept separate so updates only invalidate code on real changes)
const defsPath = getItemFilePath("metric-session-defs", { sessionId });
Expand Down
Loading
Loading