1pub use mlua;
12
13use anyhow::{Context, Result, anyhow, bail};
14use mlua::chunk::ChunkMode;
15use mlua::{Function, Lua, Table, Value, Variadic};
16use std::path::{Path, PathBuf};
17
18pub mod bundle;
19#[cfg(feature = "dts")]
20pub mod dts;
21#[cfg(feature = "pkg")]
22pub mod pkg;
23pub mod teal;
24pub mod testing;
25
26pub(crate) const PRELUDE_REGISTRY_KEY: &str = "htl.prelude";
29
30const TL_SRC: &str = include_str!("../vendor/tl.lua");
31const LINT_SRC: &str = include_str!("lint.lua");
32const FMT_SRC: &str = include_str!("fmt.lua");
33const PRELUDE: &str = include_str!("prelude.lua");
34
35pub const TEAL_VERSION: &str = "0.24.8";
37
38#[derive(Debug, Clone, Default)]
40pub struct CheckInfo {
41 pub errors: Vec<String>,
43 pub warnings: Vec<String>,
45 pub deps: Vec<PathBuf>,
47 pub lints: Vec<String>,
50}
51
52impl CheckInfo {
53 pub fn ok(&self) -> bool {
54 self.errors.is_empty()
55 }
56
57 pub fn clean(&self) -> bool {
59 self.errors.is_empty() && self.warnings.is_empty() && self.lints.is_empty()
60 }
61}
62
63pub struct Htl {
65 lua: Lua,
66 h: Table,
67}
68
69impl Htl {
70 pub fn new() -> Result<Self> {
72 let lua = unsafe { Lua::unsafe_new() };
74 Self::from_lua(lua)
75 }
76
77 pub fn from_lua(lua: Lua) -> Result<Self> {
79 let tl_loader: Function = lua
80 .load(TL_SRC)
81 .set_name("=tl.lua")
82 .into_function()
83 .context("compiling vendored tl.lua")?;
84 let lint_loader: Function = lua
85 .load(LINT_SRC)
86 .set_name("=htl-lint")
87 .into_function()
88 .context("compiling htl lint.lua")?;
89 let package: Table = lua.globals().get("package")?;
90 let preload: Table = package.get("preload")?;
91 let fmt_loader: Function = lua
92 .load(FMT_SRC)
93 .set_name("=htl-fmt")
94 .into_function()
95 .context("compiling htl fmt.lua")?;
96 preload.set("tl", tl_loader)?;
97 preload.set("htl.lint", lint_loader)?;
98 preload.set("htl.fmt", fmt_loader)?;
99 let h: Table = lua
100 .load(PRELUDE)
101 .set_name("=htl-prelude")
102 .eval()
103 .context("loading htl prelude")?;
104 lua.set_named_registry_value(PRELUDE_REGISTRY_KEY, h.clone())?;
105 Ok(Self { lua, h })
106 }
107
108 pub fn lua(&self) -> &Lua {
109 &self.lua
110 }
111
112 pub fn check(&self, file: &Path) -> Result<CheckInfo> {
114 let f: Function = self.h.get("check")?;
115 let t: Table = f.call(path_str(file))?;
116 read_checkinfo(&t)
117 }
118
119 pub fn gen_lua(&self, file: &Path) -> Result<(Option<String>, CheckInfo)> {
121 let f: Function = self.h.get("gen")?;
122 let (code, t): (Option<String>, Table) = f.call(path_str(file))?;
123 Ok((code, read_checkinfo(&t)?))
124 }
125
126 pub fn configure_lints(&self, spec: &str) -> Result<()> {
128 let f: Function = self.h.get("set_lints")?;
129 let (ok, err): (Option<bool>, Option<String>) = f.call(spec)?;
130 if ok.unwrap_or(false) {
131 Ok(())
132 } else {
133 bail!("{}", err.unwrap_or_else(|| "invalid lint spec".into()))
134 }
135 }
136
137 pub fn lint_rules(&self) -> Result<Vec<String>> {
139 let f: Function = self.h.get("lint_rules")?;
140 let t: Table = f.call(())?;
141 Ok(t.sequence_values::<String>().collect::<mlua::Result<_>>()?)
142 }
143
144 pub fn format_file(&self, file: &Path, indent: usize) -> Result<String> {
146 let f: Function = self.h.get("format")?;
147 let (out, err): (Option<String>, Option<String>) = f.call((path_str(file), indent))?;
148 out.ok_or_else(|| anyhow!("{}", err.unwrap_or_else(|| "format failed".into())))
149 }
150
151 pub fn add_path(&self, dir: &Path) -> Result<()> {
153 let f: Function = self.h.get("add_path")?;
154 f.call::<()>(path_str(dir))?;
155 Ok(())
156 }
157
158 pub fn install_searcher(&self) -> Result<()> {
160 let f: Function = self.h.get("install_searcher")?;
161 f.call::<()>(())?;
162 Ok(())
163 }
164
165 pub fn preload(&self, name: &str, lua_src: &str) -> Result<()> {
167 let loader = self
168 .lua
169 .load(lua_src)
170 .set_name(format!("={name}"))
171 .into_function()
172 .with_context(|| format!("compiling preloaded module {name}"))?;
173 self.preload_table()?.set(name, loader)?;
174 Ok(())
175 }
176
177 pub fn preload_bytes(&self, name: &str, bytecode: &[u8]) -> Result<()> {
179 let loader = self
180 .lua
181 .load(bytecode)
182 .set_name(format!("={name}"))
183 .set_mode(ChunkMode::Binary)
184 .into_function()
185 .with_context(|| format!("loading bytecode for module {name}"))?;
186 self.preload_table()?.set(name, loader)?;
187 Ok(())
188 }
189
190 pub fn exec_bytes(&self, bytecode: &[u8], chunk_name: &str, args: &[String]) -> Result<()> {
192 let f = self
193 .lua
194 .load(bytecode)
195 .set_name(chunk_name)
196 .set_mode(ChunkMode::Binary)
197 .into_function()?;
198 let va: Variadic<String> = args.iter().cloned().collect();
199 f.call::<()>(va)?;
200 Ok(())
201 }
202
203 pub fn preload_value(&self, name: &str, value: impl mlua::IntoLua) -> Result<()> {
205 let value = value.into_lua(&self.lua)?;
206 let loader = self
207 .lua
208 .create_function(move |_, ()| Ok(value.clone()))?;
209 self.preload_table()?.set(name, loader)?;
210 Ok(())
211 }
212
213 fn preload_table(&self) -> Result<Table> {
214 let package: Table = self.lua.globals().get("package")?;
215 Ok(package.get("preload")?)
216 }
217
218 pub fn set_arg(&self, script: &str, args: &[String]) -> Result<()> {
220 let t = self.lua.create_table()?;
221 t.set(0, script)?;
222 for (i, a) in args.iter().enumerate() {
223 t.set(i + 1, a.as_str())?;
224 }
225 self.lua.globals().set("arg", t)?;
226 Ok(())
227 }
228
229 pub fn exec(&self, lua_src: &str, chunk_name: &str, args: &[String]) -> Result<()> {
231 let f = self
232 .lua
233 .load(lua_src)
234 .set_name(chunk_name)
235 .into_function()?;
236 let va: Variadic<String> = args.iter().cloned().collect();
237 f.call::<()>(va)?;
238 Ok(())
239 }
240
241 pub fn run_file(&self, file: &Path, args: &[String]) -> Result<CheckInfo> {
244 self.add_path(&parent_dir(file))?;
245 self.install_searcher()?;
246 self.set_arg(&file.to_string_lossy(), args)?;
247 let (code, ci) = self.gen_lua(file)?;
248 let Some(code) = code else { return Ok(ci) };
249 self.exec(&code, &format!("@{}", file.display()), args)?;
250 Ok(ci)
251 }
252
253 pub fn compile(&self, name: &str, lua_src: &str) -> Result<Vec<u8>> {
255 let f = self
256 .lua
257 .load(lua_src)
258 .set_name(format!("={name}"))
259 .into_function()
260 .with_context(|| format!("compiling generated Lua for {name}"))?;
261 Ok(f.dump(true))
262 }
263
264 pub fn install_bundle(&self, b: &bundle::Bundle) -> Result<()> {
266 let modules = b.modules.clone();
267 let searcher = self.lua.create_function(move |lua, name: String| {
268 match modules.iter().find(|(n, _)| *n == name) {
269 Some((_, bc)) => {
270 let f = lua
271 .load(bc.as_slice())
272 .set_name(format!("={name}"))
273 .set_mode(ChunkMode::Binary)
274 .into_function()?;
275 Ok(Value::Function(f))
276 }
277 None => Ok(Value::String(
278 lua.create_string(format!("\n\tno bundled module '{name}'"))?,
279 )),
280 }
281 })?;
282 let package: Table = self.lua.globals().get("package")?;
283 let searchers: Table = package.get("searchers")?;
284 searchers.raw_insert(2, searcher)?;
285 Ok(())
286 }
287
288 pub fn run_bundle(&self, b: &bundle::Bundle, args: &[String]) -> Result<()> {
290 let entry_bc = b
291 .modules
292 .iter()
293 .find(|(n, _)| *n == b.entry)
294 .map(|(_, bc)| bc.clone())
295 .ok_or_else(|| anyhow!("entry module '{}' not in bundle", b.entry))?;
296 self.install_bundle(b)?;
297 self.set_arg(&b.entry, args)?;
298 let main: Function = self
299 .lua
300 .load(entry_bc.as_slice())
301 .set_name(format!("={}", b.entry))
302 .set_mode(ChunkMode::Binary)
303 .into_function()?;
304 let va: Variadic<String> = args.iter().cloned().collect();
305 main.call::<()>(va)?;
306 Ok(())
307 }
308}
309
310fn path_str(p: &Path) -> String {
311 p.to_string_lossy().into_owned()
312}
313
314pub fn write_if_changed(path: &Path, text: &str) -> std::io::Result<bool> {
317 if let Ok(cur) = std::fs::read_to_string(path)
318 && cur == text
319 {
320 return Ok(false);
321 }
322 if let Some(dir) = path.parent() {
323 std::fs::create_dir_all(dir)?;
324 }
325 std::fs::write(path, text)?;
326 Ok(true)
327}
328
329pub fn parent_dir(file: &Path) -> PathBuf {
331 let dir = file.parent().unwrap_or(Path::new("."));
332 if dir.as_os_str().is_empty() { PathBuf::from(".") } else { dir.to_path_buf() }
333}
334
335fn read_checkinfo(t: &Table) -> Result<CheckInfo> {
336 let seq = |key: &str| -> Result<Vec<String>> {
337 let inner: Table = t.get(key)?;
338 Ok(inner.sequence_values::<String>().collect::<mlua::Result<_>>()?)
339 };
340 Ok(CheckInfo {
341 errors: seq("errors")?,
342 warnings: seq("warnings")?,
343 deps: seq("deps")?.into_iter().map(PathBuf::from).collect(),
344 lints: seq("lints")?,
345 })
346}
347
348pub fn is_tl_source(p: &Path) -> bool {
350 let name = p.file_name().and_then(|s| s.to_str()).unwrap_or("");
351 p.is_file() && name.ends_with(".tl") && !name.ends_with(".d.tl")
352}
353
354pub fn collect_tl(paths: &[PathBuf]) -> Result<Vec<PathBuf>> {
356 let mut out = Vec::new();
357 for p in paths {
358 if p.is_dir() {
359 for e in walkdir::WalkDir::new(p).sort_by_file_name() {
360 let e = e?;
361 if is_tl_source(e.path()) {
362 out.push(e.path().to_path_buf());
363 }
364 }
365 } else if p.is_file() {
366 out.push(p.clone());
367 } else {
368 bail!("no such file or directory: {}", p.display());
369 }
370 }
371 Ok(out)
372}
373
374pub fn module_name(root: &Path, file: &Path) -> Result<String> {
376 let rel = file.strip_prefix(root)?.with_extension("");
377 let mut parts: Vec<String> = rel
378 .components()
379 .map(|c| c.as_os_str().to_string_lossy().into_owned())
380 .collect();
381 if parts.last().map(|s| s == "init").unwrap_or(false) {
382 parts.pop();
383 }
384 if parts.is_empty() {
385 bail!("cannot derive module name for {}", file.display());
386 }
387 Ok(parts.join("."))
388}