FE-514: Compile TypeScript expressions to SymPy#8541
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
This stack of pull requests is managed by Graphite. Learn more about stacking. |
PR SummaryMedium Risk Overview Integrates SymPy compilation into Extends the editor export menu with “JSON with SymPy expressions”, generating an export that embeds per-expression SymPy output (or conversion errors) alongside the original SDCPN model. Written by Cursor Bugbot for commit 2c76726. This will update automatically on new commits. Configure here. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
|
|
||
| // Run SymPy compilation checks on all code expressions | ||
| checkSymPyCompilation(sdcpn, itemDiagnostics); | ||
|
|
There was a problem hiding this comment.
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)
| } | ||
| } | ||
|
|
||
| const exportExpr = exportAssignment!.expression; |
There was a problem hiding this comment.
Null dereference when fallback export search succeeds
Medium Severity
When exportAssignment is undefined (first find failed), the fallback search at line 130 can match an export = statement (since it doesn't filter by !isExportEquals). If exportDefault is found, the code falls through the if block and reaches exportAssignment!.expression on line 142, but exportAssignment is still undefined, causing a runtime crash. The fallback block never reassigns exportAssignment, so the non-null assertion is incorrect on that path.
| if (!exportAssignment) { | ||
| // Try export default as ExpressionStatement pattern | ||
| const exportDefault = sourceFile.statements.find((stmt) => { | ||
| if (ts.isExportAssignment(stmt)) { | ||
| return true; | ||
| } | ||
| // Handle "export default X(...)" which parses as ExportAssignment | ||
| return false; | ||
| }); | ||
| if (!exportDefault) { | ||
| return errNoPos("No default export found"); | ||
| } | ||
| } | ||
|
|
||
| const exportExpr = exportAssignment!.expression; |
There was a problem hiding this comment.
Critical null reference bug. If exportAssignment is null (line 128), the code enters the fallback logic (lines 129-136) to find exportDefault. However, line 142 unconditionally accesses exportAssignment!.expression which will be null, causing a runtime error.
Fix: The fallback logic appears incomplete. After finding exportDefault, it should be assigned to a variable and used instead of exportAssignment. The code should be:
let exportAssignment = sourceFile.statements.find(
(stmt): stmt is ts.ExportAssignment =>
ts.isExportAssignment(stmt) && !stmt.isExportEquals,
);
if (!exportAssignment) {
return errNoPos("No default export found");
}
const exportExpr = exportAssignment.expression;The fallback logic (lines 129-136) appears to be dead code that doesn't actually change the outcome and should be removed.
| if (!exportAssignment) { | |
| // Try export default as ExpressionStatement pattern | |
| const exportDefault = sourceFile.statements.find((stmt) => { | |
| if (ts.isExportAssignment(stmt)) { | |
| return true; | |
| } | |
| // Handle "export default X(...)" which parses as ExportAssignment | |
| return false; | |
| }); | |
| if (!exportDefault) { | |
| return errNoPos("No default export found"); | |
| } | |
| } | |
| const exportExpr = exportAssignment!.expression; | |
| const exportAssignment = sourceFile.statements.find( | |
| (stmt): stmt is ts.ExportAssignment => | |
| ts.isExportAssignment(stmt) && !stmt.isExportEquals, | |
| ); | |
| if (!exportAssignment) { | |
| return errNoPos("No default export found"); | |
| } | |
| const exportExpr = exportAssignment.expression; |
Spotted by Graphite
Is this helpful? React 👍 or 👎 to let us know.
🤖 Augment PR SummarySummary: This PR introduces SymPy compilation support for Petrinaut code snippets and surfaces conversion issues as diagnostics, plus adds an export option that includes SymPy-rendered expressions. Changes:
Technical Notes: Diagnostics are generated as synthetic 🤖 Was this summary useful? React with 👍 or 👎 |
| ); | ||
| } | ||
|
|
||
| const kernelCtx = buildContextForTransition( |
There was a problem hiding this comment.
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
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| checkSymPyCompilation(sdcpn, itemDiagnostics); | ||
|
|
||
| return { | ||
| isValid: itemDiagnostics.length === 0, |
There was a problem hiding this comment.
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
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| } | ||
| } | ||
|
|
||
| const exportExpr = exportAssignment!.expression; |
There was a problem hiding this comment.
If exportAssignment is not found but an ExportAssignment with isExportEquals is present, the current flow falls through and then dereferences exportAssignment!, which will throw at runtime instead of returning a clean { ok: false } result.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| ); | ||
| if (!result.ok) return result; | ||
| lines.push(result.sympyCode); | ||
| } else if (ts.isExpressionStatement(stmt)) { |
There was a problem hiding this comment.
compileBlock() currently skips all ExpressionStatements under the assumption they are comments, but comments aren’t represented as expression statements in the TS AST; this can silently ignore real statements like console.log(...) and incorrectly report a successful SymPy compilation.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| sourceFile, | ||
| ); | ||
| } | ||
| const key = prop.name.getText(sourceFile); |
There was a problem hiding this comment.
For object literals, const key = prop.name.getText(sourceFile) will include quotes for string-literal keys (e.g. "Place A"), and then the output wraps it in quotes again ('"Place A"'), producing incorrect SymPy/Python dict keys.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| }); | ||
| } | ||
|
|
||
| const exportData = { |
There was a problem hiding this comment.
exportData spreads ...petriNetDefinition after setting title/sympy_expressions; since imported JSON can carry extra fields (e.g. exporting then re-importing), the spread can overwrite these new fields and drop the freshly-compiled SymPy output.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
2017cee to
8f94821
Compare
3125bda to
feb318b
Compare
8f94821 to
aa1f091
Compare
Integrate compileToSymPy into the LSP checker so SymPy-incompatible expressions surface as warnings in the Diagnostics tab with accurate source positions. Add "JSON with SymPy expressions" export menu entry. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Boolean(expr) maps to sp.Ne(expr, 0) matching JS truthiness semantics. Number(expr) passes through as identity in symbolic math context. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Maps TypeScript array literals to Python lists, enabling transition kernels that return arrays of token objects to compile to SymPy. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Compiles tokens.map(callback) to [body for _iter in collection],
handling both destructured ({ x, y }) and simple (token) parameters.
Enables dynamics expressions that iterate over token arrays.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ompiler Only const is allowed — both let and var are now rejected. Standalone expression statements (not assigned or returned) produce a diagnostic. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Refactors the compiler into two layers: TypeScript → Expression IR (JSON) → SymPy. The IR captures bindings, expressions, distributions, and derived distributions as a typed JSON AST, enabling future backends beyond SymPy. Key changes: - expression-ir.ts: 18 IR node types (number, symbol, parameter, tokenAccess, binary, unary, call, distribution, derivedDistribution, piecewise, etc.) - compile-to-ir.ts: TS→IR compiler with scope tracking for distribution bindings and support for Distribution.map() as derived distributions - ir-to-sympy.ts: IR→SymPy converter with let-binding inlining - compile-to-sympy.ts: thin wrapper composing the two layers - checker.ts: separate SymPy warnings from TS errors so isValid only reflects TypeScript validity; skip kernel checks for uncoloured outputs - Storybook playground for live TS→IR visualization Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reorganize into src/expression/{ts-to-ir,ir-to-sympy} and remove the
compile-to-sympy convenience wrapper. The LSP checker now uses
compileToIR directly and reports "Invalid expression" instead of
"SymPy" in diagnostics.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a `showExpressionOutput` user setting (default: false) with a toggle in the viewport settings dialog. When enabled, a resizable split panel appears below the Lambda and Transition Results code editors showing the compiled output. A floating format selector allows switching between expression IR (JSON) and SymPy (Python). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Rewrite SymPy emitter to produce clean Python: explicit `from X import Y` imports (no `sp.` prefix), hoisted named distributions from let-bindings, inline anonymous distributions, `return` statement, and auto-indented dicts/lists when >80 chars. DerivedDistribution replaced with direct symbolic substitution. - Add OCaml backend (ir-to-ocaml) with Float operators, let-in bindings, Distribution functions, auto-indented records/lists, and direct distribution substitution (no Distribution.map wrapper). - Add Lean 4 backend (ir-to-lean) with Mathlib imports, Real.cos/sin/sqrt, Unicode operators (≤, ≥, ≠, ∧, ∨, ¬), gaussianReal/uniformOn distributions, and ℝ type annotations on let-bindings. - Wire all four targets (IR, SymPy, OCaml, Lean) into the expression output panel selector. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Import Python and F# language contributions in the Monaco provider. Map output formats to Monaco languages: JSON for IR, Python for SymPy, F# for OCaml and Lean. Use key-based remount to ensure language switches correctly when the user changes format. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract shared `formatIRResult` and `useCompileToOutput` helpers from the expression output hook. Add `useDiffEqExpressionOutput` for differential equations using `buildContextForDifferentialEquation`. Show the same resizable split panel with format selector in the differential equation Code section when the setting is enabled. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Destructured .map(({ x, y }) => ...) now emits propertyAccess nodes
(token.x, token.y) instead of prefixed symbols (_iter_x, _iter_y).
The iteration variable is derived from the collection name by
singularizing it (tokens → token). Simple .map((token) => ...) uses
the user's parameter name directly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
70fa667 to
b8e3990
Compare
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>



🌟 What is the purpose of this PR?
Adds the ability to compile Petrinaut's TypeScript expressions (rate laws, guard conditions, dynamics) into SymPy Python code — enabling interoperability with external scientific computing tools (TOPOS, Coherence Research). This is a demo-quality implementation proving the concept and establishing the TS→SymPy mapping.
🔗 Related links
🔍 What does this change?
compile-to-sympy.ts— AST-walking compiler that parses TypeScript expressions and emits SymPy equivalents. Supports arithmetic, comparisons, logical operators, ternary→Piecewise,Math.*functions/constants,Distribution.*, token/parameter access, object literals,constbindings, and type assertion unwrapping. Rejects unsupported syntax with positioned error diagnostics (start/length).compile-to-sympy.test.ts— 51 tests covering all supported syntax, real-world expressions (SIR model, orbital dynamics, transition kernels), and error cases with position assertions.checker.ts— Integrates SymPy compilation into the LSP checker. RunscompileToSymPyon all differential equations, transition lambdas, and transition kernels. Failures surface as warnings (code99000) with accurate source positions in the Diagnostics tab.export-sympy.ts— New export function that converts all model expressions to SymPy and produces a JSON file containing both the original SDCPN and asympy_expressionsarray.editor-view.tsx— Adds "JSON with SymPy expressions" to the Export submenu.Pre-Merge Checklist 🚀
🚢 Has this modified a publishable library?
This PR:
📜 Does this require a change to the docs?
The changes in this PR:
🕸️ Does this require a change to the Turbo Graph?
The changes in this PR:
let, arbitrary function calls) are rejected with warnings rather than errors, since the TS code itself is still valid.🐾 Next steps
🛡 What tests cover this?
compile-to-sympy.test.tscovering basic expressions, parameter/token access, all operators, Math functions, distributions, block bodies, real-world expressions, and error positioning.❓ How to test this?
sympy_expressionswith converted code