// 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 wasm_bindgen::prelude::wasm_bindgen; 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_builder = tvix_eval::Evaluation::builder_pure(); let source = eval_builder.source_map().clone(); let result = { let mut compiler_observer = DisassemblingObserver::new(source.clone(), &mut out.bytecode); eval_builder.set_compiler_observer(Some(&mut compiler_observer)); let mut runtime_observer = TracingObserver::new(&mut out.trace); if model.trace { eval_builder.set_runtime_observer(Some(&mut runtime_observer)); } let eval = eval_builder.build(); 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 } #[wasm_bindgen] pub fn main() { yew::start_app::<Model>(); }