pub use mlua;
use anyhow::{Context, Result, anyhow, bail};
use mlua::chunk::ChunkMode;
use mlua::{Function, Lua, Table, Value, Variadic};
use std::path::{Path, PathBuf};
pub mod bundle;
#[cfg(feature = "pkg")]
pub mod pkg;
pub mod teal;
pub mod testing;
pub(crate) const PRELUDE_REGISTRY_KEY: &str = "htl.prelude";
const TL_SRC: &str = include_str!("../vendor/tl.lua");
const LINT_SRC: &str = include_str!("lint.lua");
const FMT_SRC: &str = include_str!("fmt.lua");
const PRELUDE: &str = include_str!("prelude.lua");
pub const TEAL_VERSION: &str = "0.24.8";
#[derive(Debug, Clone, Default)]
pub struct CheckInfo {
pub errors: Vec<String>,
pub warnings: Vec<String>,
pub deps: Vec<PathBuf>,
pub lints: Vec<String>,
}
impl CheckInfo {
pub fn ok(&self) -> bool {
self.errors.is_empty()
}
pub fn clean(&self) -> bool {
self.errors.is_empty() && self.warnings.is_empty() && self.lints.is_empty()
}
}
pub struct Htl {
lua: Lua,
h: Table,
}
impl Htl {
pub fn new() -> Result<Self> {
let lua = unsafe { Lua::unsafe_new() };
Self::from_lua(lua)
}
pub fn from_lua(lua: Lua) -> Result<Self> {
let tl_loader: Function = lua
.load(TL_SRC)
.set_name("=tl.lua")
.into_function()
.context("compiling vendored tl.lua")?;
let lint_loader: Function = lua
.load(LINT_SRC)
.set_name("=htl-lint")
.into_function()
.context("compiling htl lint.lua")?;
let package: Table = lua.globals().get("package")?;
let preload: Table = package.get("preload")?;
let fmt_loader: Function = lua
.load(FMT_SRC)
.set_name("=htl-fmt")
.into_function()
.context("compiling htl fmt.lua")?;
preload.set("tl", tl_loader)?;
preload.set("htl.lint", lint_loader)?;
preload.set("htl.fmt", fmt_loader)?;
let h: Table = lua
.load(PRELUDE)
.set_name("=htl-prelude")
.eval()
.context("loading htl prelude")?;
lua.set_named_registry_value(PRELUDE_REGISTRY_KEY, h.clone())?;
Ok(Self { lua, h })
}
pub fn lua(&self) -> &Lua {
&self.lua
}
pub fn check(&self, file: &Path) -> Result<CheckInfo> {
let f: Function = self.h.get("check")?;
let t: Table = f.call(path_str(file))?;
read_checkinfo(&t)
}
pub fn gen_lua(&self, file: &Path) -> Result<(Option<String>, CheckInfo)> {
let f: Function = self.h.get("gen")?;
let (code, t): (Option<String>, Table) = f.call(path_str(file))?;
Ok((code, read_checkinfo(&t)?))
}
pub fn configure_lints(&self, spec: &str) -> Result<()> {
let f: Function = self.h.get("set_lints")?;
let (ok, err): (Option<bool>, Option<String>) = f.call(spec)?;
if ok.unwrap_or(false) {
Ok(())
} else {
bail!("{}", err.unwrap_or_else(|| "invalid lint spec".into()))
}
}
pub fn lint_rules(&self) -> Result<Vec<String>> {
let f: Function = self.h.get("lint_rules")?;
let t: Table = f.call(())?;
Ok(t.sequence_values::<String>().collect::<mlua::Result<_>>()?)
}
pub fn format_file(&self, file: &Path, indent: usize) -> Result<String> {
let f: Function = self.h.get("format")?;
let (out, err): (Option<String>, Option<String>) = f.call((path_str(file), indent))?;
out.ok_or_else(|| anyhow!("{}", err.unwrap_or_else(|| "format failed".into())))
}
pub fn add_path(&self, dir: &Path) -> Result<()> {
let f: Function = self.h.get("add_path")?;
f.call::<()>(path_str(dir))?;
Ok(())
}
pub fn install_searcher(&self) -> Result<()> {
let f: Function = self.h.get("install_searcher")?;
f.call::<()>(())?;
Ok(())
}
pub fn preload(&self, name: &str, lua_src: &str) -> Result<()> {
let loader = self
.lua
.load(lua_src)
.set_name(format!("={name}"))
.into_function()
.with_context(|| format!("compiling preloaded module {name}"))?;
self.preload_table()?.set(name, loader)?;
Ok(())
}
pub fn preload_bytes(&self, name: &str, bytecode: &[u8]) -> Result<()> {
let loader = self
.lua
.load(bytecode)
.set_name(format!("={name}"))
.set_mode(ChunkMode::Binary)
.into_function()
.with_context(|| format!("loading bytecode for module {name}"))?;
self.preload_table()?.set(name, loader)?;
Ok(())
}
pub fn exec_bytes(&self, bytecode: &[u8], chunk_name: &str, args: &[String]) -> Result<()> {
let f = self
.lua
.load(bytecode)
.set_name(chunk_name)
.set_mode(ChunkMode::Binary)
.into_function()?;
let va: Variadic<String> = args.iter().cloned().collect();
f.call::<()>(va)?;
Ok(())
}
pub fn preload_value(&self, name: &str, value: impl mlua::IntoLua) -> Result<()> {
let value = value.into_lua(&self.lua)?;
let loader = self
.lua
.create_function(move |_, ()| Ok(value.clone()))?;
self.preload_table()?.set(name, loader)?;
Ok(())
}
fn preload_table(&self) -> Result<Table> {
let package: Table = self.lua.globals().get("package")?;
Ok(package.get("preload")?)
}
pub fn set_arg(&self, script: &str, args: &[String]) -> Result<()> {
let t = self.lua.create_table()?;
t.set(0, script)?;
for (i, a) in args.iter().enumerate() {
t.set(i + 1, a.as_str())?;
}
self.lua.globals().set("arg", t)?;
Ok(())
}
pub fn exec(&self, lua_src: &str, chunk_name: &str, args: &[String]) -> Result<()> {
let f = self
.lua
.load(lua_src)
.set_name(chunk_name)
.into_function()?;
let va: Variadic<String> = args.iter().cloned().collect();
f.call::<()>(va)?;
Ok(())
}
pub fn run_file(&self, file: &Path, args: &[String]) -> Result<CheckInfo> {
self.add_path(&parent_dir(file))?;
self.install_searcher()?;
self.set_arg(&file.to_string_lossy(), args)?;
let (code, ci) = self.gen_lua(file)?;
let Some(code) = code else { return Ok(ci) };
self.exec(&code, &format!("@{}", file.display()), args)?;
Ok(ci)
}
pub fn compile(&self, name: &str, lua_src: &str) -> Result<Vec<u8>> {
let f = self
.lua
.load(lua_src)
.set_name(format!("={name}"))
.into_function()
.with_context(|| format!("compiling generated Lua for {name}"))?;
Ok(f.dump(true))
}
pub fn install_bundle(&self, b: &bundle::Bundle) -> Result<()> {
let modules = b.modules.clone();
let searcher = self.lua.create_function(move |lua, name: String| {
match modules.iter().find(|(n, _)| *n == name) {
Some((_, bc)) => {
let f = lua
.load(bc.as_slice())
.set_name(format!("={name}"))
.set_mode(ChunkMode::Binary)
.into_function()?;
Ok(Value::Function(f))
}
None => Ok(Value::String(
lua.create_string(format!("\n\tno bundled module '{name}'"))?,
)),
}
})?;
let package: Table = self.lua.globals().get("package")?;
let searchers: Table = package.get("searchers")?;
searchers.raw_insert(2, searcher)?;
Ok(())
}
pub fn run_bundle(&self, b: &bundle::Bundle, args: &[String]) -> Result<()> {
let entry_bc = b
.modules
.iter()
.find(|(n, _)| *n == b.entry)
.map(|(_, bc)| bc.clone())
.ok_or_else(|| anyhow!("entry module '{}' not in bundle", b.entry))?;
self.install_bundle(b)?;
self.set_arg(&b.entry, args)?;
let main: Function = self
.lua
.load(entry_bc.as_slice())
.set_name(format!("={}", b.entry))
.set_mode(ChunkMode::Binary)
.into_function()?;
let va: Variadic<String> = args.iter().cloned().collect();
main.call::<()>(va)?;
Ok(())
}
}
fn path_str(p: &Path) -> String {
p.to_string_lossy().into_owned()
}
pub fn write_if_changed(path: &Path, text: &str) -> std::io::Result<bool> {
if let Ok(cur) = std::fs::read_to_string(path)
&& cur == text
{
return Ok(false);
}
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir)?;
}
std::fs::write(path, text)?;
Ok(true)
}
pub fn parent_dir(file: &Path) -> PathBuf {
let dir = file.parent().unwrap_or(Path::new("."));
if dir.as_os_str().is_empty() { PathBuf::from(".") } else { dir.to_path_buf() }
}
fn read_checkinfo(t: &Table) -> Result<CheckInfo> {
let seq = |key: &str| -> Result<Vec<String>> {
let inner: Table = t.get(key)?;
Ok(inner.sequence_values::<String>().collect::<mlua::Result<_>>()?)
};
Ok(CheckInfo {
errors: seq("errors")?,
warnings: seq("warnings")?,
deps: seq("deps")?.into_iter().map(PathBuf::from).collect(),
lints: seq("lints")?,
})
}
pub fn is_tl_source(p: &Path) -> bool {
let name = p.file_name().and_then(|s| s.to_str()).unwrap_or("");
p.is_file() && name.ends_with(".tl") && !name.ends_with(".d.tl")
}
pub fn collect_tl(paths: &[PathBuf]) -> Result<Vec<PathBuf>> {
let mut out = Vec::new();
for p in paths {
if p.is_dir() {
for e in walkdir::WalkDir::new(p).sort_by_file_name() {
let e = e?;
if is_tl_source(e.path()) {
out.push(e.path().to_path_buf());
}
}
} else if p.is_file() {
out.push(p.clone());
} else {
bail!("no such file or directory: {}", p.display());
}
}
Ok(out)
}
pub fn module_name(root: &Path, file: &Path) -> Result<String> {
let rel = file.strip_prefix(root)?.with_extension("");
let mut parts: Vec<String> = rel
.components()
.map(|c| c.as_os_str().to_string_lossy().into_owned())
.collect();
if parts.last().map(|s| s == "init").unwrap_or(false) {
parts.pop();
}
if parts.is_empty() {
bail!("cannot derive module name for {}", file.display());
}
Ok(parts.join("."))
}