Skip to content

FE-514: Compile TypeScript expressions to SymPy#8541

Draft
kube wants to merge 5 commits intomainfrom
cf/fe-514-compile-typescript-expressions-to-sympy
Draft

FE-514: Compile TypeScript expressions to SymPy#8541
kube wants to merge 5 commits intomainfrom
cf/fe-514-compile-typescript-expressions-to-sympy

Conversation

@kube
Copy link
Collaborator

@kube kube commented Mar 13, 2026

🌟 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, const bindings, 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. Runs compileToSymPy on all differential equations, transition lambdas, and transition kernels. Failures surface as warnings (code 99000) 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 a sympy_expressions array.
  • editor-view.tsx — Adds "JSON with SymPy expressions" to the Export submenu.

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • does not modify any publishable blocks or libraries, or modifications do not need publishing

📜 Does this require a change to the docs?

The changes in this PR:

  • are internal and do not require a docs change

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • do not affect the execution graph

⚠️ Known issues

  • This is a restricted subset compiler — not all valid TypeScript will produce SymPy output. Unsupported constructs (control flow, let, arbitrary function calls) are rejected with warnings rather than errors, since the TS code itself is still valid.

🐾 Next steps

  • Python language support (H-6323) would complement this by providing the runtime for SymPy expressions.

🛡 What tests cover this?

  • 51 unit tests in compile-to-sympy.test.ts covering basic expressions, parameter/token access, all operators, Math functions, distributions, block bodies, real-world expressions, and error positioning.

❓ How to test this?

  1. Checkout the branch
  2. Open Petrinaut editor, load an example model (e.g. SIR Model, Satellites)
  3. Check the Diagnostics tab for SymPy warnings on any unsupported expressions
  4. Use Export → "JSON with SymPy expressions" and verify the output contains sympy_expressions with converted code

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>
@vercel
Copy link

vercel bot commented Mar 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hash Error Error Mar 13, 2026 2:55am
petrinaut Ready Ready Preview Mar 13, 2026 2:55am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
hashdotdesign Ignored Ignored Preview Mar 13, 2026 2:55am
hashdotdesign-tokens Ignored Ignored Preview Mar 13, 2026 2:55am

@github-actions github-actions bot added area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team labels Mar 13, 2026
Copy link
Collaborator Author

kube commented Mar 13, 2026

This stack of pull requests is managed by Graphite. Learn more about stacking.

@kube kube marked this pull request as ready for review March 13, 2026 01:35
@cursor
Copy link

cursor bot commented Mar 13, 2026

PR Summary

Medium Risk
Introduces a new AST-based compiler and feeds its failures into the LSP diagnostics pipeline; because these warnings are added to itemDiagnostics, they may affect overall isValid and reviewer expectations around “valid” nets.

Overview
Adds a new compileToSymPy pipeline that parses SDCPN transition/differential-equation TypeScript snippets and converts a restricted subset of expressions into SymPy code, with positional error reporting and extensive unit tests.

Integrates SymPy compilation into checkSDCPN by appending synthetic warning diagnostics (code: 99000) for any expressions that can’t be converted.

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.

@kube kube marked this pull request as draft March 13, 2026 01:35
@kube kube changed the title FE-514: Add SymPy compilation diagnostics and export option FE-514: Compile TypeScript expressions to SymPy Mar 13, 2026
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

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);

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

}
}

const exportExpr = exportAssignment!.expression;
Copy link

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Comment on lines +128 to +142
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;
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Suggested change
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

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

@augmentcode
Copy link

augmentcode bot commented Mar 13, 2026

🤖 Augment PR Summary

Summary: 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:

  • Added a TypeScript-AST based compiler (compileToSymPy) that converts a restricted subset of Petrinaut TS expressions into SymPy/Python code.
  • Added context builders for transitions and differential equations to distinguish parameters/token fields during compilation.
  • Integrated SymPy compilation into the LSP checker to emit warning diagnostics when code is valid TS but not convertible to SymPy.
  • Added a new “JSON with SymPy expressions” export option in the editor.
  • Implemented exportWithSymPy to export the SDCPN alongside a sympy_expressions section containing compiled code/errors.
  • Added extensive Vitest coverage for supported/unsupported SymPy compilation cases and error positioning.

Technical Notes: Diagnostics are generated as synthetic ts.Diagnostics (custom code 99000) and are mapped to LSP warnings using existing serialization utilities.

🤖 Was this summary useful? React with 👍 or 👎

Copy link

@augmentcode augmentcode bot left a comment

Choose a reason for hiding this comment

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

Review completed. 6 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

);
}

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.

checkSymPyCompilation(sdcpn, itemDiagnostics);

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.

}
}

const exportExpr = exportAssignment!.expression;
Copy link

Choose a reason for hiding this comment

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

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

Fix This in Augment

🤖 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)) {
Copy link

Choose a reason for hiding this comment

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

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

Fix This in Augment

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

sourceFile,
);
}
const key = prop.name.getText(sourceFile);
Copy link

Choose a reason for hiding this comment

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

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

Fix This in Augment

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

});
}

const exportData = {
Copy link

Choose a reason for hiding this comment

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

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

Fix This in Augment

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

kube and others added 4 commits March 13, 2026 02:47
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team

Development

Successfully merging this pull request may close these issues.

1 participant