// 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>();
}