about summary refs log tree commit diff
path: root/web/tvixbolt/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'web/tvixbolt/src/lib.rs')
-rw-r--r--web/tvixbolt/src/lib.rs317
1 files changed, 317 insertions, 0 deletions
diff --git a/web/tvixbolt/src/lib.rs b/web/tvixbolt/src/lib.rs
new file mode 100644
index 000000000000..1f47b0eaf59d
--- /dev/null
+++ b/web/tvixbolt/src/lib.rs
@@ -0,0 +1,317 @@
+// 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 = 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
+}
+
+#[wasm_bindgen]
+pub fn main() {
+    yew::start_app::<Model>();
+}