The thing answering your editor is the compiler, not an approximation of it. A margin that marks which lines leave the machine, tests in a tree, and a debugger that stops on the line you wrote — with one call stack across the client/server seam, and a parked run you can rewind.
01
The thing answering your editor is the compiler
Not a grammar that approximates it, and not a second implementation that drifts. The language server runs the same front-end osy validate runs, over the whole workspace — so the squiggle under your cursor and the error from the build are the same sentence, and they cannot disagree.
What it answers, today
Diagnostics as you type
whole workspace
Cross-file references resolve, so a rename in one file is an error in the other before
you save either.
Completion · signature help · hover
from the model
Completion over what is actually in scope, and hover that knows an entity's security
posture and a function's effects — because it is asking the resolved model, not an index.
Go to definition · find references · rename
semantic
Rename with a prepare pass, so the editor refuses the position rather than performing a
half-rename.
Incoming and outgoing calls across the whole app, and a
quick fix where a diagnostic knows one.
Test discovery
from the server
The Test Explorer's tree comes from the same discovery pass the runner uses, so a
streamed result binds to a tree node even after edits have moved every span — see §3.
A real page from the chat app, in a real editor. The colour on an identifier is
semantic — it comes from the resolved model, not a regex. var want: string is an inlay
hint; 0 references is a code lens; the status bar says Osy#, 0 errors.
02
The editor knows which half of the program each line is
Osy# makes the client/server seam invisible on purpose — you write ordinary code across it, and the compiler decides where each body runs. That is right for correctness and it hides the one thing you still have to know: this line is a network round trip. So the editor draws it.
Ask the editor about a file and it answers for every call in it — the side that call
runs on, and the side of the body it sits in. Here is an ordinary game loop. Nothing in it declares a
side; the compiler worked all of this out.
componentGame() {
int score = 0;
on every (TimeSpan.FromMilliseconds(16)) {
1if (Keyboard.Down(Space)) { score = score + 1; }
SaveScore(score);
}
render { Box(keys: [Space]) { Text($"{score}"); } }
}
Captured from the real language server. The mark is on the line you would never
suspect — and NOT on the one that looks expensive.
Read that the other way round and it is the whole argument for the margin.SaveScore writes to the database and does not cross: the compiler put the whole tick body
on the server, so by the time that call runs it is already there. Keyboard.Down reads the
browser's held keys — a browser object — so it hands off and comes back. Sixty times a second. Nothing
in the source says either of those things, and no amount of reading the file tells you.
03
Tests in the tree, and one keystroke to stop inside one
The Test Explorer is FILE → FIXTURE → TEST, discovered by the language server. A [TestFixture] is a group rather than a test — it seeds the branch the tests under it fork from. A [Skip("reason")] test appears with its reason, because a parked test is a visible reminder that a gap exists.
From the editor, without a terminal
Run one test · a fixture's worth · the file
▶
Results stream back into the tree as each finishes.
Debug this test
breakpoint
Set one and start. It runs on a throwaway clone with your security enforced, exactly
as an ordinary run does — debugging is not a weaker mode.
Compile · Launch · Launch the page at the cursor · Tail logs
a command
Against the local platform, or a deployed one, from
the same palette.
Attach to a running flow
a picker
Lists the live and parked durable runs and attaches to the one you choose — §5.
FILE → FIXTURE → TEST, discovered by the language server rather than by a second parser
in the extension. The ids are the same ones osy test --test takes.
04
A debugger that pauses inside your own language
Not a JavaScript debugger, and not a .NET one showing you the interpreter's frames. It stops on the line you wrote, in the file you wrote it in, with your own values in the pane.
a real session — what the editor sends, and what comes back
Captured by driving osy debug-test the way the extension drives it.
The stack is Osy# frames — your function, the call site that reached it, and the test that started
the whole thing.
The same session, in the editor. Paused on breakpoint, the stack in Osy# frames,
total = 200 in Locals — and the same value rendered inline beside the code, on the lines
that have actually run.
05
One call stack, across the network — and a run you can rewind
This is the part that has no equivalent elsewhere, because nowhere else is one program. A durable flow IS the debuggee: the same handle addresses a run that is executing right now and one that parked two days ago waiting for a person.
What attaching to a flow gives you
A stack that spans the seam
one stack
The server's frames and the browser's frames are stitched into a single call stack,
innermost first. Not two debuggers side by side — one, because it is one program.
One breakpoint set, pushed to both
both sides
You set a breakpoint on an editor LINE. It is resolved separately for each side, which
is exactly why it survives the hand-off: the run crosses the wire and stops where you asked.
Attach to a PARKED run
days later
A flow waiting on a person is not gone — it is parked, and it reads identically to a
live one: the same position, the same stack, the same answer from an evaluate.
Step BACKWARDS
time travel
A parked flow rewinds to the step boundary before this one and re-materialises there.
State-only today: a step that already committed data is not unwound with it.
Conditional breakpoints · hit counts · logpoints
yes
Stop on the hundredth row, or on the one whose total is wrong, or do not stop at all
and just print.
06
What happens when you change it
Editor changes, and what they cost
Rename a field the whole app uses
compiles
Rename from the editor. It is semantic, and the diagnostics that follow come from the compiler, so the list you get is the list the build would give you.
Move a function to another file
compiles
Nothing to update. The workspace is one model — the gutter, the tokens and the diagnostics all see across files.
Work without the extension
compiles
Everything here has a CLI twin: osy check, osy test, osy debug-test, osy model. The editor is a front end for the same answers, which is why an agent and a person get the same ones.
Use an editor that is not VS Code
compiles
The server speaks LSP over stdio and the debugger speaks DAP. Neither knows what is on the other end — the extension itself contains no protocol logic.
Point the extension at a different compiler
refused
There is nothing to point. The extension ships the server, so the editor and the build cannot be two versions.