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
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.