about summary refs log tree commit diff
path: root/web/tvixbolt/src/lib.rs
// 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::history::BrowserHistory;
use yew_router::history::History;

#[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("/", 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::Renderer::<Model>::new().render();
}