use std::path::{Path, PathBuf};
use std::process::Command;
fn main() {
let args: Vec<String> = std::env::args().collect();
match args.get(1).map(|s| s.as_str()) {
Some("run") => {
let file = args.get(2).map(|s| s.as_str()).unwrap_or_else(|| {
eprintln!("Usage: lingc run <file.ling>");
std::process::exit(1);
});
run_file(file);
}
Some("webgl") => {
let file = args.get(2).map(|s| s.as_str()).unwrap_or_else(|| {
eprintln!("Usage: lingc webgl <file.ling> [--out <dir>]");
std::process::exit(1);
});
let out_dir = parse_out_flag(&args).unwrap_or_else(|| {
Path::new(file)
.file_stem()
.map(|s| format!("{}-web", s.to_string_lossy()))
.unwrap_or_else(|| "webgl-out".into())
});
build_webgl(file, &out_dir);
}
Some(file) if file.ends_with(".ling") => run_file(file),
_ => {
println!("lingc {} — Ling Compiler", ling::VERSION);
println!("Usage:");
println!(" lingc run <file.ling> interpret and run");
println!(" lingc webgl <file.ling> [--out d] build WebGL folder");
println!(" lingc <file.ling> same as run");
}
}
}
fn parse_out_flag(args: &[String]) -> Option<String> {
args.windows(2)
.find(|w| w[0] == "--out")
.map(|w| w[1].clone())
}
fn run_file(path: &str) {
let source = std::fs::read_to_string(path).unwrap_or_else(|e| {
eprintln!("error reading '{}': {}", path, e);
std::process::exit(1);
});
if let Err(e) = ling::run(&source) {
eprintln!("{e}");
std::process::exit(1);
}
}
fn build_webgl(ling_file: &str, out_dir: &str) {
let source = std::fs::read_to_string(ling_file).unwrap_or_else(|e| {
eprintln!("error reading '{}': {e}", ling_file);
std::process::exit(1);
});
std::fs::create_dir_all(out_dir).unwrap_or_else(|e| {
eprintln!("cannot create output dir '{}': {e}", out_dir);
std::process::exit(1);
});
let wasm_crate = find_ling_wasm_crate().unwrap_or_else(|| {
eprintln!(
"cannot find crates/ling-wasm/. \
Run from the Ling repository root or set LING_HOME."
);
std::process::exit(1);
});
if !has_wasm_pack() {
eprintln!(
"wasm-pack not found. Install it with:\n cargo install wasm-pack\n \
or visit https://rustwasm.github.io/wasm-pack/installer/"
);
std::process::exit(1);
}
println!("Building WASM (this may take a minute on first run)...");
let status = Command::new("wasm-pack")
.args(["build", "--target", "no-modules", "--release"])
.arg(&wasm_crate)
.status()
.unwrap_or_else(|e| { eprintln!("wasm-pack failed: {e}"); std::process::exit(1); });
if !status.success() {
eprintln!("wasm-pack build failed (exit {:?})", status.code());
std::process::exit(1);
}
let pkg_dir = wasm_crate.join("pkg");
for filename in &["ling_wasm.js", "ling_wasm_bg.wasm"] {
let src = pkg_dir.join(filename);
let dst = Path::new(out_dir).join(filename);
std::fs::copy(&src, &dst).unwrap_or_else(|e| {
eprintln!("copy {} → {}: {e}", src.display(), dst.display());
std::process::exit(1);
});
}
std::fs::write(Path::new(out_dir).join("program.ling"), &source).unwrap_or_else(|e| {
eprintln!("write program.ling: {e}");
std::process::exit(1);
});
std::fs::write(Path::new(out_dir).join("worker.js"), WORKER_JS).unwrap_or_else(|e| {
eprintln!("write worker.js: {e}");
std::process::exit(1);
});
let title = Path::new(ling_file)
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "Ling".into());
let html = INDEX_HTML.replace("{{TITLE}}", &title);
std::fs::write(Path::new(out_dir).join("index.html"), html).unwrap_or_else(|e| {
eprintln!("write index.html: {e}");
std::process::exit(1);
});
println!("Done! Output folder: {out_dir}/");
println!(" Upload the folder to any static web server and open index.html.");
println!(" Local preview: python -m http.server --directory {out_dir}");
}
fn has_wasm_pack() -> bool {
Command::new("wasm-pack").arg("--version").output().is_ok()
}
fn find_ling_wasm_crate() -> Option<PathBuf> {
if let Ok(home) = std::env::var("LING_HOME") {
let p = PathBuf::from(home).join("crates/ling-wasm");
if p.exists() { return Some(p); }
}
if let Ok(exe) = std::env::current_exe() {
if let Some(repo) = exe.parent().and_then(|p| p.parent()).and_then(|p| p.parent()) {
let p = repo.join("crates/ling-wasm");
if p.exists() { return Some(p); }
}
}
let p = PathBuf::from("crates/ling-wasm");
if p.exists() { return Some(p); }
None
}
const WORKER_JS: &str = r#"// worker.js — runs the Ling interpreter inside a Web Worker
// The main thread transfers an OffscreenCanvas; the WASM runtime renders to it via WebGL2.
importScripts('./ling_wasm.js');
let wasmReady = false;
let pendingCanvas = null;
let pendingSource = null;
// Queue both canvas and source until wasm is instantiated — calling exported
// functions before the wasm_bindgen() promise resolves fails with "wasm is undefined".
wasm_bindgen('./ling_wasm_bg.wasm').then(() => {
wasmReady = true;
if (pendingCanvas !== null) {
wasm_bindgen.init_canvas(pendingCanvas);
pendingCanvas = null;
}
if (pendingSource !== null) {
wasm_bindgen.run_program(pendingSource);
pendingSource = null;
}
});
self.onmessage = function(e) {
const { type } = e.data;
if (type === 'init') {
if (wasmReady) {
wasm_bindgen.init_canvas(e.data.canvas);
} else {
pendingCanvas = e.data.canvas;
}
} else if (type === 'run') {
if (wasmReady) {
wasm_bindgen.run_program(e.data.source);
} else {
pendingSource = e.data.source;
}
}
};
"#;
const INDEX_HTML: &str = r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{TITLE}}</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { width: 100%; height: 100%; background: #000; overflow: hidden; }
#canvas {
display: block;
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
/* Scale canvas to fit viewport while preserving aspect ratio */
max-width: 100vw;
max-height: 100vh;
width: auto;
height: auto;
}
#status {
position: fixed; bottom: 8px; right: 12px;
color: #888; font: 12px/1 monospace;
}
</style>
</head>
<body>
<!-- Canvas starts at window size; Ling's open_window/open_fullscreen may resize it -->
<canvas id="canvas"></canvas>
<div id="status">loading…</div>
<script>
const canvas = document.getElementById('canvas');
const status = document.getElementById('status');
// Size the canvas backing store to the window so the Ling program's
// open_fullscreen() reads the correct dimensions from canvas_size().
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const worker = new Worker('worker.js');
// Transfer canvas control to the worker (enables OffscreenCanvas + WebGL in worker)
const offscreen = canvas.transferControlToOffscreen();
worker.postMessage({ type: 'init', canvas: offscreen }, [offscreen]);
fetch('program.ling')
.then(r => { if (!r.ok) throw new Error(r.statusText); return r.text(); })
.then(source => {
status.textContent = 'running';
worker.postMessage({ type: 'run', source });
})
.catch(err => {
status.textContent = 'error: ' + err.message;
console.error(err);
});
worker.onerror = (e) => {
status.textContent = 'worker error: ' + e.message;
console.error('worker error', e);
};
</script>
</body>
</html>
"#;