Skip to content

Commit b9e7358

Browse files
Merge pull request #18 from aayush-tripathi/playground-bytecode
Playground bytecode
2 parents bd9975b + 3e6a37f commit b9e7358

6 files changed

Lines changed: 139 additions & 62 deletions

File tree

executor/src/main.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ use serde::{Deserialize, Serialize};
66
use std::net::SocketAddr;
77
use std::sync::Arc;
88
use tokio::net::TcpListener;
9+
use tower_http::cors::AllowOrigin;
910
use tower_http::cors::{Any, CorsLayer};
1011
use tundra::run;
11-
use tower_http::cors::AllowOrigin;
1212

1313
#[derive(Deserialize)]
1414
struct ExecReq {
@@ -21,6 +21,11 @@ struct ExecResp {
2121
stderr: String,
2222
}
2323

24+
#[derive(Serialize)]
25+
struct DisasmResp {
26+
bytecode: String,
27+
stderr: String,
28+
}
2429
#[tokio::main]
2530
async fn main() -> anyhow::Result<()> {
2631
static ALLOWED: &[&str] = &[
@@ -38,6 +43,7 @@ async fn main() -> anyhow::Result<()> {
3843
.allow_headers(Any);
3944
let app = Router::new()
4045
.route("/execute", post(exec))
46+
.route("/disassemble", post(disassemble))
4147
.layer(cors)
4248
.with_state(Arc::new(()));
4349

@@ -61,3 +67,15 @@ async fn exec(State(_): State<Arc<()>>, Json(req): Json<ExecReq>) -> Json<ExecRe
6167
}),
6268
}
6369
}
70+
async fn disassemble(State(_): State<Arc<()>>, Json(req): Json<ExecReq>) -> Json<DisasmResp> {
71+
match tundra::disassemble(&req.code) {
72+
Ok(bc) => Json(DisasmResp {
73+
bytecode: bc,
74+
stderr: String::new(),
75+
}),
76+
Err(e) => Json(DisasmResp {
77+
bytecode: String::new(),
78+
stderr: e.to_string(),
79+
}),
80+
}
81+
}

playground/src/App.tsx

Lines changed: 47 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,52 @@
11
import { useState } from "react";
22
import { Editor } from "./Editor";
3-
import { exec } from "./api";
3+
import { exec, disassemble } from "./api";
44
import { Analytics } from "@vercel/analytics/react"
55
import "./styles.css";
66

7-
import {
8-
Panel,
9-
PanelGroup,
10-
PanelResizeHandle,
11-
} from "react-resizable-panels";
7+
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
128

139
const PlayIcon = () => (
14-
<svg
15-
viewBox="0 0 24 24"
16-
fill="currentColor"
17-
style={{ marginRight: "8px", height: "16px", width: "16px" }}
18-
>
10+
<svg viewBox="0 0 24 24" fill="currentColor" style={{ marginRight: 8, height: 16, width: 16 }}>
1911
<path d="M8 5v14l11-7z" />
2012
</svg>
2113
);
2214

