Parsware Platform
One script, two interpreters
BS Lang runs in two places. A form rule runs in the browser, in TypeScript; a plugin step or a Custom API runs on the server, in C#. Those are two separate interpreters of one language, so we keep them the same with one shared set of test scripts that both must pass. This is a developer's look at how that works, and at the two cases where they still disagree.
Este artículo solo está disponible en inglés.

This one is for developers. It explains something you don't normally have to think about, and why you can rely on it.
Two places a script runs
BS Lang runs in the browser and on the server:
| In the browser (TypeScript) | On the server (C#) |
|---|---|
| Form rules | Plugin steps |
| Command buttons | Custom APIs |
| Studio screens | Scheduled jobs |
| Processes |
The browser side needs no round trip, so a field can appear the moment you type. The server side is the one that can't be skipped: it runs however the record was saved.
And one script can reach both. A solution script is imported by whatever needs it: a form rule in the browser, a plugin step on the server. The editor says so above the code:

So the same needsApproval(amount) can decide what a form shows and what the server accepts. That
only works if both sides agree on what the script means.
Two implementations of one language
Underneath, there are two interpreters: one written in TypeScript, one in C#. Both are built from the same grammar, so they read a script the same way. What they do with it is written twice.
Two implementations drift. Not on purpose, and not all at once, but a fix goes into one and not the other, and nothing in either compiler notices. The result is the worst kind of bug: a rule that shows a field on the form and then refuses the save, or a condition that works in one place and branches the other way in another.
One set of tests, run by both
So there's a conformance suite: a folder of test scripts, each with the values it must produce. Both interpreters run the same files. Neither side owns them, so a case added for one is added for both.
Each case is a script and what it must record:

record(v) is a function both test runners give the script. It notes the type and the value of
whatever it's handed. The comparison is on what a script can tell apart, the type and the
value, not on how each side stores it inside.
int and number are separate types on purpose. 1 is an int and 1.0 is a number in both,
so a case that records 1 / 2 is also checking which one the language gives you.
There are 131 cases in eight files: arithmetic, strings, control flow, functions and closures, lists and objects, equality, the execution budget, and variables.
Both sides, green
The TypeScript side runs them with Vitest:

And the C# side with xUnit, reading the very same files:

(The totals differ because each side also has a couple of checks of its own, like one that fails if the case files can't be found, so an empty run can't pass by accident.)
What the suite has caught
The equality file exists because the two sides disagreed. true == true was false in the
browser, silently, and an error on the server. A comparison with an empty value was never true
in the browser, so a sample rule that should have filled in a default currency did nothing at all.
The fix was one rule, written down as cases: nothing equals nothing, values compare within their kind, different kinds are never equal, and none of that is an error. Now both sides pass it, and a change that breaks it fails the build on whichever side broke it.
Where they still disagree
Some cases are marked pending: a known difference, with the reason written next to it. Both runners skip them and list them, so the suite stays green and the list stays visible. There are two today. This is the one that matters:

0.1 + 0.2 is exactly 0.3 on the server and 0.30000000000000004 in the browser, because the
two runtimes use different kinds of number. That's a real difference a script can see. Until the
language picks one kind of number, compare money with a tolerance, or round first, rather than
testing a computed total for exact equality.
The other pending case is a bug on the browser side: reverse() on a list isn't found. It's listed
so it can't be forgotten.
The rules we follow
- A behaviour that isn't in the suite isn't a promise. If you rely on it, it should be a case.
- A change to either interpreter lands with a case, run on both sides.
- A difference found in the wild becomes a case first, then a fix.
- The TypeScript interpreter is the reference, unless it's the one that's wrong.
One known limit: the suite checks that a script was refused and roughly why, not the exact error code, because the C# side doesn't pass the code through yet.
Try it
The cases are in ui/packages/bs-lang/conformance/cases. Run
npm run test -w ui/packages/bs-lang and dotnet test platform/tests/Parsware.BsLang.Tests,
and add a case of your own to both by adding it once.