Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ package format · 0004 compiling procedure bodies · 0005 mutable pairs · 0006
as abnormal passes · 0007 identity for derived combiners · 0008 caching compiled
bodies (proposed, deliberately not built) · 0009 editor tooling from the runtime
outward (`ik check --json`, environment enumeration, the `ik lsp` language
server, and reader recovery are its phases 1–4).
server, reader recovery, and the environment/profile views are its phases 1–5).

They record measurements and rejected options, not just decisions. When a prediction
in one turns out wrong, correct it in place rather than leaving it — several carry
Expand Down
33 changes: 33 additions & 0 deletions IronKernel.Tests/LspTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,36 @@ let ``a broken buffer still gets tokens defines and every error`` () =
0; 7; 3; 0; 0 // vau
2; 1; 5; 0; 0 ], // twice (use, on the completed trailing form)
data)

[<Fact>]
let ``the environment request reports capabilities and frames`` () =
let frames =
session
[ request 1 "initialize" "{}"
didOpen "(define twice (vau (x) e x))\n"
request 2 "ironkernel/environment"
"""{"textDocument":{"uri":"file:///probe.ikr"}}""" ]
let report = resultOf 2 frames
let capabilities =
report.GetProperty("capabilities").EnumerateArray()
|> Seq.map (fun value -> value.GetString())
|> List.ofSeq
Assert.Contains("host-io", capabilities)
let reportFrames = report.GetProperty("frames").EnumerateArray() |> List.ofSeq
// The buffer frame comes first and carries the vau define as an operative.
let buffer = reportFrames.Head
Assert.Equal("buffer", buffer.GetProperty("label").GetString())
let bufferSymbols =
buffer.GetProperty("symbols").EnumerateArray()
|> Seq.map (fun symbol ->
symbol.GetProperty("name").GetString(), symbol.GetProperty("class").GetString())
|> List.ofSeq
Assert.Contains(("twice", "operative"), bufferSymbols)
// Environment frames follow; somewhere in them `+` is an applicative
// carrying its certified contract as detail.
let plus =
reportFrames.Tail
|> List.collect (fun frame -> frame.GetProperty("symbols").EnumerateArray() |> List.ofSeq)
|> List.find (fun symbol -> symbol.GetProperty("name").GetString() = "+")
Assert.Equal("applicative", plus.GetProperty("class").GetString())
Assert.Contains("certified", plus.GetProperty("detail").GetString())
73 changes: 73 additions & 0 deletions IronKernel/LanguageServer.fs
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,67 @@ module LanguageServer =
previousColumn <- column
yield! [ deltaLine; deltaColumn; length; int64 tokenType; 0L ] ]

/// `ironkernel/environment`: the inspector's data. Frame structure is
/// host-side by design -- phase 2 deliberately exposes no parent
/// environments to Kernel code, and the server holds the records
/// in-process (ADR 0009 phases 2 and 5).
let private renderCapability = function
| RawClrInterop -> "raw-clr-interop"
| HostIO -> "host-io"
| SourceLoading -> "source-loading"
| HostAsync -> "host-async"
| GeneratedClr name -> sprintf "(generated-clr \"%s\")" name

let private handleEnvironment output session (id: JsonElement) (uri: string option) =
respond output id (fun writer ->
writer.WriteStartObject()
writer.WritePropertyName "capabilities"
writer.WriteStartArray()
for capability in Capabilities.ofEnvironment session.env |> Set.toList do
writer.WriteStringValue(renderCapability capability)
writer.WriteEndArray()
writer.WritePropertyName "frames"
writer.WriteStartArray()
let writeSymbol (name: string) symbolClass (value: LispVal option) =
writer.WriteStartObject()
writer.WriteString("name", name)
writer.WriteString("class", renderClass symbolClass)
match value |> Option.bind Contracts.tryGetContract with
| Some contract -> writer.WriteString("detail", renderContract contract)
| None -> ()
writer.WriteEndObject()
// The buffer's defines are the innermost frame the reader sees.
match uri with
| Some uri ->
let text =
match session.documents.TryGetValue uri with
| true, value -> value
| _ -> ""
writer.WriteStartObject()
writer.WriteString("label", "buffer")
writer.WritePropertyName "symbols"
writer.WriteStartArray()
for name, symbolClass in Map.toList (definesIn uri text) do
writeSymbol name symbolClass None
writer.WriteEndArray()
writer.WriteEndObject()
| None -> ()
SymbolTable.reachableFrames session.env
|> List.iteri (fun index record ->
writer.WriteStartObject()
writer.WriteString(
"label",
sprintf "frame %d (%d bindings)" (index + 1) record.bindings.Count)
writer.WritePropertyName "symbols"
writer.WriteStartArray()
for name in record.bindings.Keys |> Seq.sort do
let value = record.bindings.[name].state.value
writeSymbol name (classifyValue value) (Some value)
writer.WriteEndArray()
writer.WriteEndObject())
writer.WriteEndArray()
writer.WriteEndObject())

