The engine gains `engine::editor`, the language service an editor needs, pure and with no protocol in it: - diagnostics: the reader's refusal on its line (one mapping function, `reader_diagnostic`, where stable codes and exact spans plug in), and the critique's smells on theirs, with their kind as the code; a generator is read at the given inputs, or 4; - completion of keywords, the standard gates (with parameter placeholders), and the registers, gates, subroutines and variables in scope, by what the cursor's place takes; snippets, and sample programs in an empty file; - hover: each standard gate's matrix written exactly, held entry by entry to the engine's own matrix under all 46 names the reader accepts; a user gate's matrix from the engine's matrices of its gates; each gate's cost as the engine counts it (two-qubit gates, T count, depth), at the call's angles or at generic ones, said which; a register's size; - definition, references, highlights and rename over an index of declarations and scopes that reads the whole of OpenQASM 3, and a half-typed program; symbols, folding (blocks, comments, held blocks); - a formatter with one layout, refusing a program whose brackets do not match, checked to keep every token, and tested stable and meaning-preserving (the engine reads the formatted program to the same circuit) on a corpus in every part of the language; - lenses for Run, Plan, Critique, Shortcuts and Receipt. The command line gains two verbs. `wai-quantum lsp` serves it over standard input and output, UTF-16 or UTF-8 positions as agreed, the commands run in-process on a thread of their own and signed with the user's key; `wai-quantum fmt` formats, `--check`s and `--write`s. tests/lsp.rs drives the binary through a scripted session from initialize to exit. Co-Authored-By: Claude Opus 5.5 <[email protected]>
642 lines
30 KiB
Rust
642 lines
30 KiB
Rust
//! `wai-quantum lsp`: the engine's language service for editors, as a server
|
|
//! of the language server protocol on standard input and output (each message
|
|
//! a `Content-Length` header and its JSON).
|
|
//!
|
|
//! What a program means is the engine's to say: diagnostics are its reader's
|
|
//! refusal and its critique's smells, hover shows its matrices and its
|
|
//! counts, and the commands (Run, Plan, Critique, Shortcuts, Receipt) run
|
|
//! here, through the engine the command line uses, a run signed with this
|
|
//! user's key as `wai-quantum run` signs it. Nothing leaves the machine.
|
|
//!
|
|
//! The service itself is [`wai_quantum::engine::editor`]; this module only
|
|
//! reads and writes the protocol, keeps the open documents, and turns byte
|
|
//! offsets into the editor's positions (UTF-16 by default, UTF-8 when the
|
|
//! editor offers it).
|
|
|
|
use serde_json::{Value, json};
|
|
use std::collections::BTreeMap;
|
|
use std::io::{BufRead, Write};
|
|
use std::sync::{Arc, Mutex};
|
|
use wai_quantum::ed25519_dalek::SigningKey;
|
|
use wai_quantum::engine::editor::{self, Analysis, Severity, index::DeclKind};
|
|
use wai_quantum::engine::{catalog, critique, job, shortcuts};
|
|
use wai_quantum::qasm;
|
|
|
|
/// What the commands run with: the memory a job may take, and the key a run is signed with.
|
|
pub struct Config {
|
|
pub memory_gb: u64,
|
|
pub key: SigningKey,
|
|
}
|
|
|
|
/// The settings an editor may give, in `initializationOptions` or a
|
|
/// configuration change, at the top level or under `wai-quantum`: the values a
|
|
/// generator is read at, and the shots a run samples.
|
|
#[derive(Clone, Debug, Default, PartialEq)]
|
|
pub struct Settings {
|
|
pub inputs: qasm::Inputs,
|
|
pub shots: Option<u64>,
|
|
}
|
|
|
|
impl Settings {
|
|
fn read(v: &Value) -> Option<Settings> {
|
|
let s = ["wai-quantum", "waiQuantum"].iter().find_map(|k| v.get(*k)).unwrap_or(v);
|
|
if !s.is_object() {
|
|
return None;
|
|
}
|
|
let inputs = s.get("inputs").and_then(Value::as_object).map(|o| o.iter().filter_map(|(k, v)| Some((k.clone(), v.as_i64()?))).collect()).unwrap_or_default();
|
|
Some(Settings { inputs, shots: s.get("shots").and_then(Value::as_u64) })
|
|
}
|
|
}
|
|
|
|
struct Doc {
|
|
version: Value,
|
|
analysis: Analysis,
|
|
}
|
|
|
|
type Out = Arc<Mutex<Box<dyn Write + Send>>>;
|
|
|
|
struct Server {
|
|
out: Out,
|
|
docs: BTreeMap<String, Doc>,
|
|
utf8: bool,
|
|
snippets: bool,
|
|
settings: Settings,
|
|
cfg: Arc<Config>,
|
|
initialized: bool,
|
|
shut_down: bool,
|
|
workers: Vec<std::thread::JoinHandle<()>>,
|
|
}
|
|
|
|
/// Writes one message.
|
|
fn send(out: &Out, msg: &Value) {
|
|
let body = msg.to_string();
|
|
if let Ok(mut w) = out.lock() {
|
|
let _ = write!(w, "Content-Length: {}\r\n\r\n{body}", body.len()).and_then(|_| w.flush());
|
|
}
|
|
}
|
|
|
|
/// Reads one message: `Ok(None)` at the end of the input, `Err` for a body that is not JSON.
|
|
fn read_message(r: &mut impl BufRead) -> std::io::Result<Option<Result<Value, String>>> {
|
|
let mut len: Option<usize> = None;
|
|
loop {
|
|
let mut line = String::new();
|
|
if r.read_line(&mut line)? == 0 {
|
|
return Ok(None);
|
|
}
|
|
let line = line.trim_end_matches(['\r', '\n']);
|
|
if line.is_empty() {
|
|
if len.is_some() {
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
if let Some((k, v)) = line.split_once(':')
|
|
&& k.trim().eq_ignore_ascii_case("content-length")
|
|
{
|
|
len = v.trim().parse().ok();
|
|
}
|
|
}
|
|
let mut buf = vec![0u8; len.unwrap_or(0)];
|
|
r.read_exact(&mut buf)?;
|
|
Ok(Some(serde_json::from_slice(&buf).map_err(|e| e.to_string())))
|
|
}
|
|
|
|
/// The protocol's error codes this server answers with.
|
|
const METHOD_NOT_FOUND: i64 = -32601;
|
|
const INVALID_PARAMS: i64 = -32602;
|
|
const INTERNAL_ERROR: i64 = -32603;
|
|
const NOT_INITIALIZED: i64 = -32002;
|
|
const REQUEST_FAILED: i64 = -32803;
|
|
|
|
/// Serves on standard input and output until the editor says `exit`; the
|
|
/// process's exit status (0 after `shutdown`, 1 otherwise).
|
|
pub fn serve(memory_gb: u64, key: SigningKey) -> i32 {
|
|
let stdin = std::io::stdin();
|
|
serve_on(stdin.lock(), Box::new(std::io::stdout()), Config { memory_gb, key })
|
|
}
|
|
|
|
/// [`serve`] on any reader and writer.
|
|
pub fn serve_on(mut reader: impl BufRead, writer: Box<dyn Write + Send>, cfg: Config) -> i32 {
|
|
let mut s = Server {
|
|
out: Arc::new(Mutex::new(writer)),
|
|
docs: BTreeMap::new(),
|
|
utf8: false,
|
|
snippets: false,
|
|
settings: Settings::default(),
|
|
cfg: Arc::new(cfg),
|
|
initialized: false,
|
|
shut_down: false,
|
|
workers: Vec::new(),
|
|
};
|
|
let code = loop {
|
|
let msg = match read_message(&mut reader) {
|
|
Ok(Some(Ok(m))) => m,
|
|
Ok(Some(Err(e))) => {
|
|
send(&s.out, &json!({ "jsonrpc": "2.0", "id": null, "error": { "code": -32700, "message": format!("not JSON: {e}") } }));
|
|
continue;
|
|
}
|
|
Ok(None) | Err(_) => break 1,
|
|
};
|
|
let method = msg.get("method").and_then(Value::as_str).unwrap_or("").to_string();
|
|
if method == "exit" {
|
|
break if s.shut_down { 0 } else { 1 };
|
|
}
|
|
let params = msg.get("params").cloned().unwrap_or(Value::Null);
|
|
match msg.get("id").cloned() {
|
|
Some(id) => {
|
|
let reply = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| s.request(&method, ¶ms, &id)));
|
|
match reply {
|
|
Ok(Some(Ok(result))) => send(&s.out, &json!({ "jsonrpc": "2.0", "id": id, "result": result })),
|
|
Ok(Some(Err((code, message)))) => send(&s.out, &json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": message } })),
|
|
// A command answers from its own thread.
|
|
Ok(None) => {}
|
|
Err(_) => send(&s.out, &json!({ "jsonrpc": "2.0", "id": id, "error": { "code": INTERNAL_ERROR, "message": format!("{method} failed inside the server") } })),
|
|
}
|
|
}
|
|
None => {
|
|
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| s.notification(&method, ¶ms)));
|
|
}
|
|
}
|
|
};
|
|
for w in s.workers.drain(..) {
|
|
let _ = w.join();
|
|
}
|
|
code
|
|
}
|
|
|
|
type Reply = Option<Result<Value, (i64, String)>>;
|
|
|
|
impl Server {
|
|
fn capabilities(&self) -> Value {
|
|
json!({
|
|
"positionEncoding": if self.utf8 { "utf-8" } else { "utf-16" },
|
|
"textDocumentSync": { "openClose": true, "change": 1, "save": false },
|
|
"completionProvider": { "resolveProvider": false },
|
|
"hoverProvider": true,
|
|
"definitionProvider": true,
|
|
"referencesProvider": true,
|
|
"documentHighlightProvider": true,
|
|
"renameProvider": { "prepareProvider": true },
|
|
"documentSymbolProvider": true,
|
|
"foldingRangeProvider": true,
|
|
"documentFormattingProvider": true,
|
|
"codeLensProvider": { "resolveProvider": false },
|
|
"executeCommandProvider": { "commands": editor::COMMANDS.iter().map(|c| c.0).collect::<Vec<_>>() },
|
|
})
|
|
}
|
|
|
|
fn request(&mut self, method: &str, params: &Value, id: &Value) -> Reply {
|
|
if method == "initialize" {
|
|
let encodings = params.pointer("/capabilities/general/positionEncodings").and_then(Value::as_array).cloned().unwrap_or_default();
|
|
self.utf8 = encodings.iter().any(|e| e == "utf-8");
|
|
self.snippets = params.pointer("/capabilities/textDocument/completion/completionItem/snippetSupport") == Some(&Value::Bool(true));
|
|
if let Some(s) = params.get("initializationOptions").and_then(Settings::read) {
|
|
self.settings = s;
|
|
}
|
|
self.initialized = true;
|
|
return Some(Ok(json!({ "capabilities": self.capabilities(), "serverInfo": { "name": "wai-quantum", "version": env!("CARGO_PKG_VERSION") } })));
|
|
}
|
|
if !self.initialized {
|
|
return Some(Err((NOT_INITIALIZED, "the server has not been initialized".into())));
|
|
}
|
|
if method == "shutdown" {
|
|
self.shut_down = true;
|
|
return Some(Ok(Value::Null));
|
|
}
|
|
if method == "workspace/executeCommand" {
|
|
return self.execute(params, id);
|
|
}
|
|
let uri = params.pointer("/textDocument/uri").and_then(Value::as_str).unwrap_or("").to_string();
|
|
let Some(doc) = self.docs.get(&uri) else {
|
|
return Some(match method {
|
|
m if m.starts_with("textDocument/") => Err((INVALID_PARAMS, format!("{uri} is not open"))),
|
|
_ => Err((METHOD_NOT_FOUND, format!("no method {method}"))),
|
|
});
|
|
};
|
|
let a = &doc.analysis;
|
|
let at = || params.get("position").map(|p| self.offset(a, p)).unwrap_or(0);
|
|
let loc = |s: editor::index::Span| json!({ "uri": uri, "range": self.range(a, s) });
|
|
Some(Ok(match method {
|
|
"textDocument/completion" => {
|
|
let items: Vec<Value> = editor::complete(a, at()).iter().map(|i| self.item(i)).collect();
|
|
json!({ "isIncomplete": false, "items": items })
|
|
}
|
|
"textDocument/hover" => match editor::hover(a, at()) {
|
|
Some(h) => json!({ "contents": { "kind": "markdown", "value": h.markdown }, "range": self.range(a, h.span) }),
|
|
None => Value::Null,
|
|
},
|
|
"textDocument/definition" => editor::definition(a, at()).map(loc).unwrap_or(Value::Null),
|
|
"textDocument/references" => {
|
|
let with_decl = params.pointer("/context/includeDeclaration") != Some(&Value::Bool(false));
|
|
Value::Array(editor::references(a, at(), with_decl).into_iter().map(loc).collect())
|
|
}
|
|
"textDocument/documentHighlight" => {
|
|
let spans = editor::references(a, at(), true);
|
|
Value::Array(spans.iter().enumerate().map(|(i, &s)| json!({ "range": self.range(a, s), "kind": if i == 0 { 3 } else { 2 } })).collect())
|
|
}
|
|
"textDocument/prepareRename" => match editor::prepare_rename(a, at()) {
|
|
Ok((span, name)) => json!({ "range": self.range(a, span), "placeholder": name }),
|
|
Err(e) => return Some(Err((REQUEST_FAILED, e))),
|
|
},
|
|
"textDocument/rename" => {
|
|
let new = params.get("newName").and_then(Value::as_str).unwrap_or("");
|
|
match editor::rename(a, at(), new) {
|
|
Ok(spans) => {
|
|
let edits: Vec<Value> = spans.iter().map(|&s| json!({ "range": self.range(a, s), "newText": new })).collect();
|
|
json!({ "changes": { uri.clone(): edits } })
|
|
}
|
|
Err(e) => return Some(Err((REQUEST_FAILED, e))),
|
|
}
|
|
}
|
|
"textDocument/documentSymbol" => Value::Array(editor::symbols(a).iter().map(|s| self.symbol(a, s)).collect()),
|
|
"textDocument/foldingRange" => Value::Array(
|
|
editor::folding(a)
|
|
.iter()
|
|
.map(|f| {
|
|
let mut v = json!({ "startLine": f.start, "endLine": f.end });
|
|
if let Some(k) = f.kind {
|
|
v["kind"] = json!(k);
|
|
}
|
|
v
|
|
})
|
|
.collect(),
|
|
),
|
|
"textDocument/formatting" => match editor::format(&a.src) {
|
|
Ok(f) if f == a.src => json!([]),
|
|
Ok(f) => json!([{ "range": self.range(a, (0, a.src.len())), "newText": f }]),
|
|
Err(e) => return Some(Err((REQUEST_FAILED, e))),
|
|
},
|
|
"textDocument/codeLens" => Value::Array(
|
|
editor::lenses(a).iter().map(|l| json!({ "range": self.range(a, l.span), "command": { "title": l.title, "command": l.command, "arguments": [uri] } })).collect(),
|
|
),
|
|
_ => return Some(Err((METHOD_NOT_FOUND, format!("no method {method}")))),
|
|
}))
|
|
}
|
|
|
|
fn notification(&mut self, method: &str, params: &Value) {
|
|
let uri = params.pointer("/textDocument/uri").and_then(Value::as_str).unwrap_or("").to_string();
|
|
match method {
|
|
"textDocument/didOpen" => {
|
|
let text = params.pointer("/textDocument/text").and_then(Value::as_str).unwrap_or("");
|
|
let version = params.pointer("/textDocument/version").cloned().unwrap_or(Value::Null);
|
|
self.docs.insert(uri.clone(), Doc { version, analysis: Analysis::new(text) });
|
|
self.publish(&uri);
|
|
}
|
|
"textDocument/didChange" => {
|
|
let Some(doc) = self.docs.get_mut(&uri) else { return };
|
|
let mut text = doc.analysis.src.clone();
|
|
for c in params.get("contentChanges").and_then(Value::as_array).into_iter().flatten() {
|
|
let new = c.get("text").and_then(Value::as_str).unwrap_or("");
|
|
match c.get("range") {
|
|
Some(r) => {
|
|
let a = Analysis::new(&text);
|
|
let (s, e) = (offset_in(&a, &r["start"], self.utf8), offset_in(&a, &r["end"], self.utf8));
|
|
text.replace_range(s.min(e)..e.max(s), new);
|
|
}
|
|
None => text = new.to_string(),
|
|
}
|
|
}
|
|
doc.version = params.pointer("/textDocument/version").cloned().unwrap_or(Value::Null);
|
|
doc.analysis = Analysis::new(&text);
|
|
self.publish(&uri);
|
|
}
|
|
"textDocument/didClose" => {
|
|
self.docs.remove(&uri);
|
|
send(&self.out, &json!({ "jsonrpc": "2.0", "method": "textDocument/publishDiagnostics", "params": { "uri": uri, "diagnostics": [] } }));
|
|
}
|
|
"workspace/didChangeConfiguration" => {
|
|
if let Some(s) = params.get("settings").and_then(Settings::read) {
|
|
self.settings = s;
|
|
let uris: Vec<String> = self.docs.keys().cloned().collect();
|
|
for u in uris {
|
|
self.publish(&u);
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn publish(&self, uri: &str) {
|
|
let Some(doc) = self.docs.get(uri) else { return };
|
|
let a = &doc.analysis;
|
|
let diags: Vec<Value> = editor::diagnostics(a, &self.settings.inputs)
|
|
.iter()
|
|
.map(|d| {
|
|
let severity = match d.severity {
|
|
Severity::Error => 1,
|
|
Severity::Warning => 2,
|
|
Severity::Information => 3,
|
|
Severity::Hint => 4,
|
|
};
|
|
let mut v = json!({ "range": self.range(a, d.span), "severity": severity, "source": "wai-quantum", "message": d.message });
|
|
if let Some(c) = &d.code {
|
|
v["code"] = json!(c);
|
|
}
|
|
v
|
|
})
|
|
.collect();
|
|
send(&self.out, &json!({ "jsonrpc": "2.0", "method": "textDocument/publishDiagnostics", "params": { "uri": uri, "version": doc.version, "diagnostics": diags } }));
|
|
}
|
|
|
|
fn offset(&self, a: &Analysis, p: &Value) -> usize {
|
|
offset_in(a, p, self.utf8)
|
|
}
|
|
|
|
fn position(&self, a: &Analysis, off: usize) -> Value {
|
|
position_in(a, off, self.utf8)
|
|
}
|
|
|
|
fn range(&self, a: &Analysis, s: editor::index::Span) -> Value {
|
|
json!({ "start": self.position(a, s.0), "end": self.position(a, s.1) })
|
|
}
|
|
|
|
fn item(&self, i: &editor::Item) -> Value {
|
|
use editor::ItemKind::*;
|
|
let kind = match i.kind {
|
|
Keyword => 14,
|
|
Gate | Subroutine => 3,
|
|
Register | Variable => 6,
|
|
Constant => 21,
|
|
Function => 3,
|
|
Parameter => 25,
|
|
Snippet => 15,
|
|
};
|
|
let mut v = json!({ "label": i.label, "kind": kind, "detail": i.detail, "sortText": i.sort });
|
|
if let Some(d) = &i.doc {
|
|
v["documentation"] = json!({ "kind": "markdown", "value": d });
|
|
}
|
|
match (&i.snippet, &i.plain, self.snippets) {
|
|
(Some(s), _, true) => {
|
|
v["insertText"] = json!(s);
|
|
v["insertTextFormat"] = json!(2);
|
|
}
|
|
(_, Some(p), _) => v["insertText"] = json!(p),
|
|
_ => {}
|
|
}
|
|
v
|
|
}
|
|
|
|
fn symbol(&self, a: &Analysis, s: &editor::Symbol) -> Value {
|
|
let kind = match s.kind {
|
|
DeclKind::QubitRegister | DeclKind::BitRegister => 18,
|
|
DeclKind::Gate | DeclKind::Subroutine | DeclKind::Extern => 12,
|
|
DeclKind::Constant => 14,
|
|
DeclKind::GateParameter | DeclKind::Argument => 26,
|
|
_ => 13,
|
|
};
|
|
json!({
|
|
"name": s.name,
|
|
"detail": s.detail,
|
|
"kind": kind,
|
|
"range": self.range(a, s.span),
|
|
"selectionRange": self.range(a, s.name_span),
|
|
"children": s.children.iter().map(|c| self.symbol(a, c)).collect::<Vec<_>>(),
|
|
})
|
|
}
|
|
|
|
/// A command: run on a thread of its own, so the editor's other requests
|
|
/// are answered meanwhile; the answer is sent from there.
|
|
fn execute(&mut self, params: &Value, id: &Value) -> Reply {
|
|
let command = params.get("command").and_then(Value::as_str).unwrap_or("").to_string();
|
|
if !editor::COMMANDS.iter().any(|c| c.0 == command) {
|
|
return Some(Err((INVALID_PARAMS, format!("no command {command}; the commands are {}", editor::COMMANDS.iter().map(|c| c.0).collect::<Vec<_>>().join(", ")))));
|
|
}
|
|
let arg = params.pointer("/arguments/0").cloned().unwrap_or(Value::Null);
|
|
let uri = arg.as_str().or_else(|| arg.get("uri").and_then(Value::as_str)).unwrap_or("").to_string();
|
|
let Some(doc) = self.docs.get(&uri) else { return Some(Err((INVALID_PARAMS, format!("{uri} is not open")))) };
|
|
let src = doc.analysis.src.clone();
|
|
let (settings, cfg, out, id) = (self.settings.clone(), self.cfg.clone(), self.out.clone(), id.clone());
|
|
let work = move || {
|
|
let msg = match run_command(&command, &src, &settings, &cfg) {
|
|
Ok(r) => {
|
|
let level = if r["ok"] == true { 3 } else { 1 };
|
|
send(&out, &json!({ "jsonrpc": "2.0", "method": "window/logMessage", "params": { "type": level, "message": r["text"] } }));
|
|
send(&out, &json!({ "jsonrpc": "2.0", "method": "window/showMessage", "params": { "type": level, "message": r["summary"] } }));
|
|
json!({ "jsonrpc": "2.0", "id": id, "result": r })
|
|
}
|
|
Err(e) => json!({ "jsonrpc": "2.0", "id": id, "error": { "code": REQUEST_FAILED, "message": e } }),
|
|
};
|
|
send(&out, &msg);
|
|
};
|
|
type Work = Box<dyn FnOnce() + Send>;
|
|
let slot: Arc<Mutex<Option<Work>>> = Arc::new(Mutex::new(Some(Box::new(work))));
|
|
let theirs = slot.clone();
|
|
let spawned = std::thread::Builder::new().name("wai-quantum-command".into()).spawn(move || {
|
|
if let Some(w) = theirs.lock().ok().and_then(|mut g| g.take()) {
|
|
w()
|
|
}
|
|
});
|
|
match spawned {
|
|
Ok(h) => self.workers.push(h),
|
|
// Where there are no threads (a WASI build), the command runs here.
|
|
Err(_) => {
|
|
if let Some(w) = slot.lock().ok().and_then(|mut g| g.take()) {
|
|
w()
|
|
}
|
|
}
|
|
}
|
|
self.workers.retain(|h| !h.is_finished());
|
|
None
|
|
}
|
|
}
|
|
|
|
/// The byte offset of an editor's position.
|
|
fn offset_in(a: &Analysis, p: &Value, utf8: bool) -> usize {
|
|
let line = p.get("line").and_then(Value::as_u64).unwrap_or(0) as usize;
|
|
let ch = p.get("character").and_then(Value::as_u64).unwrap_or(0) as usize;
|
|
let Some(start) = a.lines.start(line) else { return a.src.len() };
|
|
let end = a.lines.start(line + 1).map(|e| e - 1).unwrap_or(a.src.len());
|
|
let text = &a.src[start..end];
|
|
let mut units = 0usize;
|
|
for (i, c) in text.char_indices() {
|
|
if units >= ch {
|
|
return start + i;
|
|
}
|
|
units += if utf8 { c.len_utf8() } else { c.len_utf16() };
|
|
}
|
|
end
|
|
}
|
|
|
|
/// An editor's position of a byte offset.
|
|
fn position_in(a: &Analysis, off: usize, utf8: bool) -> Value {
|
|
let off = off.min(a.src.len());
|
|
let line = a.line(off);
|
|
let start = a.lines.start(line).unwrap_or(0);
|
|
let text = &a.src[start..off];
|
|
let ch: usize = if utf8 { text.len() } else { text.chars().map(char::len_utf16).sum() };
|
|
json!({ "line": line, "character": ch })
|
|
}
|
|
|
|
/// A command's answer: `ok`, a one-line `summary`, the `text` a reader takes
|
|
/// in, and the engine's `json`.
|
|
fn run_command(command: &str, src: &str, settings: &Settings, cfg: &Config) -> Result<Value, String> {
|
|
let inputs = editor::read_inputs(src, &settings.inputs);
|
|
let at = if inputs.is_empty() { String::new() } else { format!(" at {}", editor::inputs_text(&inputs)) };
|
|
// A generator runs as the circuit it writes out at its inputs.
|
|
let program = || if inputs.is_empty() { Ok(src.to_string()) } else { qasm::instantiate(src, &inputs) };
|
|
let request = |qasm: String| {
|
|
let mut r = json!({ "qasm": qasm });
|
|
if let Some(s) = settings.shots {
|
|
r["shots"] = json!(s);
|
|
}
|
|
r
|
|
};
|
|
let answer = |ok: bool, summary: String, text: String, json: Value| json!({ "command": command, "ok": ok, "summary": summary, "text": text, "json": json });
|
|
match command {
|
|
"wai-quantum.run" | "wai-quantum.receipt" => {
|
|
let body = crate::run_here(&request(program()?), cfg.memory_gb, &cfg.key)?;
|
|
let (verified, key) = job::verify(&body)?;
|
|
let signer = key.as_str().and_then(job::did_of).unwrap_or_default();
|
|
let method = body["method"].as_str().unwrap_or("").to_string();
|
|
if command == "wai-quantum.run" {
|
|
let summary = format!("Run{at}: {method}, {}; the receipt {}", body["result"]["accuracy"]["class"].as_str().unwrap_or(""), if verified { "verifies" } else { "DOES NOT VERIFY" });
|
|
return Ok(answer(verified, summary, job_text(&body, verified, &signer), body));
|
|
}
|
|
let receipt = &body["result"]["receipt"];
|
|
let text = format!(
|
|
"receipt {}\nsigned by {signer}\nbinds the request and the result, as given below\nmethod {method}\n\n{}\n",
|
|
if verified { "verifies" } else { "DOES NOT VERIFY" },
|
|
serde_json::to_string_pretty(&body).unwrap_or_default()
|
|
);
|
|
let summary = format!("Receipt{at}: {} by {signer}, digest {}", if verified { "verifies, signed" } else { "DOES NOT VERIFY, signed" }, receipt["digest"].as_str().unwrap_or(""));
|
|
Ok(answer(verified, summary, text, body))
|
|
}
|
|
"wai-quantum.plan" => {
|
|
let p = catalog::plan(&request(program()?), cfg.memory_gb << 30)?;
|
|
let text = format!("method {}\nwhy {}\nqubits {}\nmemory {:.1} MiB at most\nwork {}\n", p.method, p.why, p.qubits, p.memory_bytes as f64 / (1u64 << 20) as f64, p.work);
|
|
let summary = format!("Plan{at}: {} — {}", p.method, p.why);
|
|
Ok(answer(true, summary, text, serde_json::to_value(&p).unwrap_or(Value::Null)))
|
|
}
|
|
"wai-quantum.critique" => {
|
|
let c = if inputs.is_empty() { critique::critique(src) } else { critique::critique_with(src, &inputs) };
|
|
if c["ok"] != true {
|
|
return Err(c["error"].as_str().unwrap_or("the program does not read").to_string());
|
|
}
|
|
let n = |k: &str| c[k].as_u64().unwrap_or(0);
|
|
let summary = format!("Critique{at}: {} gates, {} two-qubit, depth {}, T count {}, {} smell{}", n("gates"), n("two_qubit"), n("depth"), n("t_count"), c["smells"].as_array().map_or(0, Vec::len), if c["smells"].as_array().map_or(0, Vec::len) == 1 { "" } else { "s" });
|
|
Ok(answer(true, summary, critique_text(&c), c))
|
|
}
|
|
"wai-quantum.shortcuts" => {
|
|
let v = shortcuts::shortcuts(&program()?, false);
|
|
if v["ok"] != true {
|
|
return Err(v["error"].as_str().unwrap_or("the program does not read").to_string());
|
|
}
|
|
let summary = format!("Shortcuts{at}: {}", v["summary"].as_str().unwrap_or(""));
|
|
Ok(answer(true, summary, shortcuts_text(&v), v))
|
|
}
|
|
_ => Err(format!("no command {command}")),
|
|
}
|
|
}
|
|
|
|
fn s(v: &Value) -> String {
|
|
v.as_str().map(str::to_string).unwrap_or_else(|| v.to_string())
|
|
}
|
|
|
|
/// A job as a reader takes it in.
|
|
fn job_text(body: &Value, verified: bool, signer: &str) -> String {
|
|
let r = &body["result"];
|
|
let mut out = format!("method {} — {}\n", s(&body["method"]), s(&body["why"]));
|
|
if let Some(acc) = r.get("accuracy") {
|
|
out.push_str(&format!("accuracy {}{}\n", s(&acc["class"]), acc.get("note").map(|n| format!(" ({})", s(n))).unwrap_or_default()));
|
|
}
|
|
out.push_str(&format!("circuit {} qubits, {} gates\n", r["qubits"], r["gates"]));
|
|
if let Some(w) = r.get("warning") {
|
|
out.push_str(&format!("warning {}\n", s(w)));
|
|
}
|
|
if let Some(counts) = r.get("counts").and_then(Value::as_object) {
|
|
let total: u64 = counts.values().filter_map(Value::as_u64).sum::<u64>() + r["omitted_shots"].as_u64().unwrap_or(0);
|
|
out.push_str(&format!("counts {total} shots, {} outcomes\n", r["distinct_outcomes"].as_u64().unwrap_or(counts.len() as u64)));
|
|
let mut v: Vec<(&String, u64)> = counts.iter().map(|(k, c)| (k, c.as_u64().unwrap_or(0))).collect();
|
|
v.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
|
|
for (k, c) in v.iter().take(16) {
|
|
out.push_str(&format!(" {k} {c:>8} {:.4}\n", *c as f64 / total.max(1) as f64));
|
|
}
|
|
if v.len() > 16 {
|
|
out.push_str(&format!(" … {} more\n", v.len() - 16));
|
|
}
|
|
}
|
|
for e in r.get("expectations").and_then(Value::as_array).into_iter().flatten() {
|
|
out.push_str(&format!("⟨{}⟩ {}\n", s(&e["observable"]), s(&e["value"])));
|
|
}
|
|
out.push_str(&format!("receipt {}, signed by {signer}\n", if verified { "verifies" } else { "DOES NOT VERIFY" }));
|
|
out
|
|
}
|
|
|
|
/// A critique as a reader takes it in.
|
|
fn critique_text(c: &Value) -> String {
|
|
let n = |k: &str| c[k].as_u64().unwrap_or(0);
|
|
let mut out = format!(
|
|
"{} qubits, {} gates ({} one-qubit, {} two-qubit, {} three-qubit), depth {}, two-qubit depth {}\n{} T gates; {}; {} qubits measured\n",
|
|
n("qubits"),
|
|
n("gates"),
|
|
n("one_qubit"),
|
|
n("two_qubit"),
|
|
n("three_qubit"),
|
|
n("depth"),
|
|
n("two_qubit_depth"),
|
|
n("t_count"),
|
|
if c["clifford"] == true { "a Clifford circuit".to_string() } else { format!("{} gates off the Clifford angles", n("non_clifford")) },
|
|
n("measured")
|
|
);
|
|
let f = &c["fidelity_model"];
|
|
out.push_str(&format!("fidelity {:.4}, modelled: {}\n", f["estimate"].as_f64().unwrap_or(f64::NAN), s(&f["note"])));
|
|
for sm in c["smells"].as_array().into_iter().flatten() {
|
|
out.push_str(&format!("{}: {}{}\n", s(&sm["kind"]), s(&sm["message"]), if sm["fixable"] == true { " (improve fixes it)" } else { "" }));
|
|
}
|
|
out
|
|
}
|
|
|
|
/// The classical routes as a reader takes them in.
|
|
fn shortcuts_text(v: &Value) -> String {
|
|
let mut out = format!("{}\n\n", s(&v["summary"]));
|
|
for r in v["routes"].as_array().into_iter().flatten() {
|
|
let applies = r["applies"] == true;
|
|
let cost = match (applies, r.get("log2_work").and_then(Value::as_f64)) {
|
|
(true, Some(w)) => format!("2^{w:.2} operations"),
|
|
(true, None) => "—".into(),
|
|
_ => "no".into(),
|
|
};
|
|
out.push_str(&format!("{:<15} {cost:<22} {}\n", s(&r["route"]), s(&r["why"])));
|
|
}
|
|
out.push_str(&format!("\n{}\n", s(&v["note"])));
|
|
out
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn positions_count_utf16_units_or_bytes_as_agreed() {
|
|
let a = Analysis::new("rz(π) q;\nx q;\n");
|
|
// After `π`: four UTF-16 units in, five bytes.
|
|
let at = a.src.find(')').unwrap();
|
|
assert_eq!(position_in(&a, at, false), json!({ "line": 0, "character": 4 }));
|
|
assert_eq!(position_in(&a, at, true), json!({ "line": 0, "character": 5 }));
|
|
assert_eq!(offset_in(&a, &json!({ "line": 0, "character": 4 }), false), at);
|
|
assert_eq!(offset_in(&a, &json!({ "line": 0, "character": 5 }), true), at);
|
|
assert_eq!(offset_in(&a, &json!({ "line": 1, "character": 0 }), false), a.src.find("x q").unwrap());
|
|
// Past a line's end: its end; past the last line: the end.
|
|
assert_eq!(offset_in(&a, &json!({ "line": 1, "character": 99 }), false), a.src.len() - 1);
|
|
assert_eq!(offset_in(&a, &json!({ "line": 9, "character": 0 }), false), a.src.len());
|
|
}
|
|
|
|
#[test]
|
|
fn a_message_is_read_by_its_length() {
|
|
let body = r#"{"jsonrpc":"2.0","id":1,"method":"x"}"#;
|
|
let text = format!("Content-Length: {}\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n{body}Content-Length: 3\r\n\r\n{{x}}", body.len());
|
|
let mut r = std::io::Cursor::new(text.into_bytes());
|
|
assert_eq!(read_message(&mut r).unwrap().unwrap().unwrap()["id"], 1);
|
|
assert!(read_message(&mut r).unwrap().unwrap().is_err());
|
|
assert!(read_message(&mut r).unwrap().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn settings_are_read_at_the_top_or_under_the_server_s_name() {
|
|
let want = Settings { inputs: [("n".to_string(), 6)].into_iter().collect(), shots: Some(100) };
|
|
assert_eq!(Settings::read(&json!({ "inputs": { "n": 6 }, "shots": 100 })), Some(want.clone()));
|
|
assert_eq!(Settings::read(&json!({ "wai-quantum": { "inputs": { "n": 6 }, "shots": 100 } })), Some(want));
|
|
assert_eq!(Settings::read(&json!(null)), None);
|
|
}
|
|
}
|