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
116 changes: 116 additions & 0 deletions libs/@hashintel/petrinaut/src/lsp/lib/checker.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import type ts from "typescript";

import type { SDCPN } from "../../core/types/sdcpn";
import {
buildContextForDifferentialEquation,
buildContextForTransition,
compileToSymPy,
type SymPyResult,
} from "../../simulation/simulator/compile-to-sympy";
import type { SDCPNLanguageServer } from "./create-sdcpn-language-service";
import { getItemFilePath } from "./file-paths";

Expand All @@ -27,6 +33,113 @@ export type SDCPNCheckResult = {
itemDiagnostics: SDCPNDiagnostic[];
};

/**
* Creates a synthetic ts.Diagnostic from a SymPy compilation error result.
* Uses category 0 (Warning) since SymPy compilation failures are informational
* — the TypeScript code may still be valid, just not convertible to SymPy.
*/
function makeSymPyDiagnostic(
result: SymPyResult & { ok: false },
): ts.Diagnostic {
return {
category: 0, // Warning
code: 99000, // Custom code for SymPy diagnostics
messageText: `SymPy: ${result.error}`,
file: undefined,
start: result.start,
length: result.length,
};
}

/**
* Appends a SymPy diagnostic to the item diagnostics list, merging with
* any existing entry for the same item.
*/
function appendSymPyDiagnostic(
itemDiagnostics: SDCPNDiagnostic[],
itemId: string,
itemType: ItemType,
filePath: string,
result: SymPyResult & { ok: false },
): void {
const diag = makeSymPyDiagnostic(result);
const existing = itemDiagnostics.find(
(di) => di.itemId === itemId && di.itemType === itemType,
);
if (existing) {
existing.diagnostics.push(diag);
} else {
itemDiagnostics.push({ itemId, itemType, filePath, diagnostics: [diag] });
}
}

/**
* Runs SymPy compilation on all SDCPN code expressions and appends
* any errors as warning diagnostics.
*/
function checkSymPyCompilation(
sdcpn: SDCPN,
itemDiagnostics: SDCPNDiagnostic[],
): void {
// Check differential equations
for (const de of sdcpn.differentialEquations) {
const ctx = buildContextForDifferentialEquation(sdcpn, de.colorId);
const result = compileToSymPy(de.code, ctx);
if (!result.ok) {
const filePath = getItemFilePath("differential-equation-code", {
id: de.id,
});
appendSymPyDiagnostic(
itemDiagnostics,
de.id,
"differential-equation",
filePath,
result,
);
}
}

// Check transition lambdas and kernels
for (const transition of sdcpn.transitions) {
const lambdaCtx = buildContextForTransition(sdcpn, transition, "Lambda");
const lambdaResult = compileToSymPy(transition.lambdaCode, lambdaCtx);
if (!lambdaResult.ok) {
const filePath = getItemFilePath("transition-lambda-code", {
transitionId: transition.id,
});
appendSymPyDiagnostic(
itemDiagnostics,
transition.id,
"transition-lambda",
filePath,
lambdaResult,
);
}

const kernelCtx = buildContextForTransition(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checkSymPyCompilation() compiles transitionKernelCode for every transition, but the TS checker explicitly skips kernel validation when there are no coloured output places; this will likely produce noisy SymPy warnings (and change isValid) for kernels that are effectively unused.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

sdcpn,
transition,
"TransitionKernel",
);
const kernelResult = compileToSymPy(
transition.transitionKernelCode,
kernelCtx,
);
if (!kernelResult.ok) {
const filePath = getItemFilePath("transition-kernel-code", {
transitionId: transition.id,
});
appendSymPyDiagnostic(
itemDiagnostics,
transition.id,
"transition-kernel",
filePath,
kernelResult,
);
}
}
}

/**
* Checks the validity of an SDCPN by running TypeScript validation
* on all user-provided code (transitions and differential equations).
Expand Down Expand Up @@ -111,6 +224,9 @@ export function checkSDCPN(
}
}

// Run SymPy compilation checks on all code expressions
checkSymPyCompilation(sdcpn, itemDiagnostics);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SymPy warnings incorrectly invalidate the SDCPN check result

High Severity

checkSymPyCompilation appends SymPy failure diagnostics (intentionally marked as warnings, category 0) into itemDiagnostics, but isValid is computed as itemDiagnostics.length === 0. This means any code that can't be converted to SymPy — such as transition kernels returning array literals like [{ x: 1 }], which aren't handled by compile-to-sympy.ts — will cause the entire SDCPN to be reported as invalid, even though the TypeScript is perfectly valid. The comment on makeSymPyDiagnostic explicitly states these are "informational" and "the TypeScript code may still be valid," contradicting the effect on isValid.

Additional Locations (1)
Fix in Cursor Fix in Web

return {
isValid: itemDiagnostics.length === 0,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isValid: itemDiagnostics.length === 0 now treats SymPy compilation failures (which are emitted as warnings) as making the SDCPN invalid, which seems to contradict the “informational” intent in makeSymPyDiagnostic() and will also break callers/tests that expect warnings not to invalidate the model.

Severity: high

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

itemDiagnostics,
Expand Down
Loading
Loading