As expected, this ends up being significantly nicer to use than the previous API. While doing this, I've combined the error fields into one. This is because there would only ever be one of those anyways, and combining them ensures that we have consistent formatting (for example, parser errors would previously not be run through the pretty formatter but are now). Change-Id: I6074ec8a4a3901ea82d5d07174b76a345210967b Reviewed-on: https://cl.tvl.fyi/c/depot/+/7547 Autosubmit: tazjin <tazjin@tvl.su> Reviewed-by: grfn <grfn@gws.fyi> Tested-by: BuildkiteCI
326 lines
8.5 KiB
Rust
326 lines
8.5 KiB
Rust
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://cs.tvl.fyi/depot/-/tree/tvix">{"Tvix"}</a>
|
||
{" compiler for the Nix language. See the "}
|
||
<a href="https://tvl.fyi/blog/rewriting-nix">{"Tvix announcement"}</a>
|
||
{" for some background information on Tvix itself."}
|
||
</p>
|
||
<p>
|
||
{"Tvix is still "}<i>{"extremely work-in-progress"}</i>{" and you "}
|
||
{"should expect to be able to cause bugs and errors in this tool."}
|
||
</p>
|
||
</>
|
||
}
|
||
}
|
||
|
||
/// This renders an ad in the Tvixbolt footer. Most people that end up
|
||
/// on Tvixbolt will probably block this anyways, but might as well.
|
||
fn ad() -> Html {
|
||
let ad_code = r#"
|
||
window.yaContextCb.push(()=>{
|
||
Ya.Context.AdvManager.render({
|
||
renderTo: 'yandex_rtb_R-A-1943274-1',
|
||
blockId: 'R-A-1943274-1'
|
||
})
|
||
})
|
||
"#;
|
||
|
||
html! {
|
||
<div id="ad">
|
||
<div id="yandex_rtb_R-A-1943274-1"></div>
|
||
<script>{ad_code}</script>
|
||
</div>
|
||
}
|
||
}
|
||
|
||
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.su", "home")}
|
||
{footer_link("https://cs.tvl.fyi", "code")}
|
||
{footer_link("https://tvl.fyi/builds", "ci")}
|
||
{footer_link("https://b.tvl.fyi", "bugs")}
|
||
{"© ООО ТВЛ"}
|
||
</p>
|
||
<p class="lod">{"ಠ_ಠ"}</p>
|
||
{ad()}
|
||
</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 0.1-alpha"}</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(&model.code, Some("/nixbolt".into()));
|
||
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()
|
||
};
|
||
|
||
if model.display_ast {
|
||
if let Some(ref expr) = result.expr {
|
||
out.ast = tvix_eval::pretty_print_expr(expr);
|
||
}
|
||
}
|
||
|
||
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(&source).trim(),
|
||
)
|
||
.unwrap();
|
||
}
|
||
|
||
return out;
|
||
}
|
||
|
||
out
|
||
}
|
||
|
||
fn main() {
|
||
yew::start_app::<Model>();
|
||
}
|