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
2 changes: 2 additions & 0 deletions libs/@hashintel/petrinaut-core/src/hir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ export {
} from "./hir/analyze";
export {
compileHirArtifacts,
type HirCompileOptions,
type HirCompileFailure,
type HirCompileResult,
type HirMetricReturnType,
} from "./hir/compile";
export {
hirDistributionRuntime,
Expand Down
10 changes: 8 additions & 2 deletions libs/@hashintel/petrinaut-core/src/hir/BUFFER_ABI.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,18 @@ The field order matches `TokenSlotLayout.realFieldF64Offsets`.

## Metrics

Metrics read a frame's raw token region and dense place metadata:
Metric-shaped frame readers β€” numeric metrics and boolean experiment
predicates β€” read a frame's raw token region and dense place metadata:

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

Numeric metrics must return a finite number. Predicates are compiled with a
boolean return type (`HirMetricContext.returnType` /
`HirCompileOptions.metricReturnTypes`) and must return a boolean; the runtime
evaluators enforce the respective return type per call.

`HirMetricArtifact.placeNames` records places in first-reference order. At
instantiation, `__places[ordinal]` maps those names to frame place indexes.

Expand Down
17 changes: 16 additions & 1 deletion libs/@hashintel/petrinaut-core/src/hir/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ export type HirCompileResult = {
failures: HirCompileFailure[];
};

export type HirMetricReturnType = "number" | "boolean";

export type HirCompileOptions = {
/**
* Optional per-metric return-type override. Regular model metrics default to
* numeric; experiment predicates use the same metric-shaped frame surface but
* compile as boolean-returning artifacts.
*/
metricReturnTypes?: Readonly<Record<string, HirMetricReturnType>>;
};

function notCompilableDiagnostic(fn: HirFunction): HirDiagnostic {
return {
code: "hir:not-compilable",
Expand Down Expand Up @@ -102,6 +113,7 @@ function lowerAndCheck(
export function compileHirArtifacts(
sdcpn: SDCPN,
extensions: PetrinautExtensionSettings = DEFAULT_PETRINAUT_EXTENSIONS,
options: HirCompileOptions = {},
): HirCompileResult {
const sanitized = sanitizeSDCPNForExtensions(sdcpn, extensions);
const artifacts: HirArtifacts = {
Expand Down Expand Up @@ -248,7 +260,10 @@ export function compileHirArtifacts(
// error, but they must not block the rest of the net from compiling.
continue;
}
const item = lowerAndCheck(metric.code, "metric", metricContext);
const item = lowerAndCheck(metric.code, "metric", {
...metricContext,
returnType: options.metricReturnTypes?.[metric.id] ?? "number",
});
if (!item.ok) {
failures.push({
itemId: metric.id,
Expand Down
6 changes: 3 additions & 3 deletions libs/@hashintel/petrinaut-core/src/hir/emit-buffer-js.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*
* lambda: (f64, u64, u8, placeBases, indices) => number | boolean
* kernel: (f64, u64, u8, placeBases, indices, outF64, outU64, outU8, sink) => void
* metric: (f64, u64, u8, placeCounts, placeOffsets) => number
* metric-shaped frame reader: (f64, u64, u8, placeCounts, placeOffsets) => number | boolean
*
* The full ABI is documented in `BUFFER_ABI.md`.
*
Expand Down Expand Up @@ -928,9 +928,9 @@ export function emitBufferKernelJs(
}

/**
* Emits a buffer-ABI metric program (token format v2):
* Emits a buffer-ABI metric-shaped frame-reader program (token format v2):
*
* (f64, u64, u8, placeCounts, placeOffsets) => number
* (f64, u64, u8, placeCounts, placeOffsets) => number | boolean
*
* - `f64`/`u64`/`u8` β€” shared views over the frame's token byte region.
* - `placeCounts`/`placeOffsets` β€” the frame's dense per-place token counts
Expand Down
16 changes: 9 additions & 7 deletions libs/@hashintel/petrinaut-core/src/hir/instantiate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,17 @@ export type HirCompiledBufferLambda = (
/**
* Buffer-ABI metric: reads a frame's packed token region through shared
* views plus its dense per-place `placeCounts`/`placeOffsets` arrays and
* returns one number. The place-ordinal→frame-place-index table and string
* pool are pre-bound at instantiation.
* returns one scalar. Numeric metrics and boolean predicates share this ABI.
* The place-ordinal→frame-place-index table and string pool are pre-bound at
* instantiation.
*/
export type HirCompiledMetric = (
f64: Float64Array,
u64: BigUint64Array,
u8: Uint8Array,
placeCounts: Uint32Array,
placeOffsets: Uint32Array,
) => number;
) => number | boolean;

/** Buffer-ABI kernel: writes output attributes into per-transition staging
* (place-major, baked offsets); RNG-consuming slots defer through the sink. */
Expand Down Expand Up @@ -203,11 +204,12 @@ export function instantiateHirBufferKernel(
}

/**
* Instantiates a compiled metric program. `placeIndices[ordinal]` maps each
* Instantiates a compiled metric-shaped frame-reader program.
* `placeIndices[ordinal]` maps each
* of the artifact's `placeNames` to the frame's place index (resolve once
* per experiment/simulation, not per frame). Metrics have no parameters
* object and never intern strings; `__params`/`__dist` are bound for ABI
* parity with the other programs but unused.
* per experiment/simulation, not per frame). Metrics and predicates have no
* parameters object and never intern strings; `__params`/`__dist` are bound
* for ABI parity with the other programs but unused.
*/
export function instantiateHirMetric(
source: string,
Expand Down
8 changes: 8 additions & 0 deletions libs/@hashintel/petrinaut-core/src/hir/lint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,14 @@ return all.reduce((sum, t) => sum + t.x, 0) / all.length;`,
"error",
]);

// Predicate sessions reuse the metric state surface but return booleans.
expect(
codes(`return true;`, { ...metricContext, returnType: "boolean" }),
).toEqual([]);
expect(
codes(`return 1;`, { ...metricContext, returnType: "boolean" }),
).toContainEqual(["hir:metric-return", "error"]);

// Unknown place.
expect(
codes(`return state.places.Missing.count;`, metricContext),
Expand Down
7 changes: 7 additions & 0 deletions libs/@hashintel/petrinaut-core/src/hir/surface-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@ export type HirMetricContext = {
surface: "metric";
/** Metrics have no parameters object β€” always empty. */
parameters: HirParameterInfo[];
/**
* The scalar result expected from the metric-shaped frame reader surface.
*
* Experiment predicates reuse the same `state` object and buffer emission as
* metrics, but they return a boolean instead of a numeric sample.
*/
returnType?: "number" | "boolean";
/** ALL places of the root net, keyed by display name (last name wins for
* duplicates, matching the runtime object-key overwrite). */
places: HirMetricPlaceInfo[];
Expand Down
11 changes: 11 additions & 0 deletions libs/@hashintel/petrinaut-core/src/hir/typecheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,17 @@ class Typechecker {
return;
}
case "metric": {
if (context.returnType === "boolean") {
if (!isBoolish(returnType)) {
this.report(
bodySpan,
"hir:metric-return",
`Predicates must return a boolean, got ${formatHirType(returnType)}.`,
);
}
return;
}

if (!isNumeric(returnType)) {
this.report(
bodySpan,
Expand Down
10 changes: 10 additions & 0 deletions libs/@hashintel/petrinaut-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,10 @@ export type {
MonteCarloExperiment,
MonteCarloExperimentEvent,
MonteCarloExperimentMetrics,
MonteCarloExperimentPredicates,
MonteCarloExperimentState,
MonteCarloExpressionMetricSpec,
MonteCarloNumericMetricSpec,
MonteCarloFrameMetric,
MonteCarloFrameMetricContext,
MonteCarloMetricDistributionBinning,
Expand All @@ -191,10 +193,12 @@ export type {
MonteCarloMetricNumericAccumulatorState,
MonteCarloMetricSpec,
MonteCarloMetricSpecBase,
MonteCarloMetricType,
MonteCarloMetricRunOutput,
MonteCarloMetricRunStatus,
MonteCarloMetricValueAccumulator,
MonteCarloPlaceTokenCountMeanMetricSpec,
MonteCarloPredicateSpec,
MonteCarloRunFrameMetricView,
MonteCarloRunFrameMetricVisitor,
MonteCarloRunConfig,
Expand All @@ -214,6 +218,11 @@ export type {
MonteCarloUserDefinedMetricSampleRuns,
MonteCarloUserDefinedScalarMetricFrame,
MonteCarloUserDefinedMetricTimeAggregation,
MonteCarloUserDefinedPredicate,
MonteCarloUserDefinedPredicateConfig,
MonteCarloUserDefinedPredicateMeasureInput,
MonteCarloUserDefinedPredicateRunResult,
MonteCarloUserDefinedPredicateSnapshot,
MonteCarloTransitionFiringCountMetricSpec,
MonteCarloWorkerProgress,
} from "./simulation";
Expand Down Expand Up @@ -248,6 +257,7 @@ export type {
// LSP worker, runtime instantiation in ./hir-runtime) ---
export type {
HirArtifacts,
HirCompileOptions,
HirCompileFailure,
HirCompileResult,
HirDiagnostic,
Expand Down
6 changes: 4 additions & 2 deletions libs/@hashintel/petrinaut-core/src/lsp/language-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {

import type { PetrinautExtensionSettings } from "../extensions";
// Type-only: must not pull the compiler (`typescript`) into client bundles.
import type { HirCompileResult } from "../hir";
import type { HirCompileOptions, HirCompileResult } from "../hir";
import type { ReadableStore } from "../store";
import type { SDCPN } from "../types/sdcpn";
import type {
Expand Down Expand Up @@ -93,6 +93,7 @@ export interface LanguageClient {
this: void,
sdcpn: SDCPN,
extensions?: PetrinautExtensionSettings,
options?: HirCompileOptions,
): Promise<HirCompileResult>;

/**
Expand Down Expand Up @@ -314,10 +315,11 @@ export function createLanguageClient(
position,
});
},
requestHirArtifacts(sdcpn, extensions) {
requestHirArtifacts(sdcpn, extensions, options) {
return sendRequest<HirCompileResult>("sdcpn/compileHirArtifacts", {
sdcpn,
extensions,
options,
});
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -409,4 +409,26 @@ describe("SDCPNLanguageServer completions", () => {
expect(names).toContain("gravity");
});
});

describe("metric session files", () => {
it("wraps boolean-return sessions as predicates", () => {
const sdcpn = createSDCPN({
places: [{ id: "place1", name: "Done", colorId: null }],
});
const server = new SDCPNLanguageServer();
server.syncMetricFiles(sdcpn, {
sessionId: "predicate-session",
code: "return state.places.Done.count > 0;",
returnType: "boolean",
});

const filePath = getItemFilePath("metric-code", {
sessionId: "predicate-session",
});

expect(server.getFileContent(filePath)).toContain(
"function __metric(state: MetricState): boolean",
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -583,16 +583,18 @@ export function generateScenarioSessionFiles(
*/
export type MetricSessionData = {
sessionId: string;
/** Metric function body (must `return` a finite number). */
/** Metric-shaped function body. Metrics return numbers; predicates return booleans. */
code: string;
returnType?: "number" | "boolean";
};

/**
* Generates virtual files for a metric editing session.
*
* The user code is a function body whose `state` parameter exposes
* `state.places.<PlaceName>` with `count` and (for colored places) `tokens`.
* The expression is wrapped so the body is type-checked as `(state) => number`.
* The expression is wrapped so the body is type-checked as `(state) => number`
* for metrics and `(state) => boolean` for predicates.
*/
export function generateMetricSessionFiles(
sdcpn: SDCPN,
Expand Down Expand Up @@ -659,7 +661,7 @@ export function generateMetricSessionFiles(
files.set(codePath, {
prefix: [
`import type { MetricState } from "${defsPath}";`,
`function __metric(state: MetricState): number {`,
`function __metric(state: MetricState): ${session.returnType ?? "number"} {`,
"",
].join("\n"),
content: session.code,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,10 @@ function publishAllDiagnostics(
);
if (!hasTsError) {
allDiags.push(
...getHirDiagnosticsForItem(userContent, metricHirContext),
...getHirDiagnosticsForItem(userContent, {
...metricHirContext,
returnType: session.returnType ?? "number",
}),
);
}
params.push({
Expand Down Expand Up @@ -197,6 +200,7 @@ function toMetricSessionData(params: MetricSessionParams): MetricSessionData {
return {
sessionId: params.sessionId,
code: params.code,
returnType: params.returnType,
};
}

Expand Down Expand Up @@ -384,7 +388,11 @@ workerRuntime.onMessage((data) => {
const { id } = data;
respond(
id,
compileHirArtifacts(data.params.sdcpn, data.params.extensions),
compileHirArtifacts(
data.params.sdcpn,
data.params.extensions,
data.params.options,
),
);
break;
}
Expand Down
5 changes: 4 additions & 1 deletion libs/@hashintel/petrinaut-core/src/lsp/worker/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* upstream package directly.
*/
import type { PetrinautExtensionSettings } from "../../extensions";
import type { HirCompileOptions } from "../../hir";
import type { SDCPN, ScenarioParameter } from "../../types/sdcpn";
import type {
Diagnostic,
Expand Down Expand Up @@ -51,8 +52,9 @@ export type ScenarioSessionParams = {
*/
export type MetricSessionParams = {
sessionId: string;
/** Metric function body (must `return` a finite number). */
/** Metric-shaped function body. Metrics return numbers; predicates return booleans. */
code: string;
returnType?: "number" | "boolean";
};

/** Position in a text document (LSP standard: line/character based). */
Expand Down Expand Up @@ -143,6 +145,7 @@ type ClientRequest =
params: {
sdcpn: SDCPN;
extensions?: PetrinautExtensionSettings;
options?: HirCompileOptions;
};
};

Expand Down
Loading
Loading