VersoFeature

One variable store, eight languages, no hand-off

Every kernel in a Verso notebook reads and writes the same variable store, so a value you set in C# is already there in Python, SQL and JavaScript. The interesting part is what happens to that value when it leaves the process.

Here is what I wanted out of a notebook that mixes languages: you set a value once, and every other language already has it. No command in between, and no naming it twice.

Today's release adds JavaScript and TypeScript cells, which brings Verso to eight languages: C#, F#, Python, JavaScript, TypeScript, PowerShell, SQL and HTTP. All eight read and write the same variable store, which the engine has had since 1.0. A C# cell assigns region, and the next cell down, in whatever language, already has region.

That's the part you feel at the keyboard. The part worth reading past the headline is what happens to a value when it has to leave the process to reach the cell that wants it.

One store, and every kernel holds it

There is one variable store per notebook session, and every kernel receives that same instance through the context it is handed at execution time. There is no per-kernel isolation, and names are compared without regard to case.

From your own cells it is six methods and an event:

Member What it does
Set(name, value) Upsert. Fires OnVariablesChanged.
Get<T>(name) The value cast to T, or default if missing or the wrong type.
TryGet<T>(name, out value) The same, with the type check surfaced.
GetAll() Every variable as VariableDescriptor(Name, Value, Type).
Remove(name) Remove one. Fires OnVariablesChanged.
Clear() Remove all. Fires OnVariablesChanged.

Kernels use it in two directions. On the way in, a kernel reads what it needs. On the way out, it publishes what you defined: the C# kernel hands the store your top-level variables once the cell finishes, which is why an ordinary var needs no ceremony to become visible elsewhere.

The variables panel follows OnVariablesChanged, so it repaints as values land. Notebook parameters, also new today, are written into the same store before the first cell runs, so a parameter is just a variable that was already there.

One thing the store will not take is null. Set throws on a null value, so a name either has a value or does not exist. Worth knowing before you hand an F# None across.

Four cells, one value

// C# cell
var region = "us-east";
var threshold = 0.95;
--connection reporting --name services
SELECT name, uptime
FROM   services
WHERE  region = @region AND uptime < @threshold

The SQL kernel resolves @region and @threshold from the store, and puts the result back under the name given by --name, or under lastSqlResult.

# Python cell
print(region, threshold)
alert_count = 3
// JavaScript cell
const summary = { region, threshold, alertCount: alert_count };

And back in C#, summary is waiting:

// C# cell
var summary = Variables.Get<Dictionary<string, object>>("summary");

An HTTP cell joins in too: {{region}} resolves from the store after file-level @variables and dynamic variables, and the response is written back as httpResponse and httpStatus.

Where the boundary actually is

C# cell sets region SQL cell sets services HTTP cell sets httpStatus VariableStore one per session, case-insensitive, no isolation F#, PowerShell, SQL the same CLR object Python converted to native Python types JavaScript, TypeScript JSON to a Node subprocess PROCESS BOUNDARY
One dictionary. What a kernel sees depends on how far the value has to travel.

Inside the engine's process the store holds the object itself, so an F#, PowerShell or SQL cell sees the same instance your C# cell created. The Python kernel is in that process too, and converts on the way across: primitives, lists and dictionaries become native Python list and dict so slicing and iteration behave as expected, and anything else arrives as the .NET object it is.

JavaScript is the one that leaves. With Node.js on the machine, cells run in a persistent subprocess and a variable reaches it as JSON, and your top-level declarations are promoted back onto globalThis so they survive to the next cell. Without Node, cells fall back to Jint, a pure .NET ES2024 interpreter, over the same bridge.

Value in the store In a Python cell In a JavaScript cell
string, long, double, bool The matching Python type The matching JSON type
A list or dictionary A native list or dict An array or object
Any other CLR object The object itself Whatever System.Text.Json makes of it, or skipped
Delegate, Task, CancellationToken Skipped Skipped, along with IAsyncDisposable

Coming back, a JavaScript object arrives in .NET as a Dictionary<string, object>, an array as a List<object>, and a number as a long when it fits or a double when it does not. Python publishes back everything in its scope that is not a module, not callable and not underscore-prefixed.

Names beginning with __verso_ are held back from JavaScript. That prefix is where magic commands keep their own side-channel data, and none of that belongs in someone's script.

How other notebooks do it

Verso Polyglot Notebooks Jupyter
How a value moves One VariableStore per session, handed to every kernel #!share pulls a named variable from a named kernel; #!set creates a copy of one in the current kernel An open notebook has one interactive session connected to one kernel
What you type Nothing; the name is already bound The command and the source, for example #!set --name csVarFromJs --value @javascript:jsVar Not applicable
Copy or reference The object itself in process, JSON across the JavaScript subprocess Documented as a copy, by default through the application/json MIME type, serialized with System.Text.Json for .NET kernels. Reference sharing needs both kernels in the same process, both CLR-based, and --byref on #!set One kernel's own variables. Several front ends can attach to that kernel and see the same ones
Languages in one file Eight Several, and the project notes that not all kernels support variable sharing One, recorded as a single kernelspec in the notebook metadata

I took those two columns from the Polyglot Notebooks variable sharing and magic command docs, and from the Jupyter docs and nbformat description. Copies buy something real, by the way: a JSON copy cannot be mutated out from under the kernel that produced it.

What we were after was a mixed-language notebook that stops feeling like an integration. There is no #!share in Verso and no #!set, and that is deliberate. If moving a value costs a line of ceremony, most people stop moving values, and a multi-language notebook turns into a single-language notebook with commentary.

The cost is that the store is flat: a name set in one cell can be overwritten by a cell in another language. That is the bargain a REPL session already makes, and we will take it.

Give it a run with the languages you actually use. If a value crosses over in a shape that surprises you, that's the report I want to see.