Skip to main content

cove_wasm/
lib.rs

1//! Cove in a browser tab: the front end and the linear-memory backend behind
2//! a C ABI, with no server and no Cove runtime anywhere but the page.
3//!
4//! [Issue #241](https://github.com/myuon/cove/issues/241) asks for a
5//! playground. This crate is the half of it that is Rust; `web/` is the half
6//! that is a page. What it exports is described in [`abi`], and it is seven
7//! functions and one import.
8//!
9//! The fifth is [`debug_json`]: the same run, watched by a [`record`]ing
10//! debugger, so that the page can scrub through a timeline of it. A browser
11//! cannot be given the debugger `cove debug` is — a Web Worker cannot block
12//! waiting for the page — and [`record`] says at length why, and what
13//! recording keeps and loses instead.
14//!
15//! The sixth is [`lex_json`], which is the front end's first stage and
16//! nothing after it: the page colours its editor by asking the compiler's own
17//! lexer what each piece of the text is. [`highlight`] argues why that is the
18//! only version of syntax highlighting worth having here.
19//!
20//! The seventh is [`lex_ir_json`], the same idea for the other text the page
21//! shows: the disassembly [`cove_ir::print`] writes. That one has no lexer to
22//! borrow, so [`highlight`] reads the line shapes the printer documents and
23//! says, in `ok`, when it met a line it did not know — which is what
24//! `web/check.mjs` holds it to, against the real disassembly of every shipped
25//! sample.
26//!
27//! # What is the same as `cove run`, and what is not
28//!
29//! The same: the parser, the checker, the lowering, the VM, the diagnostics
30//! (rendered by [`cove_diag::render`], so the browser shows the sentences the
31//! CLI shows), and the shipped host schemas — so a program that uses `http`
32//! type-checks here exactly as it does on the command line, and is refused at
33//! the boundary here exactly as it is there without a grant.
34//!
35//! Not the same, and each for a reason a browser gives:
36//!
37//! - **No filesystem.** `files` and `documents` are the in-memory hosts the
38//!   differential harness uses, seeded empty; `process` is recorded; `http`
39//!   and `database` are denied. Nothing here can reach anything.
40//! - **No real clock host.** `clock` is [`VirtualTime`], which is what makes
41//!   `clock.sleep` finish at once and a program that measures itself
42//!   deterministic. The *run's* clock is a different thing and is real: see
43//!   [`RUN_LIMITS`].
44//! - **No tasks.** `spawn` is refused, with a span, in the runtime. A Cove
45//!   task is a thread (ADR 0008) and one Web Worker is one thread; the
46//!   alternative was to run a task's body inline, which would make this
47//!   answer differently from the tree-walking oracle, and the corpus is held
48//!   together by those two agreeing. `examples/tasks` does not run here, and
49//!   that is the honest outcome rather than a bug.
50
51pub mod abi;
52pub mod highlight;
53mod json;
54pub mod record;
55
56use std::collections::BTreeMap;
57use std::io::Write;
58use std::path::PathBuf;
59use std::rc::Rc;
60use std::sync::{Arc, Mutex};
61use std::time::Duration;
62
63use cove_diag::{Diagnostic, Severity, SourceMap};
64use cove_runtime::{
65    Budget, Cancellation, Clock, Console, Database, Documents, Env, Files, Grants, HostRegistry,
66    Http, Limits, Process, ProcessLog, RunOutcome, Runtime, ValueCapture, VirtualTime, Vm,
67};
68use cove_sema::{Compiler, Config, HostSchemas, Module, Package, Unit};
69
70/// The module a playground's one source file belongs to, and the function
71/// that is run.
72///
73/// A package on disk takes its module names from its directory names, and a
74/// page has no directories, so one is chosen here and written into the
75/// diagnostics the reader sees. `playground.main` reads as what it is.
76pub const MODULE: &str = "playground";
77
78/// The entry function looked for in [`MODULE`].
79pub const ENTRY: &str = "main";
80
81/// The path the one source file is filed under in the [`SourceMap`], which is
82/// what a diagnostic's header names.
83const PATH: &str = "playground/main.cove";
84
85/// What a run in the playground is granted.
86///
87/// Every host is registered, as `cove run` registers every host — a grant and
88/// not a registration is what decides, and a refusal that names the missing
89/// capability is a better answer than an operation that does not exist. These
90/// five are the ones whose in-memory implementations can honestly answer:
91/// `console` prints into a buffer the page shows, `clock` is virtual, `env`
92/// is empty, and `files` and `documents` start empty and live as long as the
93/// run does.
94///
95/// `http`, `database` and `process` are absent. They are registered denied or
96/// recorded, so a program that calls them is told it was not granted the
97/// capability rather than being told the module does not exist.
98pub const GRANTS: [&str; 5] = ["console", "clock", "env", "files", "documents"];
99
100/// What bounds a run that the page did not bound itself.
101///
102/// A page can pass its own fuel and deadline; this is what it gets when it
103/// passes neither. Both are set, and deliberately: a tab that is running a
104/// Cove program is a tab that is not repainting, and the two bounds fail
105/// differently — fuel is deterministic and portable within one backend, and
106/// the deadline is what catches a program that spends its time inside one
107/// long host call rather than in a loop.
108///
109/// The deadline is enforced against the clock the embedder imports, which is
110/// `performance.now()` in a page and under node. It is a real bound and not a
111/// decoration: `cove_runtime`'s `wallclock` module says why the import is
112/// required rather than defaulted.
113pub const RUN_LIMITS: (u64, u64) = (200_000_000, 5_000);
114
115/// Bytes a Cove program printed, readable after the run.
116///
117/// The idiom is `crates/cove-cli/tests/differential.rs`'s, because the
118/// question is the same one: run a program with nothing of the machine
119/// attached and read back what it said.
120#[derive(Clone, Default)]
121struct Buffer(Arc<Mutex<Vec<u8>>>);
122
123impl Buffer {
124    fn text(&self) -> String {
125        String::from_utf8_lossy(&self.0.lock().unwrap_or_else(|held| held.into_inner()))
126            .into_owned()
127    }
128}
129
130impl Write for Buffer {
131    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
132        self.0
133            .lock()
134            .unwrap_or_else(|held| held.into_inner())
135            .extend_from_slice(buf);
136        Ok(buf.len())
137    }
138
139    fn flush(&mut self) -> std::io::Result<()> {
140        Ok(())
141    }
142}
143
144/// One source file, checked and lowered as far as it goes.
145struct Front {
146    sources: SourceMap,
147    diagnostics: Vec<Diagnostic>,
148    /// The checked program and its lowering, or neither. They are kept
149    /// together because a [`Runtime`] holds the first and the VM runs the
150    /// second, and a run needs both to exist.
151    lowered: Option<(cove_sema::resolve::Program, cove_ir::Program)>,
152}
153
154impl Front {
155    /// Whether anything stopped this from being a program that could run.
156    fn failed(&self) -> bool {
157        self.lowered.is_none()
158    }
159}
160
161/// A front end that got no further than these diagnostics.
162fn stopped(sources: SourceMap, diagnostics: Vec<Diagnostic>) -> Front {
163    Front {
164        sources,
165        diagnostics,
166        lowered: None,
167    }
168}
169
170/// Parses, checks and lowers `source`, collecting whatever diagnostics each
171/// stage produced.
172///
173/// The stages are `cove run`'s, in `cove run`'s order, against
174/// `HostSchemas::new()` — the shipped set — for both the check and the
175/// lowering. Using a narrower set for one than the other would let a program
176/// pass the checker and fail the lowering over a host neither the page nor
177/// the reader ever mentioned.
178fn front(source: &str) -> Front {
179    let mut sources = SourceMap::new();
180    let path = PathBuf::from(PATH);
181    let file = sources.add(path.clone(), source.to_string());
182
183    let ast = match cove_syntax::parse_file(&sources, file) {
184        Ok(ast) => ast,
185        Err(diagnostics) => return stopped(sources, diagnostics),
186    };
187
188    let mut modules = BTreeMap::new();
189    modules.insert(
190        MODULE.to_string(),
191        Module {
192            name: MODULE.to_string(),
193            dir: PathBuf::from(MODULE),
194            units: vec![Unit { file, path, ast }],
195        },
196    );
197    // A playground program is a package of one module built by hand rather
198    // than loaded with `cove_sema::package::load`, so it has to attach the
199    // standard library itself, the same way `load` does — a call into
200    // `Array.isEmpty` has to find `std.array.isEmpty` here exactly as it
201    // would in a package read from disk.
202    let std_modules = match cove_sema::stdlib::attach(&mut sources) {
203        Ok(std_modules) => std_modules,
204        Err(diagnostics) => return stopped(sources, diagnostics),
205    };
206    for (name, module) in std_modules {
207        modules.insert(name, module);
208    }
209    let package = Package {
210        root: PathBuf::new(),
211        config: Config::default(),
212        modules,
213    };
214
215    let schemas = HostSchemas::new();
216    let checked = match Compiler::new().with_schemas(schemas).compile(&package) {
217        Ok(checked) => checked,
218        Err(diagnostics) => return stopped(sources, diagnostics),
219    };
220
221    match cove_ir::lower_entry(&checked, &sources, &HostSchemas::new(), MODULE, ENTRY) {
222        Ok(program) => Front {
223            sources,
224            diagnostics: Vec::new(),
225            lowered: Some((checked, program)),
226        },
227        Err(diagnostics) => stopped(sources, diagnostics),
228    }
229}
230
231/// One diagnostic as JSON: what the CLI would have printed, plus the two
232/// fields a page needs in order to sort and count without re-parsing the
233/// printed form.
234fn diagnostic_json(sources: &SourceMap, diagnostic: &Diagnostic) -> String {
235    json::object([
236        (
237            "severity",
238            json::string(match diagnostic.severity {
239                Severity::Error => "error",
240                Severity::Warning => "warning",
241                Severity::Note => "note",
242            }),
243        ),
244        ("code", json::string(&diagnostic.code)),
245        ("message", json::string(&diagnostic.message)),
246        (
247            "rendered",
248            json::string(&cove_diag::render(sources, diagnostic)),
249        ),
250    ])
251}
252
253fn diagnostics_json(sources: &SourceMap, diagnostics: &[Diagnostic]) -> String {
254    json::array(
255        diagnostics
256            .iter()
257            .map(|diagnostic| diagnostic_json(sources, diagnostic)),
258    )
259}
260
261/// Checks and lowers `source`, and answers what a reader would want to see
262/// before running it.
263///
264/// ```json
265/// {"ok":bool,"diagnostics":[...],"ir":string|null}
266/// ```
267///
268/// `ok` is "nothing stopped this from running", which is not the same as "no
269/// diagnostics": a warning leaves `ok` true and is still shown. `ir` is
270/// this crate's own rendering of the lowered program's disassembly, minus
271/// the standard library, and is `null` for a source that did not reach the
272/// lowering.
273pub fn compile_json(source: &str) -> String {
274    let front = front(source);
275    json::object([
276        ("ok", (!front.failed()).to_string()),
277        (
278            "diagnostics",
279            diagnostics_json(&front.sources, &front.diagnostics),
280        ),
281        (
282            "ir",
283            json::or_null(
284                front
285                    .lowered
286                    .as_ref()
287                    .map(|(_, program)| json::string(&disassembly(program))),
288            ),
289        ),
290    ])
291}
292
293/// The disassembly a person reads, which is [`cove_ir::print::program`]'s
294/// minus the standard library.
295///
296/// `cove_sema::stdlib::attach` puts a module in every package so a call into
297/// a builtin method that has moved into the standard library has a
298/// declaration to reach, and [`cove_ir::lower_entry`] gives *every*
299/// declaration of the package a [`cove_ir::Function`] — a stub for one
300/// nothing reaches, same as any unreached declaration a program's own author
301/// wrote. Both are right for what they are for: the package needs the
302/// module, and the lowering's whole-declaration accounting needs the stub.
303/// Neither is something a person asked to see when they open the playground
304/// and read what their own three-line program compiled to, so this is where
305/// the two facts above are read together and the standard library's
306/// functions are left out — the same reasoning `cove outline`, `cove api`
307/// and `cove check`'s summary already apply to the same module, for the
308/// same reason.
309fn disassembly(program: &cove_ir::Program) -> String {
310    let mut out = String::new();
311    for (index, function) in program.functions.iter().enumerate() {
312        if cove_sema::stdlib::module_names().contains(&&*function.module) {
313            continue;
314        }
315        if !out.is_empty() {
316            out.push('\n');
317        }
318        out.push_str(&cove_ir::print::function(
319            program,
320            cove_ir::FunctionId(index as u32),
321        ));
322    }
323    out
324}
325
326/// Lexes `source` and answers a colour for every part of it.
327///
328/// ```json
329/// {"ok":bool,"spans":[{"at":int,"len":int,"kind":string}]}
330/// ```
331///
332/// The spans *tile*: the first begins at zero, each begins where the last
333/// ended, and together they cover the source, so a page renders the whole
334/// editor by walking the list and slicing its own text. `at` and `len` count
335/// UTF-16 code units, which is what a JavaScript string is indexed in.
336///
337/// `kind` is one of `keyword`, `type`, `string`, `number`, `comment` and
338/// `plain`. [`highlight::Kind`] says what falls into each and which two are
339/// not decided by the lexer. Its seventh, `slot`, is a disassembly's and
340/// never appears here.
341///
342/// `ok` is whether the source lexed without complaint. It is false whenever
343/// the reader is part-way through typing a string literal, which is most of
344/// the time one is being typed, and the spans are still a tiling of what was
345/// sent: [`highlight`] says what a broken source is coloured as and why the
346/// answer is not "nothing".
347///
348/// This is the front end's first stage and none of the rest, so it costs a
349/// pass over the text and nothing else. That is what makes it the thing to
350/// call on every keystroke, where [`compile_json`] is not.
351pub fn lex_json(source: &str) -> String {
352    let painting = highlight::paint(source);
353    json::object([
354        ("ok", painting.ok.to_string()),
355        (
356            "spans",
357            json::array(painting.pieces.iter().map(|piece| {
358                json::object([
359                    ("at", piece.at.to_string()),
360                    ("len", piece.len.to_string()),
361                    ("kind", json::string(piece.kind.as_str())),
362                ])
363            })),
364        ),
365    ])
366}
367
368/// Colours a disassembly and answers a colour for every part of it.
369///
370/// ```json
371/// {"ok":bool,"spans":[{"at":int,"len":int,"kind":string}]}
372/// ```
373///
374/// The same shape [`lex_json`] answers, and for the same consumer: the spans
375/// tile the text in UTF-16 code units, so a page renders the pane by walking
376/// them and slicing the string it already has.
377///
378/// `text` is what [`compile_json`]'s `ir` field held. It is passed back in
379/// rather than coloured on the way out because a colouring belongs to the
380/// text a page is *showing*, and a page shows one disassembly while a run
381/// answers another every time it is asked; sending both together would put a
382/// second copy of the tiling into every run answer for the sake of saving a
383/// call that costs one pass over a string.
384///
385/// `kind` adds `slot` to [`lex_json`]'s six. `ok` means something different
386/// here: not that the text parsed — it was written by
387/// [`cove_ir::print::program`] and always does — but that every line of it
388/// was a line shape that module documents. A false answer is a printer this
389/// module has not caught up with, and [`highlight`] says where that is
390/// asserted.
391pub fn lex_ir_json(text: &str) -> String {
392    let painting = highlight::disassembly(text);
393    json::object([
394        ("ok", painting.ok.to_string()),
395        (
396            "spans",
397            json::array(painting.pieces.iter().map(|piece| {
398                json::object([
399                    ("at", piece.at.to_string()),
400                    ("len", piece.len.to_string()),
401                    ("kind", json::string(piece.kind.as_str())),
402                ])
403            })),
404        ),
405    ])
406}
407
408/// Checks, lowers and runs `source`, and answers what happened.
409///
410/// ```json
411/// {"ok":bool,"diagnostics":[...],"ir":string|null,"outcome":string|null,
412///  "stdout":string,"stderr":string,"answer":value|null,
413///  "instructions":int|null,"fuel":int|null}
414/// ```
415///
416/// `ir` is [`compile_json`]'s, repeated here so that one call fills every
417/// pane a page shows. A page that asked for the disassembly separately would
418/// be paying for the front end twice for one source, and the two answers
419/// could describe different text if the reader typed between them.
420///
421/// `outcome` is [`RunOutcome::as_str`], derived the way
422/// `crates/cove-cli/tests/differential.rs` derives it, so the name a page
423/// shows is the name a trace would have recorded. `answer` is the entry's
424/// value in [`cove_runtime::value_to_json`]'s encoding, which is this
425/// repository's existing answer to how a Cove value leaves Rust.
426///
427/// A source that did not compile is answered without being run, with the same
428/// `diagnostics` [`compile_json`] would have given: a page can call this one
429/// function and get both halves.
430///
431/// `fuel` and `deadline_ms` are `None` for "use [`RUN_LIMITS`]", not for "no
432/// bound". A playground that could be asked for an unbounded run would be a
433/// page with a hang button.
434pub fn run_json(source: &str, fuel: Option<u64>, deadline_ms: Option<u64>) -> String {
435    execute(source, fuel, deadline_ms, None)
436}
437
438/// Runs `source` as [`run_json`] does, watched by a [`record::Recorder`],
439/// and answers everything [`run_json`] answers plus the recording under
440/// `debug`.
441///
442/// ```json
443/// {…as run_json…,"debug":{"moments":[…],"functions":[…],"kept":int,
444///                         "limit":int,"bytes":int,"truncated":…}}
445/// ```
446///
447/// `debug` is `null` for a source that did not compile, because there was no
448/// run to record and an empty recording of a program that never started
449/// reads as a program that did nothing.
450///
451/// `moments` is how many moments to keep; zero asks for
452/// [`record::MOMENTS`], and anything past [`record::MOST_MOMENTS`] is
453/// clamped to it. [`record`]'s module documentation says what a moment is,
454/// what bounds it, and why a browser gets a recording rather than a
455/// debugger it can step.
456///
457/// # One blob, not pieces
458///
459/// A recording is much larger than a compile result, so the alternative was
460/// considered and refused: a first call answering the moments' outlines and
461/// a second answering one moment's detail. Two things decided it.
462///
463/// A paged ABI needs the module to *hold* the recording between calls, and
464/// [`abi`] exists partly to have no module-level state — its length prefix
465/// replaced a "how long was the last answer?" export precisely so that two
466/// calls in flight have nothing to race over. Holding a recording would put
467/// that back, and worse, because the state would now be the size of the
468/// recording rather than of a number.
469///
470/// And the size a page actually pays is not the size paging would save.
471/// What makes a recording large is repetition, and the two repeated things —
472/// a function's disassembly and its name — are interned into `functions`
473/// once each. What is left per moment is what genuinely differs between
474/// moments. A recording of the example program is a few tens of kilobytes;
475/// see `web/README.md` for what larger ones measure. The bound that keeps it
476/// from growing without limit is [`record::BYTES`], and a bound is a better
477/// answer to "this could be huge" than an ABI that hands over a huge thing
478/// slowly.
479pub fn debug_json(
480    source: &str,
481    fuel: Option<u64>,
482    deadline_ms: Option<u64>,
483    moments: usize,
484) -> String {
485    execute(source, fuel, deadline_ms, Some(moments))
486}
487
488/// Checks, lowers and runs `source`, recording it when `moments` is `Some`.
489///
490/// One function and not two so that a debugged run and a plain one are the
491/// same run: the same hosts, the same grants, the same limits, the same
492/// classification of how it ended. A second copy of this setup would be a
493/// second playground that agreed with the first until it did not.
494fn execute(
495    source: &str,
496    fuel: Option<u64>,
497    deadline_ms: Option<u64>,
498    moments: Option<usize>,
499) -> String {
500    let recording = moments.is_some();
501    let front = front(source);
502    let Some((checked, program)) = front.lowered else {
503        let mut fields = vec![
504            ("ok", "false".to_string()),
505            (
506                "diagnostics",
507                diagnostics_json(&front.sources, &front.diagnostics),
508            ),
509            ("ir", "null".to_string()),
510            ("outcome", "null".to_string()),
511            ("stdout", json::string("")),
512            ("stderr", json::string("")),
513            ("answer", "null".to_string()),
514            ("instructions", "null".to_string()),
515            ("fuel", "null".to_string()),
516        ];
517        if recording {
518            fields.push(("debug", "null".to_string()));
519        }
520        return json::object(fields);
521    };
522
523    let out = Buffer::default();
524    let err = Buffer::default();
525
526    let mut hosts = HostRegistry::new(Grants::new(GRANTS));
527    hosts.register(Box::new(Console::new(out.clone(), err.clone())));
528    hosts.register(Box::new(Env::new(BTreeMap::new())));
529    hosts.register(Box::new(Documents::in_memory(BTreeMap::new())));
530    hosts.register(Box::new(Clock::virtual_clock(VirtualTime::new())));
531    hosts.register(Box::new(Files::in_memory(BTreeMap::new())));
532    hosts.register(Box::new(Process::recorded(
533        Vec::new(),
534        BTreeMap::new(),
535        ProcessLog::new(),
536    )));
537    hosts.register(Box::new(Database::denied()));
538    hosts.register(Box::new(Http::denied()));
539    // A refused call should not send the reader to a `cove.toml` that a page
540    // does not have and cannot be given.
541    hosts.set_grant_source(cove_runtime::GrantSource::Sealed);
542
543    let limits = Limits {
544        fuel: Some(fuel.unwrap_or(RUN_LIMITS.0)),
545        deadline: Some(Duration::from_millis(deadline_ms.unwrap_or(RUN_LIMITS.1))),
546        max_host_calls: None,
547        max_call_depth: None,
548        // Refused in the runtime and refused again here, because the two say
549        // different things: this is the bound a host chose, and the runtime's
550        // refusal is the fact that there is no thread to give. A reader who
551        // raises this one still gets the honest sentence.
552        max_tasks: Some(0),
553    };
554    hosts.set_budget(Budget::with_cancellation(limits, Cancellation::new()));
555
556    let sources = Arc::new(front.sources);
557    let runtime = Runtime::new(Arc::new(checked), Arc::clone(&sources), Arc::new(hosts));
558
559    let program_disassembly = disassembly(&program);
560    let recorder = moments.map(|moments| record::Recorder::new(Arc::clone(&sources), moments));
561    let (answer, instructions, fuel_spent) = {
562        let mut vm = match &recorder {
563            Some(recorder) => Vm::debugged(&runtime, runtime.hosts(), &program, recorder),
564            None => Vm::new(&runtime, runtime.hosts(), &program),
565        };
566        let answer = vm.run_entry(MODULE, ENTRY, Vec::<Rc<str>>::new());
567        let instructions = vm.instructions();
568        let spent = runtime
569            .hosts()
570            .with_budget(|budget| budget.meter().fuel_spent());
571        (answer, instructions, spent)
572    };
573
574    let outcome = match &answer {
575        Ok(value) if value.is_err() => RunOutcome::Error,
576        Ok(_) => RunOutcome::Success,
577        Err(error) => error.outcome,
578    };
579    let diagnostics = match &answer {
580        Ok(_) => Vec::new(),
581        Err(error) => vec![error.to_diagnostic()],
582    };
583
584    let mut fields = vec![
585        ("ok", matches!(outcome, RunOutcome::Success).to_string()),
586        ("diagnostics", diagnostics_json(&sources, &diagnostics)),
587        ("ir", json::string(&program_disassembly)),
588        ("outcome", json::string(outcome.as_str())),
589        ("stdout", json::string(&out.text())),
590        ("stderr", json::string(&err.text())),
591        (
592            "answer",
593            json::or_null(
594                answer
595                    .as_ref()
596                    .ok()
597                    .map(|value| cove_runtime::value_to_json(value, ValueCapture::Full)),
598            ),
599        ),
600        ("instructions", instructions.to_string()),
601        (
602            "fuel",
603            json::or_null(fuel_spent.map(|spent| spent.to_string())),
604        ),
605    ];
606    if let Some(recorder) = &recorder {
607        fields.push(("debug", recorder.json()));
608    }
609    json::object(fields)
610}
611
612#[cfg(test)]
613mod tests {
614    use super::*;
615
616    /// The JSON string value of `key`, unescaped only as far as these tests
617    /// need — enough to compare a rendered diagnostic or a printed line.
618    ///
619    /// A parser would be the third-party dependency this crate exists partly
620    /// to avoid. What the tests need is "does the answer say this", and a
621    /// substring search over the escaped form answers it without one.
622    fn says(json: &str, fragment: &str) -> bool {
623        json.contains(fragment)
624    }
625
626    #[test]
627    fn a_program_that_compiles_answers_its_disassembly() {
628        let json = compile_json("export fn main() -> Int { 21 * 2 }");
629        assert!(says(&json, r#""ok":true"#), "{json}");
630        assert!(says(&json, r#""diagnostics":[]"#), "{json}");
631        assert!(says(&json, "playground.main"), "{json}");
632    }
633
634    /// The point of rendering with [`cove_diag::render`] rather than printing
635    /// the message: what the page shows is the caret and the snippet the CLI
636    /// shows, over the path the source was filed under.
637    #[test]
638    fn a_program_that_does_not_parse_answers_the_rendered_diagnostic() {
639        let json = compile_json("export fn main() -> Int { 1 +");
640        assert!(says(&json, r#""ok":false"#), "{json}");
641        assert!(says(&json, r#""ir":null"#), "{json}");
642        assert!(says(&json, r#""severity":"error""#), "{json}");
643        assert!(says(&json, "playground/main.cove"), "{json}");
644    }
645
646    /// A source with no `main` is refused by name, and the refusal is a
647    /// rendered diagnostic like any other rather than a blank answer.
648    #[test]
649    fn a_source_without_the_entry_is_refused_by_name() {
650        let json = run_json("export fn other() -> Int { 1 }", None, None);
651        assert!(says(&json, r#""ok":false"#), "{json}");
652        assert!(
653            says(&json, "this package does not declare `playground.main`"),
654            "{json}"
655        );
656    }
657
658    #[test]
659    fn a_run_answers_what_the_entry_produced() {
660        let json = run_json("export fn main() -> Int { 21 * 2 }", None, None);
661        assert!(says(&json, r#""outcome":"success""#), "{json}");
662        assert!(
663            says(&json, r#""answer":{"type":"int","value":42}"#),
664            "{json}"
665        );
666    }
667
668    /// `console` is granted, and what a program prints is a string in the
669    /// answer rather than bytes that went nowhere.
670    #[test]
671    fn a_run_answers_what_the_program_printed() {
672        let json = run_json(
673            r#"use console.println
674
675export fn main() -> Result<Unit, Error> {
676  println("hello from the tab")?
677  Ok(())
678}"#,
679            None,
680            None,
681        );
682        assert!(says(&json, r#""outcome":"success""#), "{json}");
683        assert!(says(&json, r#""stdout":"hello from the tab\n""#), "{json}");
684    }
685
686    /// The bound a page can put on a loop, doing what it says.
687    #[test]
688    fn a_run_past_its_fuel_is_stopped_and_classified() {
689        let json = run_json(
690            "export fn main() -> Int {\n  var n = 0\n  while true { n = n + 1 }\n  n\n}",
691            Some(10_000),
692            None,
693        );
694        assert!(says(&json, r#""outcome":"fuel""#), "{json}");
695        assert!(says(&json, r#""ok":false"#), "{json}");
696        assert!(says(&json, "fuel budget of 10000 exhausted"), "{json}");
697    }
698
699    /// A capability the playground does not grant is refused at the boundary,
700    /// in the runtime's own words, rather than by the module not existing.
701    ///
702    /// This is also what says the checker was given the *shipped* schemas: a
703    /// narrower set would have made this a type error instead, which is a
704    /// different sentence about a different thing.
705    #[test]
706    fn an_ungranted_capability_is_refused_at_the_boundary() {
707        let json = run_json(
708            "use http\n\nexport fn main() -> Result<http.Response, Error> {\n  http.fetch(\"http://example.com\")\n}",
709            None,
710            None,
711        );
712        assert!(says(&json, r#""outcome":"host_boundary""#), "{json}");
713        assert!(says(&json, "http"), "{json}");
714    }
715
716    /// On the host a task really does get a thread, so this is the one thing
717    /// these tests cannot check: that `spawn` is refused. What they can check
718    /// is that a `spawn` past the host-chosen `max_tasks` is refused in the
719    /// same vocabulary, `RunOutcome::Concurrency`, which is what the wasm
720    /// refusal answers too. `web/check.mjs` checks the other half, in wasm,
721    /// where there is no thread to be had.
722    #[test]
723    fn a_spawn_is_refused_as_a_concurrency_stop() {
724        let json = run_json(
725            r#"export fn main() -> Int {
726  scope s {
727    let t = s.spawn { 1 }
728    t.await()
729  }
730}"#,
731            None,
732            None,
733        );
734        assert!(says(&json, r#""outcome":"concurrency""#), "{json}");
735    }
736
737    /// Every value of `key` in `json`, in the order they were written.
738    ///
739    /// A recording is a sequence, and what these tests need to say about one
740    /// is "these things, in this order". A substring search answers "is this
741    /// in there" and cannot answer that, so this is the smallest thing that
742    /// can — still not a parser, still no dependency.
743    fn every(json: &str, key: &str) -> Vec<String> {
744        let needle = format!("\"{key}\":");
745        let mut found = Vec::new();
746        let mut rest = json;
747        while let Some(at) = rest.find(&needle) {
748            rest = &rest[at + needle.len()..];
749            let value = match rest.strip_prefix('"') {
750                Some(quoted) => {
751                    let end = quoted.find('"').unwrap_or(quoted.len());
752                    quoted[..end].to_string()
753                }
754                None => rest
755                    .chars()
756                    .take_while(|c| c.is_ascii_digit() || *c == '-')
757                    .collect(),
758            };
759            found.push(value);
760        }
761        found
762    }
763
764    /// A program written so that every rule in [`record`]'s capture policy
765    /// fires exactly once and in a knowable order: an entry, a new line, a
766    /// call, and the return.
767    const WALKED: &str = r#"export fn twice(n: Int) -> Int {
768  n + n
769}
770
771export fn main() -> Int {
772  let one = 21
773  let total = twice(one)
774  total
775}
776"#;
777
778    /// The recording of a known program has the moments it should have, in
779    /// the order it ran them.
780    #[test]
781    fn a_recording_holds_the_moments_the_program_ran_in_order() {
782        let json = debug_json(WALKED, None, None, 0);
783        assert!(says(&json, r#""outcome":"success""#), "{json}");
784
785        // `why` appears once per moment and nowhere else in the answer.
786        assert_eq!(
787            every(&json, "why"),
788            // Three, and it was four until `lower::inline`, and six before
789            // that until issue #302. Two of the six were copies: the callee's
790            // answer moved out of a temporary, and `main`'s moved into the
791            // location the `return` names. Neither instruction exists now —
792            // the producer writes the destination — and a `return` is written
793            // at the tail it answers rather than at the signature, so the line
794            // does not change again on the way out.
795            //
796            // The `line` went with the call. `twice` is a small leaf, so its
797            // body is written into `main`, and the instruction that used to be
798            // `main`'s `call` on line 7 is now `twice`'s `n + n` written on
799            // line 2. Line 7 has no instruction of its own left for a new-line
800            // rule to fire on. The `call` is still here, and that is the
801            // record doing its work: `Stop::depth` counts an expanded body, so
802            // the moment where one begins is still a moment where the stack
803            // got deeper.
804            ["entry", "call", "return"],
805            "{json}"
806        );
807
808        // One disassembly, held once however often it is in a moment: the
809        // interning that keeps a recording from repeating a loop body once
810        // per turn.
811        //
812        // One and not two, because `twice` is a small leaf and
813        // `lower::inline` wrote its body into `main`. There is one
814        // instruction stream and the table holds disassemblies, so a second
815        // entry could only be `main`'s four instructions under `twice`'s
816        // name — which is what keying the table on the frame's body rather
817        // than on the code's owner produced, and is the reason it does not.
818        assert_eq!(
819            every(&json, "name")
820                .iter()
821                .filter(|name| name.starts_with("playground."))
822                .count(),
823            1,
824            "{json}"
825        );
826        // Which body each frame is, though, is still recorded — on the frame,
827        // where the expansion's own record put it. Without this the backtrace
828        // of a stop inside `twice` would name `main` twice.
829        assert_eq!(
830            every(&json, "body")
831                .iter()
832                .filter(|name| *name == "playground.twice")
833                .count(),
834            1,
835            "{json}"
836        );
837        assert!(says(&json, r#""truncated":null"#), "{json}");
838        assert!(says(&json, r#""kept":3"#), "{json}");
839    }
840
841    /// The locals a moment holds are that moment's, and they change along
842    /// the timeline.
843    ///
844    /// The moment inside the callee shows `n`, and the caller's frame under
845    /// it shows the name it had already bound and not the one it is in the
846    /// middle of binding: a suspended frame is shown at the call it is
847    /// waiting on, and `total` is bound after that call answers. The moment
848    /// after the return shows it holding 42.
849    ///
850    /// It showed `total` as a zero until issue #302, which is what a caller
851    /// shown at its *resume* address answers — the slot the call is about to
852    /// write, named but not yet written. Both the destination forwarding and
853    /// the `- 1` in `Stop::frame` are that change; a test that showed 42 in
854    /// both moments would mean the recording was not per-moment at all.
855    #[test]
856    fn a_local_holds_what_it_held_at_that_moment() {
857        let json = debug_json(WALKED, None, None, 0);
858        let moments: Vec<&str> = json.split(r#"{"at":"#).collect();
859        let inside = moments
860            .iter()
861            .find(|moment| moment.contains(r#""why":"call""#))
862            .unwrap_or_else(|| panic!("a call moment: {json}"));
863        let after = moments
864            .iter()
865            .find(|moment| moment.contains(r#""why":"return""#))
866            .unwrap_or_else(|| panic!("a return moment: {json}"));
867        assert!(inside.contains(r#""name":"n","value":"21""#), "{inside}");
868        assert!(inside.contains(r#""name":"one","value":"21""#), "{inside}");
869        assert!(!inside.contains(r#""name":"total""#), "{inside}");
870        assert!(after.contains(r#""name":"total","value":"42""#), "{after}");
871    }
872
873    /// A local that names a heap object points at one the moment carries.
874    #[test]
875    fn a_local_that_names_an_object_carries_it() {
876        let json = debug_json(
877            "export fn main() -> Int {\n  let greeting = \"hello\"\n  greeting.length()\n}",
878            None,
879            None,
880            0,
881        );
882        assert!(says(&json, r#""outcome":"success""#), "{json}");
883        assert!(says(&json, r#""name":"String""#), "{json}");
884        assert!(says(&json, r#""name":"text","value":"hello""#), "{json}");
885        // The address on the local and the address on the object are the
886        // same number, which is what lets a Memory pane follow a name.
887        let addresses = every(&json, "at");
888        assert!(
889            addresses.iter().any(|at| at.len() > 4),
890            "an object address: {json}"
891        );
892    }
893
894    /// The span a moment carries, which is what the page marks in the editor.
895    ///
896    /// A pair of UTF-16 offsets into the source the page already holds, so
897    /// `text.slice(from, to)` is the text that ran. The em dash above the
898    /// code is what tells this apart from a pair of byte offsets: it is two
899    /// bytes wide and one code unit, so a byte offset would mark two
900    /// characters to the right of everything below it.
901    #[test]
902    fn a_moment_carries_the_span_the_page_marks() {
903        let source = "// an \u{2014} dash\nexport fn main() -> Int {\n  21 * 2\n}\n";
904        let json = debug_json(source, None, None, 0);
905        let units: Vec<u16> = source.encode_utf16().collect();
906        let read = |key| -> Vec<usize> {
907            every(&json, key)
908                .iter()
909                .map(|held| held.parse().expect("an offset is a number"))
910                .collect()
911        };
912        let (from, to) = (read("from"), read("to"));
913        assert!(!from.is_empty(), "{json}");
914        assert_eq!(from.len(), to.len(), "{json}");
915        for (from, to) in from.iter().zip(&to) {
916            assert!(from <= to && *to <= units.len(), "{from}..{to} of {json}");
917            let marked = String::from_utf16(&units[*from..*to]).expect("whole code points");
918            assert!(source.contains(&marked), "{marked:?} is in the source");
919        }
920        // The first moment is the entry, which is `21 * 2`'s first operand.
921        // The same two numbers read as byte offsets are the two spaces that
922        // indent the line, which is what a page marking by them would have
923        // drawn a band on.
924        assert_eq!(
925            String::from_utf16(&units[from[0]..to[0]]).expect("whole code points"),
926            "21",
927            "{json}"
928        );
929        assert_eq!(&source[from[0]..to[0]], "  ", "{json}");
930    }
931
932    /// A recording that hit its bound says which bound, and the run it was
933    /// recording still finished and still answered.
934    ///
935    /// The second half is the point. A recorder that halted the run when it
936    /// filled up would answer a question about a program with a program that
937    /// did not run.
938    #[test]
939    fn a_recording_past_its_bound_says_so_and_the_run_goes_on() {
940        let counting =
941            "export fn main() -> Int {\n  var n = 0\n  while n < 100 {\n    n = n + 1\n  }\n  n\n}";
942        let json = debug_json(counting, None, None, 4);
943        assert!(says(&json, r#""truncated":"moments""#), "{json}");
944        assert!(says(&json, r#""kept":4"#), "{json}");
945        assert!(says(&json, r#""limit":4"#), "{json}");
946        // The run reached its own end rather than the recorder's.
947        assert!(says(&json, r#""outcome":"success""#), "{json}");
948        assert!(
949            says(&json, r#""answer":{"type":"int","value":100}"#),
950            "{json}"
951        );
952    }
953
954    /// A caller cannot ask for an unbounded recording.
955    #[test]
956    fn a_recording_is_bounded_however_much_is_asked_for() {
957        let json = debug_json("export fn main() -> Int { 1 }", None, None, usize::MAX);
958        assert!(
959            says(&json, &format!(r#""limit":{}"#, record::MOST_MOMENTS)),
960            "{json}"
961        );
962    }
963
964    /// A debugged run is the same run: the recorder watches it and does not
965    /// change it.
966    #[test]
967    fn recording_does_not_change_what_the_program_did() {
968        let source = r#"use console.println
969
970export fn main() -> Result<Int, Error> {
971  println("watched")?
972  Ok(21 * 2)
973}"#;
974        let plain = run_json(source, None, None);
975        let watched = debug_json(source, None, None, 0);
976        for fragment in [
977            r#""outcome":"success""#,
978            r#""stdout":"watched\n""#,
979            r#""instructions":"#,
980        ] {
981            assert!(plain.contains(fragment), "{plain}");
982            assert!(watched.contains(fragment), "{watched}");
983        }
984        assert_eq!(
985            every(&plain, "instructions"),
986            every(&watched, "instructions")
987        );
988    }
989
990    /// A source that did not compile has no recording, rather than an empty
991    /// one that reads as a program which did nothing.
992    #[test]
993    fn a_program_that_does_not_compile_has_no_recording() {
994        let json = debug_json("export fn main() -> Int { 1 +", None, None, 0);
995        assert!(says(&json, r#""debug":null"#), "{json}");
996    }
997
998    /// The four existing entry points answer what they always answered.
999    /// `web/check.mjs` and CI depend on it.
1000    #[test]
1001    fn a_plain_run_carries_no_recording() {
1002        let json = run_json("export fn main() -> Int { 1 }", None, None);
1003        assert!(!json.contains("\"debug\""), "{json}");
1004        let json = compile_json("export fn main() -> Int { 1 }");
1005        assert!(!json.contains("\"debug\""), "{json}");
1006    }
1007
1008    /// Every answer is one JSON object and nothing else, whatever happened.
1009    #[test]
1010    fn every_answer_is_a_single_object() {
1011        for source in [
1012            "export fn main() -> Int { 1 }",
1013            "export fn main() -> Int { 1 +",
1014            "",
1015        ] {
1016            for json in [compile_json(source), run_json(source, None, None)] {
1017                assert!(json.starts_with('{') && json.ends_with('}'), "{json}");
1018                assert_eq!(json.matches("\"ok\":").count(), 1, "{json}");
1019            }
1020        }
1021    }
1022}