chore(tvixbolt): move from //corp to //web
Assigning copyright to the TVL community (whatever that is), and adding AGPL-3.0-or-later license. I also cleaned up some of the stuff on the landing page. Change-Id: I4dbca19406e00e5105fed50e8fb64e0fcca23e3a Reviewed-on: https://cl.tvl.fyi/c/depot/+/11013 Autosubmit: tazjin <tazjin@tvl.su> Reviewed-by: flokli <flokli@flokli.de> Tested-by: BuildkiteCI
This commit is contained in:
		
							parent
							
								
									91d5745c3d
								
							
						
					
					
						commit
						782cfa9e33
					
				
					 9 changed files with 676 additions and 41 deletions
				
			
		
							
								
								
									
										315
									
								
								web/tvixbolt/src/main.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										315
									
								
								web/tvixbolt/src/main.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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>();
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue