diff options
Diffstat (limited to 'web/tvixbolt/src')
-rw-r--r-- | web/tvixbolt/src/main.rs | 315 |
1 files changed, 315 insertions, 0 deletions
diff --git a/web/tvixbolt/src/main.rs b/web/tvixbolt/src/main.rs new file mode 100644 index 000000000000..2e68e03fb0ba --- /dev/null +++ b/web/tvixbolt/src/main.rs @@ -0,0 +1,315 @@ +// tvixbolt - an online tool for exploring Tvix language evaluation +// +// Copyright (C) The TVL Community +// SPDX-License-Identifier: AGPL-3.0-or-later + +use std::fmt::Write; + +use serde::{Deserialize, Serialize}; +use tvix_eval::observer::{DisassemblingObserver, TracingObserver}; +use web_sys::HtmlDetailsElement; +use web_sys::HtmlTextAreaElement; +use yew::prelude::*; +use yew::TargetCast; +use yew_router::{prelude::*, AnyRoute}; + +#[derive(Clone)] +enum Msg { + CodeChange(String), + ToggleTrace(bool), + ToggleDisplayAst(bool), + + // Required because browsers are stupid and it's easy to get into + // infinite loops with `ontoggle` events. + NoOp, +} + +#[derive(Clone, Serialize, Deserialize)] +struct Model { + code: String, + + // #[serde(skip_serializing)] + trace: bool, + + // #[serde(skip_serializing)] + display_ast: bool, +} + +fn tvixbolt_overview() -> Html { + html! { + <> + <p> + {"This page lets you explore the bytecode generated by the "} + <a href="https://tvix.dev">{"Tvix"}</a> + {" compiler for the Nix language."} + </p> + <p> + {"Tvix is still "}<i>{"work-in-progress"}</i>{" and we would appreciate "} + {"if you told us about bugs you find."} + </p> + <p> + {"Tvixbolt is a project by "} + <a href="https://tvl.fyi"> + {"TVL"} + </a> + {"."} + </p> + </> + } +} + +fn footer_link(location: &'static str, name: &str) -> Html { + html! { + <> + <a class="uncoloured-link" href={location}>{name}</a>{" | "} + </> + } +} + +fn footer() -> Html { + html! { + <> + <hr/> + <footer> + <p class="footer"> + {footer_link("https://tvl.fyi", "home")} + {footer_link("https://cs.tvl.fyi", "code")} + {footer_link("https://tvl.fyi/builds", "ci")} + {footer_link("https://b.tvl.fyi", "bugs")} + {"© TVL"} + </p> + <p class="lod">{"ಠ_ಠ"}</p> + </footer> + </> + } +} + +impl Component for Model { + type Message = Msg; + type Properties = (); + + fn create(_: &Context<Self>) -> Self { + BrowserHistory::new() + .location() + .query::<Self>() + .unwrap_or_else(|_| Self { + code: String::new(), + trace: false, + display_ast: false, + }) + } + + fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool { + match msg { + Msg::ToggleTrace(trace) => { + self.trace = trace; + } + + Msg::ToggleDisplayAst(display_ast) => { + self.display_ast = display_ast; + } + + Msg::CodeChange(new_code) => { + self.code = new_code; + } + + Msg::NoOp => {} + } + + let _ = BrowserHistory::new().replace_with_query(AnyRoute::new("/"), self.clone()); + + true + } + + fn view(&self, ctx: &Context<Self>) -> Html { + // This gives us a component's "`Scope`" which allows us to send messages, etc to the component. + let link = ctx.link(); + html! { + <> + <div class="container"> + <h1>{"tvixbolt"}</h1> + {tvixbolt_overview()} + <form> + <fieldset> + <legend>{"Input"}</legend> + + <div class="form-group"> + <label for="code">{"Nix code:"}</label> + <textarea + oninput={link.callback(|e: InputEvent| { + let ta = e.target_unchecked_into::<HtmlTextAreaElement>().value(); + Msg::CodeChange(ta) + + })} + id="code" cols="30" rows="10" value={self.code.clone()}> + </textarea> + </div> + </fieldset> + </form> + <hr /> + {self.run(ctx)} + {footer()} + </div> + </> + } + } +} + +impl Model { + fn run(&self, ctx: &Context<Self>) -> Html { + if self.code.is_empty() { + return html! { + <p> + {"Enter some Nix code above to get started. Don't know Nix yet? "} + {"Check out "} + <a href="https://code.tvl.fyi/about/nix/nix-1p/README.md">{"nix-1p"}</a> + {"!"} + </p> + }; + } + + html! { + <> + <h2>{"Result:"}</h2> + {eval(self).display(ctx, self)} + </> + } + } +} + +#[derive(Default)] +struct Output { + errors: String, + warnings: String, + output: String, + bytecode: Vec<u8>, + trace: Vec<u8>, + ast: String, +} + +fn maybe_show(title: &str, s: &str) -> Html { + if s.is_empty() { + html! {} + } else { + html! { + <> + <h3>{title}</h3> + <pre>{s}</pre> + </> + } + } +} + +fn maybe_details( + ctx: &Context<Model>, + title: &str, + s: &str, + display: bool, + toggle: fn(bool) -> Msg, +) -> Html { + let link = ctx.link(); + if display { + let msg = toggle(false); + html! { + <details open=true + ontoggle={link.callback(move |e: Event| { + let details = e.target_unchecked_into::<HtmlDetailsElement>(); + if !details.open() { + msg.clone() + } else { + Msg::NoOp + } + })}> + + <summary><h3 style="display: inline;">{title}</h3></summary> + <pre>{s}</pre> + </details> + } + } else { + let msg = toggle(true); + html! { + <details ontoggle={link.callback(move |e: Event| { + let details = e.target_unchecked_into::<HtmlDetailsElement>(); + if details.open() { + msg.clone() + } else { + Msg::NoOp + } + })}> + <summary><h3 style="display: inline;">{title}</h3></summary> + </details> + } + } +} + +impl Output { + fn display(self, ctx: &Context<Model>, model: &Model) -> Html { + html! { + <> + {maybe_show("Errors:", &self.errors)} + {maybe_show("Warnings:", &self.warnings)} + {maybe_show("Output:", &self.output)} + {maybe_show("Bytecode:", &String::from_utf8_lossy(&self.bytecode))} + {maybe_details(ctx, "Runtime trace:", &String::from_utf8_lossy(&self.trace), model.trace, Msg::ToggleTrace)} + {maybe_details(ctx, "Parsed AST:", &self.ast, model.display_ast, Msg::ToggleDisplayAst)} + </> + } + } +} + +fn eval(model: &Model) -> Output { + let mut out = Output::default(); + + if model.code.is_empty() { + return out; + } + + let mut eval = tvix_eval::Evaluation::new_pure(); + let source = eval.source_map(); + + let result = { + let mut compiler_observer = DisassemblingObserver::new(source.clone(), &mut out.bytecode); + eval.compiler_observer = Some(&mut compiler_observer); + + let mut runtime_observer = TracingObserver::new(&mut out.trace); + if model.trace { + eval.runtime_observer = Some(&mut runtime_observer); + } + + eval.evaluate(&model.code, Some("/nixbolt".into())) + }; + + if model.display_ast { + if let Some(ref expr) = result.expr { + out.ast = tvix_eval::pretty_print_expr(expr); + } + } + + out.output = match result.value { + Some(val) => val.to_string(), + None => "".to_string(), + }; + + for warning in result.warnings { + writeln!( + &mut out.warnings, + "{}\n", + warning.fancy_format_str(&source).trim(), + ) + .unwrap(); + } + + if !result.errors.is_empty() { + for error in &result.errors { + writeln!(&mut out.errors, "{}\n", error.fancy_format_str().trim(),).unwrap(); + } + + return out; + } + + out +} + +fn main() { + yew::start_app::<Model>(); +} |