Skip to content
Merged
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/token-codec-exports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@hashintel/petrinaut-core": patch
"@hashintel/petrinaut": patch
---

Export the token value codec and `compileUserCode` from `@hashintel/petrinaut-core`.
9 changes: 9 additions & 0 deletions libs/@hashintel/petrinaut-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,15 @@ export {
type ScenarioParameterValues,
} from "./simulation/authoring/scenario/compile-scenario";
export { buildMetricState } from "./simulation/frames/metric-state";
export {
coerceTokenAttributeValue,
coerceTokenRecord,
decodeTokenAttributeValue,
decodeTokenRecord,
defaultTokenAttributeValue,
encodeTokenAttributeValue,
} from "./simulation/engine/token-values";
export { compileUserCode } from "./simulation/authoring/user-code/compile-user-code";
export {
displayNameSchema,
validateDisplayName,
Expand Down
13 changes: 3 additions & 10 deletions libs/@hashintel/petrinaut/src/ui/components/spreadsheet.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useRef, useState } from "react";

import { css, cva } from "@hashintel/ds-helpers/css";
import { defaultTokenAttributeValue } from "@hashintel/petrinaut-core";

export interface SpreadsheetColumn {
id: string;
Expand Down Expand Up @@ -192,16 +193,8 @@ const booleanCellStyle = css({

const getDefaultCellValue = (
column: SpreadsheetColumn | undefined,
): SpreadsheetCellValue => {
switch (column?.type) {
case "boolean":
return false;
case "integer":
case "real":
default:
return 0;
}
};
): SpreadsheetCellValue =>
column?.type ? defaultTokenAttributeValue(column.type) : 0;

const formatCellValue = (value: SpreadsheetCellValue): string => String(value);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { Button, Select, TextInput } from "@hashintel/ds-components";
import { css } from "@hashintel/ds-helpers/css";

import type { PlaygroundDimension } from "./physical-layout";
import type { SelectItem } from "@hashintel/ds-components";
import type { ColorElementType } from "@hashintel/petrinaut-core";

export type DimensionEditorProps = {
dimensions: PlaygroundDimension[];
onChange: (dimensions: PlaygroundDimension[]) => void;
};

const typeOptions: SelectItem<ColorElementType>[] = [
{ value: "real", text: "Real" },
{ value: "integer", text: "Integer" },
{ value: "boolean", text: "Boolean" },
];

const listStyle = css({
display: "flex",
flexDirection: "column",
gap: "1.5",
});

const rowStyle = css({
display: "flex",
alignItems: "center",
gap: "1",
});

const nameInputStyle = css({
display: "flex",
flex: "[1]",
minWidth: "[0]",

"& input": {
fontFamily: "mono",
},
});

const typeSelectStyle = css({
width: "[110px]",
flexShrink: 0,
});

/**
* Store-free version of the type-properties dimension list: local state only,
* for the playground story.
*/
export const DimensionEditor: React.FC<DimensionEditorProps> = ({
dimensions,
onChange,
}) => {
const updateAt = (index: number, update: Partial<PlaygroundDimension>) => {
onChange(
dimensions.map((dimension, dimensionIndex) =>
dimensionIndex === index ? { ...dimension, ...update } : dimension,
),
);
};

return (
<div className={listStyle}>
{dimensions.map((dimension, index) => (
// eslint-disable-next-line react/no-array-index-key -- rows are positional
<div key={index} className={rowStyle}>
<TextInput
value={dimension.name}
size="sm"
width="fullWidth"
className={nameInputStyle}
placeholder="dimension_name"
onChange={(name) => updateAt(index, { name })}
connectToRightInput
/>
<Select
required
value={dimension.type}
onChange={(type) => updateAt(index, { type })}
items={typeOptions}
size="sm"
className={typeSelectStyle}
connectToLeftInput
/>
<Button
onClick={() =>
onChange(dimensions.filter((_, other) => other !== index))
}
size="xxs"
variant="ghost"
iconName="close"
aria-label={`Remove dimension ${dimension.name}`}
/>
</div>
))}
<Button
onClick={() =>
onChange([
...dimensions,
// First free dim_N: length alone can collide after removals.
{
name: (() => {
let index = dimensions.length;
while (dimensions.some((d) => d.name === `dim_${index}`)) {
index++;
}
return `dim_${index}`;
})(),
type: "real",
},
])
Comment thread
kube marked this conversation as resolved.
}
size="xs"
variant="ghost"
iconName="plus"
>
Add dimension
</Button>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* monaco-editor 0.55 ships its TypeScript language service untyped β€” the
* contribution's `.d.ts` is an empty `export {}` and the old
* `monaco.languages.typescript` namespace types were removed. Declare the
* minimal surface the playground uses.
*/
declare module "monaco-editor/esm/vs/language/typescript/monaco.contribution.js" {
export type PlaygroundExtraLib = { content: string; filePath?: string };

export const typescriptDefaults: {
setCompilerOptions(options: Record<string, unknown>): void;
setEagerModelSync(enabled: boolean): void;
setExtraLibs(libs: PlaygroundExtraLib[]): void;
setModeConfiguration(config: Record<string, boolean>): void;
};

export const ScriptTarget: { ES2020: number };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { describe, expect, it } from "vitest";

import {
computeTokenLayout,
decodeToken,
encodeToken,
getFieldBits,
getFieldHex,
} from "./physical-layout";

import type { PlaygroundDimension } from "./physical-layout";

const DIMENSIONS: PlaygroundDimension[] = [
{ name: "active", type: "boolean" },
{ name: "amount", type: "real" },
{ name: "count", type: "integer" },
];

describe("computeTokenLayout", () => {
it("v1 keeps declaration order with one f64 slot per dimension", () => {
const layout = computeTokenLayout(DIMENSIONS, "v1");

expect(layout.strideBytes).toBe(24);
expect(layout.paddingRanges).toEqual([]);
expect(
layout.fields.map((f) => [f.name, f.physical.kind, f.byteOffset]),
).toEqual([
["active", "f64", 0],
["amount", "f64", 8],
["count", "f64", 16],
]);
});

it("v2 orders by decreasing alignment and pads the stride to 8", () => {
const layout = computeTokenLayout(DIMENSIONS, "v2");

// amount and count (f64, align 8) first β€” stable relative order β€” then
// the u8 boolean, then 7 bytes of tail padding.
expect(
layout.fields.map((f) => [f.name, f.physical.kind, f.byteOffset]),
).toEqual([
["amount", "f64", 0],
["count", "f64", 8],
["active", "u8", 16],
]);
expect(layout.strideBytes).toBe(24);
expect(layout.paddingRanges).toEqual([{ start: 17, end: 24 }]);
});

it("returns an empty layout for no dimensions", () => {
const layout = computeTokenLayout([], "v2");
expect(layout.strideBytes).toBe(0);
expect(layout.fields).toEqual([]);
});
});

describe("encodeToken / decodeToken", () => {
it.each(["v1", "v2"] as const)(
"round-trips with product coercion in %s mode",
(mode) => {
const layout = computeTokenLayout(DIMENSIONS, mode);
const { stored, decoded } = encodeToken(layout, {
active: true,
amount: 1.25,
count: 2.7,
});

expect(stored).toEqual({ active: true, amount: 1.25, count: 3 });
expect(decoded).toEqual({ active: true, amount: 1.25, count: 3 });
},
);

it("applies typed defaults for missing values", () => {
const layout = computeTokenLayout(DIMENSIONS, "v2");
const { decoded } = encodeToken(layout, {});
expect(decoded).toEqual({ active: false, amount: 0, count: 0 });
});

it("stores booleans as a single byte in v2", () => {
const layout = computeTokenLayout(
[{ name: "flag", type: "boolean" }],
"v2",
);
const { buffer } = encodeToken(layout, { flag: true });
expect(layout.strideBytes).toBe(8);
expect(new Uint8Array(buffer)[0]).toBe(1);
expect(decodeToken(layout, buffer)).toEqual({ flag: true });
});
});

describe("bit inspection", () => {
it("exposes IEEE-754 bits MSB-first for f64 fields", () => {
const layout = computeTokenLayout([{ name: "x", type: "real" }], "v1");
const { buffer } = encodeToken(layout, { x: -2 });
const bits = getFieldBits(buffer, layout.fields[0]!);

// -2 = sign 1, exponent 0x400 (10000000000), mantissa all zero.
expect(bits).toHaveLength(64);
expect(bits[0]).toBe(1);
expect(bits.slice(1, 12).join("")).toBe("10000000000");
expect(bits.slice(12).every((bit) => bit === 0)).toBe(true);
expect(getFieldHex(buffer, layout.fields[0]!)).toBe("0xc000000000000000");
});
});
Loading
Loading