23-
2415
export default function App() {
2516
const [code, setCode] = useState('print("Hello, Tundra!")');
2617
const [output, setOutput] = useState("");
18+
const [bytecode, setBytecode] = useState("");
2719
const [isRunning, setIsRunning] = useState(false);
2820

2921
const runCode = async () => {
3022
setIsRunning(true);
3123
setOutput("⏳ running…");
24+
setBytecode("⏳ compiling…");
3225
try {
33-
const { stdout, stderr } = await exec(code);
34-
setOutput(stderr || stdout);
26+
const [execRes, disRes] = await Promise.all([exec(code), disassemble(code)]);
27+
setOutput(execRes.stderr || execRes.stdout);
28+
setBytecode(disRes.stderr || disRes.bytecode);
3529
} catch (error) {
3630
setOutput(`Failed to connect to executor: ${error}`);
31+
setBytecode("");
3732
} finally {
3833
setIsRunning(false);
3934
}
4035
};
4136

37+
const wikiUrl = import.meta.env.VITE_WIKI_URL ?? "https://aayush-tripathi.github.io/tundra/";
38+
4239
return (
4340
<div style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
4441
{/* Header / Toolbar */}
45-
<header style={{ display: "flex", alignItems: "center", padding: "8px", borderBottom: "1px solid #30363d" }}>
46-
<img
47-
src="/tundra_base_logo.png"
48-
alt="Tundra Logo"
49-
className="logo"
50-
/>
51-
<h1 style={{ fontSize: "1.1rem", fontWeight: 600, marginRight: "1rem" }}>
52-
Tundra Playground
53-
</h1>
42+
<header style={{ display: "flex", alignItems: "center", gap: 12, padding: 8, borderBottom: "1px solid #30363d" }}>
43+
<img src="/tundra_base_logo.png" alt="Tundra Logo" className="logo" />
44+
<h1 style={{ fontSize: "1.1rem", fontWeight: 600, marginRight: "auto" }}>Tundra Playground</h1>
45+
46+
<a href={wikiUrl} target="_blank" rel="noreferrer" className="wiki-button">Wiki</a>
47+
5448
<button onClick={runCode} disabled={isRunning} className="run-button">
55-
<PlayIcon />
56-
Run
49+
<PlayIcon /> Run
5750
</button>
5851
</header>
5952

@@ -62,23 +55,39 @@ export default function App() {
6255
<Panel defaultSize={65} minSize={20}>
6356
<Editor code={code} setCode={setCode} />
6457
</Panel>
65-
66-
<PanelResizeHandle className="resize-handle">
67-
<div className="resize-handle-bar" />
68-
</PanelResizeHandle>
58+
59+
<PanelResizeHandle className="resize-handle"><div className="resize-handle-bar" /></PanelResizeHandle>
6960

7061
<Panel defaultSize={35} minSize={10}>
71-
<div style={{ height: "100%", display: "flex", flexDirection: "column" }}>
72-
<div style={{ padding: "8px", borderBottom: "1px solid #30363d", backgroundColor: "#161b22" }}>
73-
<h2 style={{ margin: 0, fontSize: "0.9rem", fontWeight: 600 }}>Output</h2>
74-
</div>
75-
<pre style={{ flex: 1, margin: 0, padding: "12px", fontFamily: "monospace", whiteSpace: "pre-wrap", overflow: "auto" }}>
76-
{output}
77-
</pre>
78-
</div>
62+
{/* Output | Bytecode */}
63+
<PanelGroup direction="horizontal" style={{ height: "100%" }}>
64+
<Panel defaultSize={50} minSize={10}>
65+
<div style={{ height: "100%", display: "flex", flexDirection: "column" }}>
66+
<div style={{ padding: 8, borderBottom: "1px solid #30363d", backgroundColor: "#161b22" }}>
67+
<h2 style={{ margin: 0, fontSize: "0.9rem", fontWeight: 600 }}>Output</h2>
68+
</div>
69+
<pre style={{ flex: 1, margin: 0, padding: 12, fontFamily: "monospace", whiteSpace: "pre-wrap", overflow: "auto" }}>
70+
{output}
71+
</pre>
72+
</div>
73+
</Panel>
74+
75+
<PanelResizeHandle className="resize-handle vertical"><div className="resize-handle-bar" /></PanelResizeHandle>
76+
77+
<Panel defaultSize={50} minSize={10}>
78+
<div style={{ height: "100%", display: "flex", flexDirection: "column" }}>
79+
<div style={{ padding: 8, borderBottom: "1px solid #30363d", backgroundColor: "#161b22" }}>
80+
<h2 style={{ margin: 0, fontSize: "0.9rem", fontWeight: 600 }}>Bytecode</h2>
81+
</div>
82+
<pre style={{ flex: 1, margin: 0, padding: 12, fontFamily: "monospace", whiteSpace: "pre", overflow: "auto" }}>
83+
{bytecode}
84+
</pre>
85+
</div>
86+
</Panel>
87+
</PanelGroup>
7988
</Panel>
8089
</PanelGroup>
8190
<Analytics />
8291
</div>
8392
);
84-
}
93+
}

playground/src/api.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,11 @@ export const exec = async (code: string): Promise<{ stdout: string; stderr: stri
66
});
77
return resp.json();
88
};
9+
export const disassemble = async (code: string): Promise<{ bytecode: string; stderr: string }> => {
10+
const resp = await fetch(import.meta.env.VITE_EXECUTOR_URL + "/disassemble", {
11+
method: "POST",
12+
headers: { "Content-Type": "application/json" },
13+
body: JSON.stringify({ code }),
14+
});
15+
return resp.json();
16+
};

playground/src/styles.css

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ body {
3131
}
3232