let private handleSemanticTokens output session (id: JsonElement) uri =
let text =
match session.documents.TryGetValue (uri: string) with
Expand Down Expand Up @@ -547,6 +608,18 @@ module LanguageServer =
handleHover output session id (textDocumentUri parameters) line character
| "textDocument/semanticTokens/full" ->
handleSemanticTokens output session id (textDocumentUri parameters)
| "ironkernel/environment" ->
let uri =
match parameters.ValueKind with
| JsonValueKind.Object ->
match parameters.TryGetProperty "textDocument" with
| true, textDocument ->
match textDocument.TryGetProperty "uri" with
| true, value -> Some(value.GetString())
| _ -> None
| _ -> None
| _ -> None
handleEnvironment output session id uri
| other when hasId ->
respondError output id -32601 (sprintf "method not found: %s" other)
| _ -> ()
Expand Down
38 changes: 33 additions & 5 deletions docs/adr/0009-editor-tooling-from-the-runtime-outward.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# ADR 0009: Editor tooling grows from the runtime outward

Status: Accepted — phases 1 through 4 implemented
Status: Accepted — phases 1 through 5 implemented (phase 5's third item
deferred behind phase 6)

## Decision

Expand Down Expand Up @@ -179,10 +180,37 @@ cache is deleted, completion and semantic tokens run against the actual
buffer mid-edit, `ik check` reports every broken region in a file, and the
server publishes every error instead of the first.

**Phase 5 — the visible surfaces.** Environment inspector (a UI over phase
2), a profile status-bar item that also reads the project's own `<Profile>`,
compiled-vs-residual highlighting (the IR's `CLocated` forms already mark
what the analyzer touched).
**Phase 5 — the visible surfaces.** *Done in two parts of three; the third
is corrected and deferred, not merely postponed.*

The environment inspector is a tree view over a custom
`ironkernel/environment` request: the capability set, the buffer's defines
as the innermost frame, then every reachable frame with each binding
classified and its contract as detail. Frame structure stays host-side
exactly as phase 2 decided — the view knows the parents, Kernel code still
cannot reach them. "Eval in this env" did not ship: it needs the phase-6
session protocol, not a tree view.

The profile status bar shows the effective profile and, when the nearest
`.ikproj` declares a different `<IronKernelProfile>`, says so — the
extension's own `--profile` has silently overridden the project's
declaration since the profile setting existed, and surfacing that mismatch
is most of the item's value. Selecting a profile restarts the language
server, so the authority the status bar reports is the authority the
server's session environment actually carries.

Compiled-vs-residual highlighting is the correction. The plan's aside —
"the IR's `CLocated` forms already mark what the analyzer touched" — was
true and beside the point: `CResidual` almost never occurs (dotted lists
and oddball values), because the hybrid boundary is not an IR node. Bodies
compile on first application (ADR 0004) and the performance story lives in
binding-guard specializations with interpreted fallbacks (`CGuarded`), so
"compiled vs residual" is a *runtime* property a static walk cannot show
truthfully — and located spans exist only on the special-form spine anyway.
An honest coverage view needs runtime instrumentation, which belongs with
phase 6's session protocol. Deferred with that dependency named, so the
"nearly free" framing does not get re-derived from the `CLocated` line
above.

**Phase 6 — debug adapter.** Requires a persistent session protocol that does
not exist — the REPL cannot even separate errors from values today — so the
Expand Down
8 changes: 8 additions & 0 deletions editors/vscode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ The extension never implements a second evaluator in JavaScript.
classifies operatives/applicatives by *resolution* — your own `vau`
definitions get the operative color (disable with
`ironkernel.languageServer.enabled`)
- **Environment view** (explorer sidebar): the session's capability set and
every frame's bindings — your buffer's defines first — with contracts as
tooltips, live from the language server
- **Profile status bar**: the effective capability profile at a glance,
flagging when it overrides the project's own `<IronKernelProfile>`; click
to switch (the language server restarts to match)
- **Run Current File** and **Compile Current File to IKC** commands
- **Run Project** and **Build Project** for `.ikproj` (nearest project, picker, or explorer context menu)
- Check on save: `ik check --json` runs on saved `.ikr` files and publishes
Expand Down Expand Up @@ -67,6 +73,8 @@ host authority available to editor commands and playground runs.
- `IronKernel: Compile Current File to IKC`
- `IronKernel: Run Project` — `run <project.ikproj>` (plus `ironkernel.runArgs`)
- `IronKernel: Build Project` — `build <project.ikproj>` → `bin/*.ikc`
- `IronKernel: Refresh Environment View`
- `IronKernel: Select Capability Profile`
- `IronKernel: Open Playground`
- `IronKernel: Show Output`

