Osy#betaRun it on Osyrin: osyrin.com

Why Osy# · Chapter 07 · The editor

The editor sees the program the compiler sees.

The compiler does more than read one file at a time. It resolves the application: every symbol, type and reference, what each function touches, where each body and call executes, which functions are tests. The editor asks that same resolved program. So hover, rename, a diagnostic and a paused debugger are answers from the program the build actually sees.

$ osy init · abridged$ osy init kanban --agent claude
  ✓ CLAUDE.md
  …
  ✓ .vscode/launch.json
  ✓ .vscode/extensions.json
  ✓ app.osy
  …
✓ Project initialized.

$ osy vscode install
// the extension shipped with the CLI, into VS Code
// (or Cursor, VSCodium, Windsurf). Its language
// server is `osy lsp`: the toolchain you build with.
In practiceHover, rename, go-to-definition and the debugger come from the resolver that compiles your app.

01

The editor asks the resolved program

A name in a source file means whatever the rest of the application says it means. Here is a page in the chat app: its Create action calls CreateChannel, a function declared in another file. The compiler resolves that call when it builds the app, and the editor asks the same resolver, so the answers below are computed from the resolved application, on every build of this page.

demo/ember/model/pages/shell.osyverbatim — this file compiles
  action Create() {
    var name = newName.Trim();
    if (name == "") { return; }
1    var made = CreateChannel(name, "");
    newName = "";
    Navigation.Go("/c/" + made.Id);
  }
1

One name, declared elsewhere. What the editor can say about it is what the compiler resolved it to.

what the editor answers about that name — computed at build time
hover        Channel(string, string) CreateChannel
definition   model/workspace.osy:65
rename       2 places in 2 files: model/pages/shell.osy (1), model/workspace.osy (1)

Hover gives the declaration's signature. Go to definition lands in the file that declares it. Rename lists every place the resolved application refers to it, so the edit is the symbol's, not the word's.

Misspell the call and the editor's diagnostic and the build's diagnostic are the same sentence, because they are the same resolver reading the same program.

the same misspelling, asked two ways
editor        model/pages/shell.osy:35:16
              UI: unknown function or member 'CreateChanel'. Did you mean 'CreateChannel'?

$ osy validate
model/pages/shell.osy:30:16  ERROR  RESOLVE_ERROR
              UI: unknown function or member 'CreateChanel'. Did you mean 'CreateChannel'?
  → CreateChannel
✗ 1 error

The first block is the editor's answer, computed when this page was built. The second is osy validate on a copy of the app with the same misspelling, captured 2026-09-23; its one long line is broken after the code to fit.

The editor is not reconstructing the program from the file under your cursor. It is asking the compiler's resolved application.

VS Code showing an Osy# page: semantic colouring, an inlay hint, a code lens, and the Osy# project view
The same app in the editor. The colour on an identifier comes from the resolved model; var want: string is a type the compiler inferred, drawn as an inlay hint; 0 references is a count over the whole application.

02

Execution location is part of that model

A component can hold code whose operations belong in different places: reading a held key belongs in the browser, writing a row belongs on the server. The compiler resolves where each body and each call executes from what the operations mean, and that decision is part of the resolved program. So the editor can put it on the source.

An ordinary game loop. Nothing in it names a side.

component Game() {
  int score = 0;

  on every (TimeSpan.FromMilliseconds(16)) {
1    if (Keyboard.Down(Space)) { score = score + 1; }
    SaveScore(score);
  }

  render { Box(keys: [Space]) { Text($"{score}"); } }
}
what the editor receives for that file
line  16  Keyboard.Down    side=client   body=server   crosses=true
line  17  SaveScore        side=server   body=server   crosses=false

Captured from the real language server. For every call: the side the call runs on, the side of the body it sits in, and whether the two differ.