3333
.logo {
34-
height: 5rem; /* tweak size */
35-
margin-right: 0.75rem; /* space before the title */
34+
height: 6rem;
35+
margin-right: 0.75rem;
3636
}
3737
.run-button {
3838
display: flex;
@@ -56,4 +56,32 @@ body {
5656
background-color: #555;
5757
border-color: #666;
5858
cursor: not-allowed;
59-
}
59+
}
60+
.wiki-button {
61+
border: 1px solid #1b839e;
62+
background: #1b839e;
63+
color: #c9d1d9;
64+
padding: 6px 10px;
65+
border-radius: 6px;
66+
text-decoration: none;
67+
font-weight: 600;
68+
}
69+
.wiki-button:hover { background: #0d6179; }
70+
71+
.run-button {
72+
display: inline-flex;
73+
align-items: center;
74+
gap: 6px;
75+
border: 1px solid #238636;
76+
background: #238636;
77+
color: #c9d1d9;
78+
margin-right: 4px;
79+
padding: 6px 10px;
80+
border-radius: 6px;
81+
font-weight: 600;
82+
}
83+
.run-button:disabled { opacity: 0.6; cursor: not-allowed; }
84+
85+
.resize-handle { display: flex; align-items: center; justify-content: center; }
86+
.resize-handle.vertical { width: 6px; cursor: col-resize; }
87+
.resize-handle-bar { width: 60%; height: 2px; background: #30363d; border-radius: 2px; }

tundra/src/bytecode/debug.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
// bytecode/debug.rs
22

33
use crate::bytecode::chunk::Chunk;
4-
4+
use std::fmt::Write as _;
55
/// A one‐pass disassembler
66
pub fn disassemble_chunk(chunk: &Chunk, name: &str) {
77
println!("== {} ==", name);
88
for (offset, instr) in chunk.code.iter().enumerate() {
99
let line = chunk.get_line(offset).unwrap_or(-1);
10-
// Use {:?} since OpCode implements Debug, not Display
1110
println!("{:04} Line {:4} {:?}", offset, line, instr);
1211
}
1312
}
13+
pub fn disassemble_to_string(chunk: &Chunk, name: &str) -> String {
14+
let mut s = String::new();
15+
let _ = writeln!(s, "== {} ==", name);
16+
for (offset, instr) in chunk.code.iter().enumerate() {
17+
let line = chunk.get_line(offset).unwrap_or(-1);
18+
let _ = writeln!(s, "{:04} Line {:4} {:?}", offset, line, instr);
19+
}
20+
s
21+
}

tundra/src/lib.rs

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,51 +6,59 @@ pub mod lexer;
66
pub mod vm;
77

88
pub use crate::vm::interpretresult::InterpretResult;
9+
910
use crate::{
10-
bytecode::chunk::Chunk, compiler::compiler::Compiler, lexer::scanner::Scanner, vm::vm::VM,
11+
bytecode::chunk::Chunk,
12+
bytecode::debug::disassemble_to_string,
13+
compiler::compiler::Compiler,
14+
lexer::scanner::Scanner,
15+
vm::vm::VM,
1116
};
1217
use anyhow::{anyhow, Result};
1318
use std::cell::RefCell;
1419
use std::rc::Rc;
1520

16-
/// A custom Write implementation that captures output to a shared buffer
1721
struct OutputCapture {
1822
buffer: Rc<RefCell<Vec<u8>>>,
1923
}
20-
2124
impl OutputCapture {
2225
fn new(buffer: Rc<RefCell<Vec<u8>>>) -> Self {
2326
Self { buffer }
2427
}
2528
}
26-
2729
impl std::io::Write for OutputCapture {
2830
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
2931
self.buffer.borrow_mut().extend_from_slice(buf);
3032
Ok(buf.len())
3133
}
32-
3334
fn flush(&mut self) -> std::io::Result<()> {
3435
Ok(())
3536
}
3637
}
3738

38-
pub fn run(src: &str) -> Result<String> {
39-
// a shared buffer for capturing output
40-
let output_buffer = Rc::new(RefCell::new(Vec::<u8>::new()));
41-
42-
let output_capture = Box::new(OutputCapture::new(output_buffer.clone()));
43-
let output_ref: &'static mut dyn std::io::Write = Box::leak(output_capture);
44-
45-
// ---------- compile ----------
39+
pub fn compile_only(src: &str) -> Result<Rc<RefCell<Chunk>>> {
4640
let _tokens = Scanner::new(src.to_string()).scan_tokens();
4741
let chunk = Rc::new(RefCell::new(Chunk::new()));
4842
let mut compiler = Compiler::new(chunk.clone());
4943
if !compiler.compile(src) {
5044
return Err(anyhow!("compile error"));
5145
}
46+
Ok(chunk)
47+
}
48+
49+
pub fn disassemble(src: &str) -> Result<String> {
50+
let chunk = compile_only(src)?;
51+
let s = disassemble_to_string(&chunk.borrow(), "Disassembled Bytecode");
52+
Ok(s)
53+
}
5254

53-
// ---------- execute ----------
55+
pub fn run(src: &str) -> Result<String> {
56+
// capture stdout
57+
let output_buffer = Rc::new(RefCell::new(Vec::<u8>::new()));
58+
let output_capture = Box::new(OutputCapture::new(output_buffer.clone()));
59+
let output_ref: &'static mut dyn std::io::Write = Box::leak(output_capture);
60+
let chunk = compile_only(src)?;
61+
// run
5462
let result = {
5563
let mut vm = VM::new_interpreter_only(chunk, output_ref);
5664
match vm.run() {
@@ -59,10 +67,8 @@ pub fn run(src: &str) -> Result<String> {
5967
InterpretResult::CompileError => unreachable!(),
6068
}
6169
};
62-
let output_bytes = {
63-
let buffer = output_buffer.borrow();
64-
buffer.clone()
65-
};
70+
71+
let output_bytes = output_buffer.borrow().clone();
6672
let output_string = String::from_utf8(output_bytes).unwrap_or_default();
6773
result.map(|_| output_string)
6874
}

0 commit comments

Comments
 (0)