Expand Down
40 changes: 39 additions & 1 deletion editors/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@
"onCommand:ironkernel.compileFile",
"onCommand:ironkernel.runProject",
"onCommand:ironkernel.buildProject",
"onCommand:ironkernel.openPlayground"
"onCommand:ironkernel.openPlayground",
"onCommand:ironkernel.refreshEnvironment",
"onCommand:ironkernel.selectProfile",
"onView:ironkernelEnvironment"
],
"capabilities": {
"untrustedWorkspaces": {
Expand Down Expand Up @@ -122,6 +125,17 @@
"command": "ironkernel.showOutput",
"title": "IronKernel: Show Output",
"category": "IronKernel"
},
{
"command": "ironkernel.refreshEnvironment",
"title": "IronKernel: Refresh Environment View",
"category": "IronKernel",
"icon": "$(refresh)"
},
{
"command": "ironkernel.selectProfile",
"title": "IronKernel: Select Capability Profile",
"category": "IronKernel"
}
],
"menus": {
Expand Down Expand Up @@ -155,6 +169,13 @@
"when": "resourceExtname == .ikproj && isWorkspaceTrusted",
"group": "ironkernel@2"
}
],
"view/title": [
{
"command": "ironkernel.refreshEnvironment",
"when": "view == ironkernelEnvironment",
"group": "navigation"
}
]
},
"configurationDefaults": {
Expand Down Expand Up @@ -283,6 +304,23 @@
],
"pattern": "$ironkernel"
}
],
"views": {
"explorer": [
{
"id": "ironkernelEnvironment",
"name": "IronKernel Environment",
"when": "ironkernel.environmentAvailable",
"icon": "$(symbol-namespace)",
"contextualTitle": "IronKernel Environment"
}
]
},
"viewsWelcome": [
{
"view": "ironkernelEnvironment",
"contents": "The IronKernel language server supplies this view. Open an .ikr file in a trusted workspace to populate it."
}
]
},
"scripts": {
Expand Down
124 changes: 124 additions & 0 deletions editors/vscode/src/environmentView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import * as vscode from "vscode";

export interface EnvironmentSymbol {
name: string;
class: string;
detail?: string;
}

export interface EnvironmentFrame {
label: string;
symbols: EnvironmentSymbol[];
}

export interface EnvironmentReport {
capabilities: string[];
frames: EnvironmentFrame[];
}

export type EnvironmentRequester = (
uri: string | undefined
) => Promise<EnvironmentReport | undefined>;

export type EnvironmentNode =
| { type: "capabilities" }
| { type: "capability"; label: string }
| { type: "frame"; frame: EnvironmentFrame }
| { type: "symbol"; symbol: EnvironmentSymbol };

function symbolIcon(symbolClass: string): vscode.ThemeIcon {
switch (symbolClass) {
case "operative":
return new vscode.ThemeIcon("symbol-keyword");
case "applicative":
return new vscode.ThemeIcon("symbol-function");
default:
return new vscode.ThemeIcon("symbol-variable");
}
}

/**
* The environment inspector: capabilities and the frame chain, served by the
* language server's ironkernel/environment request. Frame structure is
* host-side by design — the runtime deliberately exposes no parent
* environments to Kernel code.
*/
export class EnvironmentViewProvider implements vscode.TreeDataProvider<EnvironmentNode> {
private report: EnvironmentReport | undefined;
private readonly emitter = new vscode.EventEmitter<EnvironmentNode | undefined | void>();
readonly onDidChangeTreeData = this.emitter.event;

constructor(private readonly request: EnvironmentRequester) {}

async refresh(uri: string | undefined): Promise<void> {
this.report = await this.request(uri);
this.emitter.fire();
}

getTreeItem(node: EnvironmentNode): vscode.TreeItem {
switch (node.type) {
case "capabilities": {
const count = this.report?.capabilities.length ?? 0;
const item = new vscode.TreeItem(
`Capabilities (${count})`,
count > 0
? vscode.TreeItemCollapsibleState.Collapsed
: vscode.TreeItemCollapsibleState.None
);
item.iconPath = new vscode.ThemeIcon("shield");
item.tooltip =
count > 0
? "Host authority carried by the session environment"
: "No host authority: the minimal profile";
return item;
}
case "capability": {
const item = new vscode.TreeItem(node.label, vscode.TreeItemCollapsibleState.None);
item.iconPath = new vscode.ThemeIcon("key");
return item;
}
case "frame": {
const item = new vscode.TreeItem(
node.frame.label,
node.frame.symbols.length > 0
? node.frame.label === "buffer"
? vscode.TreeItemCollapsibleState.Expanded
: vscode.TreeItemCollapsibleState.Collapsed
: vscode.TreeItemCollapsibleState.None
);
item.iconPath = new vscode.ThemeIcon("symbol-namespace");
item.description = `${node.frame.symbols.length}`;
return item;
}
case "symbol": {
const item = new vscode.TreeItem(node.symbol.name, vscode.TreeItemCollapsibleState.None);
item.iconPath = symbolIcon(node.symbol.class);
item.description = node.symbol.class;
if (node.symbol.detail) {
item.tooltip = node.symbol.detail;
}
return item;
}
}
}

getChildren(node?: EnvironmentNode): EnvironmentNode[] {
if (!this.report) {
return [];
}
if (!node) {
return [
{ type: "capabilities" },
...this.report.frames.map((frame): EnvironmentNode => ({ type: "frame", frame }))
];
}
switch (node.type) {
case "capabilities":
return this.report.capabilities.map((label) => ({ type: "capability", label }));
case "frame":
return node.frame.symbols.map((symbol) => ({ type: "symbol", symbol }));
default:
return [];
}
}
}
Loading
Loading