The compiler put the whole tick body on the server, because it writes a row. SaveScore therefore runs where it already is. Keyboard.Down reads the browser's held keys, so it runs in the browser and comes back: that line crosses, sixty times a second. The source did not annotate either call; the compiler resolved it from what the two operations mean, and the editor draws that decision on the line where it was made. A crossing costs a round trip and can fail for every reason a network call can, which is why it belongs in front of the person writing the loop.

The compiler knows where each line of this program executes, and the editor can show you that fact.

03

From the test tree into the running program

The last chapter's tests are functions in the same program. The discovery pass that knows a [Test] exists gives it a stable place in the editor, under its file and its fixture, with the same id osy test --test takes. Run one from the tree, or debug it, and execution returns to the same source model.

The VS Code Test Explorer showing an Osy# app's tests grouped by file and fixture
File, fixture, test. Six green; results stream into the tree as each finishes. The next section is what happens when you press debug on one of them.

04

Stop inside the program you wrote

A breakpoint is set on an Osy# line. When execution reaches it, the program stops there: the stack is your functions and the call sites that reached them, the locals are your values, and an expression typed into the console is evaluated by the program's own evaluator.

paused on model/orders.osy:9, from a test — the stack and a value, as the editor received them
stack
  Discount                        orders.osy:9
  call:Discount                   orders.osy:14
  Place                           orders.osy:14
  call:Place                      a_big_order_gets_its_discount:4
  a_big_order_gets_its_discount   a_big_order_gets_its_discount:4

variables
  total = 200

evaluate  total * 2  →  400  (Decimal)

Captured by driving osy debug-test the way the extension drives it. Every frame is Osy#: the function, the call that reached it, and the test that started the run.

VS Code paused on a breakpoint in an Osy# file, with the call stack, locals and inline values
The same session in the editor: paused on the line, the stack in Osy# frames, total = 200 in Locals, and the same value drawn beside the lines that have run.

A debugged test runs on its own copy of the data with your rules on, exactly as a plain run does. An expression that would write data is accepted only while the app is in Development, and that is decided where the debugger attaches, not by the client asking.

05

A parked workflow is still an execution

Chapters 03 and 04 left a program waiting: a saga leg holding until a child run finishes, a body parked at a durable step. That waiting body is an execution of the program, suspended mid-function with its stack and its values kept, and the debugger can attach to it whenever you like, two minutes or two days later.

osy debug-flows

FulfillmentSaga

parked · started two days ago

a saga leg, holding until its child run returns

attach

by the flow's own id, from the editor's picker or osy debug-attach

what the debugger shows

order.osy:170 · await saga.Run("charge", …)

the line it is waiting on, in your source

the stack: the state's enter body, and the saga inside it

the values: reservation, charge, the trace row it made

an expression, evaluated against that frame

The execution can wait without ceasing to be an execution of the program.

The picture is a diagram of the shape, drawn over chapter 04's saga; the verbs and the position are real. The same handle addresses a run that is executing right now and one that parked two days ago waiting for a person. A flow that crosses into the browser and back is still one program, so its stack is one stack, the server's frames and the browser's stitched together innermost first, and a breakpoint set on an editor line is resolved for both sides, which is why it survives the hand-off. Once you have stepped a parked flow forward, you can step it back to the boundary before, and it re-materialises there: its state is restored, and a step that already committed data is not unwound with it.

What happens when you change it

Change the program. The model follows.

Each of these is a thing you would do on a Tuesday. The verdict is the compiler's, not a convention's.

You do thisVerdictBecause
Rename a field the whole app usescompilesRename from the editor. The edit is the symbol's, across every file that refers to it, and the diagnostics that follow are the build's.
Move a function to another filecompilesNothing to update. The workspace is one resolved program, so definition, references and the diagnostics see across files.
Change what a body touchescompilesIts execution side follows from what it now touches, and the margin in §2 changes with it.
Work from the CLI instead of the editorcompilesThe same answers: osy check, osy test, osy debug-test, osy model. The editor is one front end to the resolver, which is why an agent and a person get the same ones.
Upgrade the toolchaincompilesThe editor is upgraded with it. The extension launches the osy you installed as its language server, so the editor and the build are one version.