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;
19pub mod config;
20pub mod link;
21#[cfg(feature = "dts")]
22pub mod dts;
23#[cfg(feature = "pkg")]
24pub mod pkg;
25pub mod teal;
26pub mod testing;
27
28pub(crate) const PRELUDE_REGISTRY_KEY: &str = "htl.prelude";
31
32const TL_SRC: &str = include_str!("../vendor/tl.lua");
33const LINT_SRC: &str = include_str!("lint.lua");
34const FMT_SRC: &str = include_str!("fmt.lua");
35const PRELUDE: &str = include_str!("prelude.lua");
36
37pub const TEAL_VERSION: &str = "0.24.8";
39
40#[derive(Debug, Clone, Default)]
42pub struct CheckInfo {
43 pub errors: Vec<String>,
45 pub warnings: Vec<String>,
47 pub deps: Vec<PathBuf>,
49 pub lints: Vec<String>,
52 pub requires: Vec<RequireSite>,
55}
56
57#[derive(Debug, Clone)]
59pub struct RequireSite {
60 pub module: String,
61 pub path: Option<PathBuf>,
63 pub line: usize,
64 pub col: usize,
65}
66
67#[derive(Debug, Clone, Default)]
69pub struct ContractResult {
70 pub errors: Vec<String>,
72 pub missing: Option<Vec<String>>,
75 pub missing_at: (usize, usize),
76}
77
78impl Htl {
79 pub fn apply_config(&self, root: &Path, cfg: &config::HtlConfig) -> Result<()> {
82 for p in cfg.search_paths(root) {
83 self.add_path(&p)?;
84 }
85 Ok(())
86 }
87
88 pub fn contract_check(&self, file: &Path, modname: &str, type_path: &str, require_fields: bool) -> Result<ContractResult> {
91 let f: Function = self.h.get("contract_check")?;
92 let t: Table = f.call((path_str(file), modname, type_path, require_fields))?;
93 let errors: Table = t.get("errors")?;
94 let errors = errors.sequence_values::<String>().collect::<mlua::Result<_>>()?;
95 let missing = match t.get::<Option<Table>>("missing")? {
96 Some(m) => Some(m.sequence_values::<String>().collect::<mlua::Result<Vec<_>>>()?),
97 None => None,
98 };
99 let missing_at = (
100 t.get::<Option<usize>>("missing_y")?.unwrap_or(1),
101 t.get::<Option<usize>>("missing_x")?.unwrap_or(1),
102 );
103 Ok(ContractResult { errors, missing, missing_at })
104 }
105}
106
107pub fn contract_lints(h: &Htl, root: &Path, cfg: &config::HtlConfig, file: &Path) -> Result<Vec<String>> {
111 let canon = |p: &Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
112 let file_abs = canon(file);
113 let mut out = Vec::new();
114 if !is_tl_source(&file_abs) {
115 return Ok(out);
116 }
117 let modname = file_abs.file_stem().and_then(|s| s.to_str()).unwrap_or("").to_string();
118 for c in &cfg.contract {
119 let Some(dir) = c.dirs(root).into_iter().map(|d| canon(&d)).find(|d| file_abs.parent() == Some(d.as_path()))
120 else {
121 continue;
122 };
123 if !c.applies_to(&modname) {
124 continue;
125 }
126 h.add_path(&dir)?;
129 h.apply_config(root, cfg)?;
130 let r = h.contract_check(&file_abs, &modname, &c.type_path, c.require_fields)?;
131 for e in &r.errors {
132 let msg = e.splitn(4, ':').last().unwrap_or(e).trim();
134 out.push(format!(
135 "{}:1:1: does not satisfy contract {} ({}): {msg} [htl contract]",
136 file.display(),
137 c.type_path,
138 c.dir
139 ));
140 }
141 if let Some(missing) = &r.missing
142 && !missing.is_empty()
143 {
144 out.push(format!(
145 "{}:{}:{}: returned table lacks declared field(s) of {}: {} [htl contract]",
146 file.display(),
147 r.missing_at.0,
148 r.missing_at.1,
149 c.type_path,
150 missing.join(", ")
151 ));
152 }
153 }
154 Ok(out)
155}
156
157pub fn contract_enforcement_lints(cfg: &config::HtlConfig, cfg_path: &Path, cargo_root: Option<&Path>) -> Vec<String> {
163 let mut out = Vec::new();
164 if cfg.contract.is_empty() {
165 return out;
166 }
167 let Some(root) = cargo_root else { return out };
168 let mut sources = String::new();
169 for sub in ["src", "examples", "tests", "benches"] {
170 let dir = root.join(sub);
171 if !dir.is_dir() {
172 continue;
173 }
174 for e in walkdir::WalkDir::new(&dir).into_iter().flatten() {
175 let p = e.path();
176 if p.is_file()
177 && p.extension().and_then(|s| s.to_str()) == Some("rs")
178 && let Ok(t) = std::fs::read_to_string(p)
179 {
180 sources.push_str(&t);
181 sources.push('\n');
182 }
183 }
184 }
185 let by_config = ["contract_resolvers(", "for_contract(", "for_contract_dir("]
187 .iter()
188 .any(|api| sources.contains(api));
189 for c in &cfg.contract {
190 if c.dirs(root_of(cfg_path)).is_empty() {
193 continue;
194 }
195 let by_hand = sources.contains(&format!("expect_type(\"{}\")", c.type_path));
196 let want_fields = if c.require_fields { ".require_fields()" } else { "" };
197 if !(by_config || by_hand) {
198 let by_hand_hint = if c.dir.contains('*') {
199 String::new() } else {
201 format!("add TealResolver::new(\"{}\").expect_type(\"{}\"){} in the Rust host, or ", c.dir, c.type_path, want_fields)
202 };
203 out.push(format!(
204 "{}:1:1: contract `{}` -> {} is declared but the host does not enforce it: {}build resolvers with \
205 htl::pkg::contract_resolvers(root, &config) [htl contract-unenforced]",
206 cfg_path.display(),
207 c.dir,
208 c.type_path,
209 by_hand_hint
210 ));
211 } else if c.require_fields && !by_config && !sources.contains("require_fields()") {
212 out.push(format!(
213 "{}:1:1: contract `{}` -> {} has require_fields = true but the host never calls .require_fields(): \
214 missing fields will pass at run time [htl contract-unenforced]",
215 cfg_path.display(),
216 c.dir,
217 c.type_path
218 ));
219 }
220 }
221 out
222}
223
224fn root_of(cfg_path: &Path) -> &Path {
225 cfg_path.parent().unwrap_or(Path::new("."))
226}
227
228pub fn require_cycles(infos: &[(PathBuf, CheckInfo)]) -> Vec<String> {
233 use std::collections::{HashMap, HashSet};
234 let canon = |p: &Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
235 let mut edges: HashMap<PathBuf, Vec<(PathBuf, &RequireSite)>> = HashMap::new();
236 let mut display: HashMap<PathBuf, PathBuf> = HashMap::new();
237 for (file, ci) in infos {
238 let from = canon(file);
239 display.insert(from.clone(), file.clone());
240 let list = edges.entry(from).or_default();
241 for r in &ci.requires {
242 if let Some(p) = &r.path {
243 list.push((canon(p), r));
244 }
245 }
246 }
247 let nodes: Vec<PathBuf> = {
248 let mut v: Vec<PathBuf> = edges.keys().cloned().collect();
249 v.sort();
250 v
251 };
252 let mut out = Vec::new();
253 let mut reported: HashSet<Vec<PathBuf>> = HashSet::new();
254 let mut state: HashMap<PathBuf, u8> = HashMap::new(); let mut stack: Vec<(PathBuf, Option<&RequireSite>)> = Vec::new();
256
257 fn dfs<'a>(
258 node: PathBuf,
259 edges: &HashMap<PathBuf, Vec<(PathBuf, &'a RequireSite)>>,
260 state: &mut HashMap<PathBuf, u8>,
261 stack: &mut Vec<(PathBuf, Option<&'a RequireSite>)>,
262 reported: &mut HashSet<Vec<PathBuf>>,
263 display: &HashMap<PathBuf, PathBuf>,
264 out: &mut Vec<String>,
265 ) {
266 state.insert(node.clone(), 1);
267 if let Some(list) = edges.get(&node) {
268 for (to, site) in list {
269 match state.get(to).copied() {
270 Some(1) => {
271 let start = stack.iter().position(|(n, _)| n == to).unwrap_or(0);
273 let mut members: Vec<PathBuf> =
274 stack[start..].iter().map(|(n, _)| n.clone()).chain(std::iter::once(node.clone())).collect();
275 members.dedup();
276 let mut key = members.clone();
277 key.sort();
278 if reported.insert(key) {
279 let name = |p: &PathBuf| {
280 display
281 .get(p)
282 .unwrap_or(p)
283 .file_name()
284 .map(|s| s.to_string_lossy().into_owned())
285 .unwrap_or_else(|| p.display().to_string())
286 };
287 let chain: Vec<String> = members.iter().map(name).chain(std::iter::once(name(to))).collect();
288 let first_file = display.get(&members[0]).cloned().unwrap_or_else(|| members[0].clone());
289 let anchor = stack.get(start + 1).and_then(|(_, s)| *s).unwrap_or(site);
291 out.push(format!(
292 "{}:{}:{}: require cycle: {} (Teal types the back edge as an opaque circular require; \
293 break it by moving shared types into a module both sides require) [htl require-cycle]",
294 first_file.display(),
295 anchor.line,
296 anchor.col,
297 chain.join(" -> ")
298 ));
299 }
300 }
301 Some(2) => {}
302 _ => {
303 stack.push((to.clone(), Some(site)));
304 dfs(to.clone(), edges, state, stack, reported, display, out);
305 stack.pop();
306 }
307 }
308 }
309 }
310 state.insert(node, 2);
311 }
312
313 for n in nodes {
314 if !state.contains_key(&n) {
315 stack.push((n.clone(), None));
316 dfs(n, &edges, &mut state, &mut stack, &mut reported, &display, &mut out);
317 stack.pop();
318 }
319 }
320 out.sort();
321 out
322}
323
324impl CheckInfo {
325 pub fn ok(&self) -> bool {
326 self.errors.is_empty()
327 }
328
329 pub fn clean(&self) -> bool {
331 self.errors.is_empty() && self.warnings.is_empty() && self.lints.is_empty()
332 }
333}
334
335pub struct Htl {
337 lua: Lua,
339 h: Table,
342 split: bool,
344}
345
346pub(crate) struct CheckerHandle(pub(crate) Table);
349
350const RUNTIME_REGISTRY_KEY: &str = "htl.runtime";
351
352const RUNTIME_PRELUDE: &str = r#"
356local R = {}
357
358function R.type_only_module(module_name, decl_path)
359 return setmetatable({}, {
360 __index = function(_, key)
361 error(string.format(
362 "module '%s' is declaration-only here (%s): '%s' has no implementation on this path. " ..
363 "It must be provided by the host program (e.g. a Rust #[host_module] via cargo run) " ..
364 "or by a .tl/.lua module with that name.",
365 module_name, decl_path, tostring(key)), 2)
366 end,
367 })
368end
369
370-- gen(name) -> kind, a, b (see resolve_for_require in the checker prelude)
371function R.install_searcher(gen)
372 table.insert(package.searchers, 2, function(module_name)
373 local kind, a, b = gen(module_name)
374 if kind == "code" then
375 local chunk, lerr = load(a, "@" .. b, "t")
376 if not chunk then
377 error("htl: generated Lua failed to load: " .. tostring(lerr), 0)
378 end
379 return function(modname) return chunk(modname, b) end, b
380 elseif kind == "type_only" then
381 return function() return R.type_only_module(module_name, a) end, a
382 end
383 return a
384 end)
385end
386
387function R.add_path(dir)
388 local templates = dir .. "/?.lua;" .. dir .. "/?/init.lua;" .. dir .. "/?/?.lua"
389 if package.path == nil or package.path == "" then
390 package.path = templates
391 else
392 package.path = templates .. ";" .. package.path
393 end
394end
395
396function R.reset_path()
397 package.path = ""
398end
399
400return R
401"#;
402
403impl Htl {
404 pub fn new() -> Result<Self> {
406 let lua = unsafe { Lua::unsafe_new() };
408 Self::from_lua(lua)
409 }
410
411 pub fn with_checker(checker: &Htl) -> Result<Self> {
418 let lua = unsafe { Lua::unsafe_new() };
420 let r: Table = lua
421 .load(RUNTIME_PRELUDE)
422 .set_name("=htl-runtime")
423 .eval()
424 .context("loading htl runtime prelude")?;
425 lua.set_named_registry_value(RUNTIME_REGISTRY_KEY, r)?;
426 lua.set_app_data(CheckerHandle(checker.h.clone()));
427 let begin: Function = checker.h.get("begin_program")?;
428 begin.call::<()>(())?;
429 Ok(Self { lua, h: checker.h.clone(), split: true })
430 }
431
432 fn runtime(&self) -> Result<Table> {
433 Ok(self.lua.named_registry_value::<Table>(RUNTIME_REGISTRY_KEY)?)
434 }
435
436 pub fn search_path(&self) -> Result<String> {
438 let f: Function = self.h.get("get_path")?;
439 Ok(f.call(())?)
440 }
441
442 pub fn set_search_path(&self, path: &str) -> Result<()> {
444 let f: Function = self.h.get("set_path")?;
445 f.call::<()>(path)?;
446 Ok(())
447 }
448
449 pub fn from_lua(lua: Lua) -> Result<Self> {
451 let tl_loader: Function = lua
452 .load(TL_SRC)
453 .set_name("=tl.lua")
454 .into_function()
455 .context("compiling vendored tl.lua")?;
456 let lint_loader: Function = lua
457 .load(LINT_SRC)
458 .set_name("=htl-lint")
459 .into_function()
460 .context("compiling htl lint.lua")?;
461 let package: Table = lua.globals().get("package")?;
462 let preload: Table = package.get("preload")?;
463 let fmt_loader: Function = lua
464 .load(FMT_SRC)
465 .set_name("=htl-fmt")
466 .into_function()
467 .context("compiling htl fmt.lua")?;
468 preload.set("tl", tl_loader)?;
469 preload.set("htl.lint", lint_loader)?;
470 preload.set("htl.fmt", fmt_loader)?;
471 let h: Table = lua
472 .load(PRELUDE)
473 .set_name("=htl-prelude")
474 .eval()
475 .context("loading htl prelude")?;
476 lua.set_named_registry_value(PRELUDE_REGISTRY_KEY, h.clone())?;
477 Ok(Self { lua, h, split: false })
478 }
479
480 pub fn lua(&self) -> &Lua {
481 &self.lua
482 }
483
484 pub fn check(&self, file: &Path) -> Result<CheckInfo> {
486 let f: Function = self.h.get("check")?;
487 let t: Table = f.call(path_str(file))?;
488 read_checkinfo(&t)
489 }
490
491 pub fn gen_lua(&self, file: &Path) -> Result<(Option<String>, CheckInfo)> {
493 let f: Function = self.h.get("gen")?;
494 let (code, t): (Option<String>, Table) = f.call(path_str(file))?;
495 Ok((code, read_checkinfo(&t)?))
496 }
497
498 pub fn configure_lints(&self, spec: &str) -> Result<()> {
500 let f: Function = self.h.get("set_lints")?;
501 let (ok, err): (Option<bool>, Option<String>) = f.call(spec)?;
502 if ok.unwrap_or(false) {
503 Ok(())
504 } else {
505 bail!("{}", err.unwrap_or_else(|| "invalid lint spec".into()))
506 }
507 }
508
509 pub fn lint_rules(&self) -> Result<Vec<String>> {
511 let f: Function = self.h.get("lint_rules")?;
512 let t: Table = f.call(())?;
513 Ok(t.sequence_values::<String>().collect::<mlua::Result<_>>()?)
514 }
515
516 pub fn format_file(&self, file: &Path, indent: usize) -> Result<String> {
518 let f: Function = self.h.get("format")?;
519 let (out, err): (Option<String>, Option<String>) = f.call((path_str(file), indent))?;
520 out.ok_or_else(|| anyhow!("{}", err.unwrap_or_else(|| "format failed".into())))
521 }
522
523 pub fn reset_search_path(&self) -> Result<()> {
526 let f: Function = self.h.get("reset_path")?;
527 f.call::<()>(())?;
528 if self.split {
529 let f: Function = self.runtime()?.get("reset_path")?;
530 f.call::<()>(())?;
531 }
532 Ok(())
533 }
534
535 pub fn add_layout_paths(&self, file: &Path) -> Result<()> {
539 let dir = parent_dir(file);
540 self.add_path(&dir)?;
541 if dir.file_name().is_some_and(|n| n == "tests")
542 && let Some(root) = dir.parent()
543 {
544 self.add_path(root)?;
545 let src = root.join("src");
546 if src.is_dir() {
547 self.add_path(&src)?;
548 }
549 }
550 Ok(())
551 }
552
553 pub fn add_path(&self, dir: &Path) -> Result<()> {
555 let f: Function = self.h.get("add_path")?;
556 f.call::<()>(path_str(dir))?;
557 if self.split {
558 let f: Function = self.runtime()?.get("add_path")?;
560 f.call::<()>(path_str(dir))?;
561 }
562 Ok(())
563 }
564
565 pub fn install_searcher(&self) -> Result<()> {
567 if self.split {
568 let gen_fn: Function = self.h.get("gen_for_require")?;
570 let bridge = self.lua.create_function(move |_, name: String| {
571 let (kind, a, b): (String, Option<String>, Option<String>) = gen_fn.call(name)?;
572 Ok((kind, a, b))
573 })?;
574 let f: Function = self.runtime()?.get("install_searcher")?;
575 f.call::<()>(bridge)?;
576 return Ok(());
577 }
578 let f: Function = self.h.get("install_searcher")?;
579 f.call::<()>(())?;
580 Ok(())
581 }
582
583 pub fn preload(&self, name: &str, lua_src: &str) -> Result<()> {
585 let loader = self
586 .lua
587 .load(lua_src)
588 .set_name(format!("={name}"))
589 .into_function()
590 .with_context(|| format!("compiling preloaded module {name}"))?;
591 self.preload_table()?.set(name, loader)?;
592 Ok(())
593 }
594
595 pub fn preload_bytes(&self, name: &str, bytecode: &[u8]) -> Result<()> {
597 let loader = self
598 .lua
599 .load(bytecode)
600 .set_name(format!("={name}"))
601 .set_mode(ChunkMode::Binary)
602 .into_function()
603 .with_context(|| format!("loading bytecode for module {name}"))?;
604 self.preload_table()?.set(name, loader)?;
605 Ok(())
606 }
607
608 pub fn exec_bytes(&self, bytecode: &[u8], chunk_name: &str, args: &[String]) -> Result<()> {
610 let f = self
611 .lua
612 .load(bytecode)
613 .set_name(chunk_name)
614 .set_mode(ChunkMode::Binary)
615 .into_function()?;
616 let va: Variadic<String> = args.iter().cloned().collect();
617 f.call::<()>(va)?;
618 Ok(())
619 }
620
621 pub fn preload_value(&self, name: &str, value: impl mlua::IntoLua) -> Result<()> {
623 let value = value.into_lua(&self.lua)?;
624 let loader = self
625 .lua
626 .create_function(move |_, ()| Ok(value.clone()))?;
627 self.preload_table()?.set(name, loader)?;
628 Ok(())
629 }
630
631 fn preload_table(&self) -> Result<Table> {
632 let package: Table = self.lua.globals().get("package")?;
633 Ok(package.get("preload")?)
634 }
635
636 pub fn set_arg(&self, script: &str, args: &[String]) -> Result<()> {
638 let t = self.lua.create_table()?;
639 t.set(0, script)?;
640 for (i, a) in args.iter().enumerate() {
641 t.set(i + 1, a.as_str())?;
642 }
643 self.lua.globals().set("arg", t)?;
644 Ok(())
645 }
646
647 pub fn exec(&self, lua_src: &str, chunk_name: &str, args: &[String]) -> Result<()> {
649 let f = self
650 .lua
651 .load(lua_src)
652 .set_name(chunk_name)
653 .into_function()?;
654 let va: Variadic<String> = args.iter().cloned().collect();
655 f.call::<()>(va)?;
656 Ok(())
657 }
658
659 pub fn run_file(&self, file: &Path, args: &[String]) -> Result<CheckInfo> {
662 self.add_path(&parent_dir(file))?;
663 self.install_searcher()?;
664 self.set_arg(&file.to_string_lossy(), args)?;
665 let (code, ci) = self.gen_lua(file)?;
666 let Some(code) = code else { return Ok(ci) };
667 self.exec(&code, &format!("@{}", file.display()), args)?;
668 Ok(ci)
669 }
670
671 pub fn compile(&self, name: &str, lua_src: &str) -> Result<Vec<u8>> {
673 self.compile_with(name, lua_src, true)
674 }
675
676 pub fn compile_with(&self, name: &str, lua_src: &str, strip: bool) -> Result<Vec<u8>> {
679 let f = self
680 .lua
681 .load(lua_src)
682 .set_name(format!("={name}"))
683 .into_function()
684 .with_context(|| format!("compiling generated Lua for {name}"))?;
685 Ok(f.dump(strip))
686 }
687
688 pub fn fingerprint(&self) -> Result<Vec<u8>> {
693 let bc = self.compile_with("fp", "return 0", true)?;
694 Ok(bc.iter().take(31).copied().collect())
696 }
697
698 pub fn lua_requires(&self, src: &str, file: &Path) -> Result<Vec<RequireSite>> {
700 let f: Function = self.h.get("lua_requires")?;
701 let t: Table = f.call((src, path_str(file)))?;
702 read_requires(&t)
703 }
704
705 pub fn resolve_module(&self, name: &str) -> Result<(Option<PathBuf>, Option<PathBuf>)> {
709 let f: Function = self.h.get("resolve_module")?;
710 let (found, lua): (Option<String>, Option<String>) = f.call(name)?;
711 Ok((found.map(PathBuf::from), lua.map(PathBuf::from)))
712 }
713
714 pub fn install_bundle(&self, b: &bundle::Bundle) -> Result<()> {
716 if b.modules.iter().any(|m| m.kind == bundle::Kind::Bytecode) && !b.fingerprint.is_empty() {
719 let mine = self.fingerprint()?;
720 if mine != b.fingerprint {
721 bail!(
722 "bundle bytecode was compiled for {} but this host runs {}; rebuild the bundle here, \
723 or build it with --source",
724 bundle::describe_fingerprint(&b.fingerprint),
725 bundle::describe_fingerprint(&mine)
726 );
727 }
728 }
729 let package: Table = self.lua.globals().get("package")?;
732 let preload: Table = package.get("preload")?;
733 let loaded: Table = package.get("loaded")?;
734 let missing: Vec<&String> = b
735 .host_modules
736 .iter()
737 .filter(|n| {
738 matches!(preload.get::<Value>(n.as_str()), Ok(Value::Nil))
739 && matches!(loaded.get::<Value>(n.as_str()), Ok(Value::Nil))
740 })
741 .collect();
742 if !missing.is_empty() {
743 bail!(
744 "bundle expects host-provided module(s) {} (declared only by a .d.tl or [build] host at link \
745 time): register them with preload / preload_value / htl_preload before running",
746 missing.iter().map(|m| format!("'{m}'")).collect::<Vec<_>>().join(", ")
747 );
748 }
749 let modules = b.modules.clone();
750 let searcher = self.lua.create_function(move |lua, name: String| {
751 match modules.iter().find(|m| m.name == name) {
752 Some(m) => {
753 let chunk = lua.load(m.payload.as_slice()).set_name(format!("={name}"));
754 let f = match m.kind {
755 bundle::Kind::Bytecode => chunk.set_mode(ChunkMode::Binary).into_function()?,
756 bundle::Kind::Source => chunk.set_mode(ChunkMode::Text).into_function()?,
757 };
758 Ok((Value::Function(f), Value::String(lua.create_string(format!("bundle:{name}"))?)))
760 }
761 None => Ok((
762 Value::String(lua.create_string(format!("\n\tno bundled module '{name}'"))?),
763 Value::Nil,
764 )),
765 }
766 })?;
767 let searchers: Table = package.get("searchers")?;
768 searchers.raw_insert(2, searcher)?;
771 Ok(())
772 }
773
774 pub fn run_bundle(&self, b: &bundle::Bundle, args: &[String]) -> Result<()> {
776 let entry = b
777 .module(&b.entry)
778 .cloned()
779 .ok_or_else(|| anyhow!("entry module '{}' not in bundle", b.entry))?;
780 self.install_bundle(b)?;
781 self.set_arg(&b.entry, args)?;
782 let chunk = self.lua.load(entry.payload.as_slice()).set_name(format!("={}", b.entry));
783 let main: Function = match entry.kind {
784 bundle::Kind::Bytecode => chunk.set_mode(ChunkMode::Binary).into_function()?,
785 bundle::Kind::Source => chunk.set_mode(ChunkMode::Text).into_function()?,
786 };
787 let va: Variadic<String> = args.iter().cloned().collect();
788 main.call::<()>(va)?;
789 Ok(())
790 }
791}
792
793fn path_str(p: &Path) -> String {
794 p.to_string_lossy().into_owned()
795}
796
797pub fn user_message(err: &anyhow::Error) -> String {
806 fn from_mlua(e: &mlua::Error) -> String {
807 match e {
808 mlua::Error::CallbackError { cause, .. } => from_mlua(cause),
809 mlua::Error::ExternalError(ext) => ext.to_string(),
810 mlua::Error::WithContext { cause, .. } => from_mlua(cause),
811 other => strip_traceback(&other.to_string()),
812 }
813 }
814 if let Some(e) = err.downcast_ref::<mlua::Error>() {
815 return from_mlua(e);
816 }
817 strip_traceback(&format!("{err:#}"))
818}
819
820pub fn strip_traceback(text: &str) -> String {
822 let cut = text.find("\nstack traceback:").unwrap_or(text.len());
823 text[..cut].trim_end().to_string()
824}
825
826pub fn write_if_changed(path: &Path, text: &str) -> std::io::Result<bool> {
829 if let Ok(cur) = std::fs::read_to_string(path)
830 && cur == text
831 {
832 return Ok(false);
833 }
834 if let Some(dir) = path.parent() {
835 std::fs::create_dir_all(dir)?;
836 }
837 std::fs::write(path, text)?;
838 Ok(true)
839}
840
841pub fn parent_dir(file: &Path) -> PathBuf {
843 let dir = file.parent().unwrap_or(Path::new("."));
844 if dir.as_os_str().is_empty() { PathBuf::from(".") } else { dir.to_path_buf() }
845}
846
847fn read_checkinfo(t: &Table) -> Result<CheckInfo> {
848 let seq = |key: &str| -> Result<Vec<String>> {
849 let inner: Table = t.get(key)?;
850 Ok(inner.sequence_values::<String>().collect::<mlua::Result<_>>()?)
851 };
852 let requires = match t.get::<Table>("requires") {
853 Ok(list) => read_requires(&list)?,
854 Err(_) => Vec::new(),
855 };
856 Ok(CheckInfo {
857 errors: seq("errors")?,
858 warnings: seq("warnings")?,
859 deps: seq("deps")?.into_iter().map(PathBuf::from).collect(),
860 lints: seq("lints")?,
861 requires,
862 })
863}
864
865fn read_requires(list: &Table) -> Result<Vec<RequireSite>> {
866 let mut requires = Vec::new();
867 for r in list.sequence_values::<Table>() {
868 let r = r?;
869 requires.push(RequireSite {
870 module: r.get::<String>("name")?,
871 path: r.get::<Option<String>>("path")?.map(PathBuf::from),
872 line: r.get::<Option<usize>>("y")?.unwrap_or(0),
873 col: r.get::<Option<usize>>("x")?.unwrap_or(0),
874 });
875 }
876 Ok(requires)
877}
878
879pub fn is_tl_source(p: &Path) -> bool {
881 let name = p.file_name().and_then(|s| s.to_str()).unwrap_or("");
882 p.is_file() && name.ends_with(".tl") && !name.ends_with(".d.tl")
883}
884
885pub const SKIP_DIRS: &[&str] = &["target", "node_modules", ".mlua-pkgs", ".git"];
888
889pub fn is_skipped_dir(path: &Path, extra: &[PathBuf]) -> bool {
893 if !path.is_dir() {
894 return false;
895 }
896 let name = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
897 if SKIP_DIRS.contains(&name) || (name.starts_with('.') && name.len() > 1) {
898 return true;
899 }
900 extra.iter().any(|e| same_dir(path, e))
901}
902
903fn same_dir(a: &Path, b: &Path) -> bool {
904 match (std::fs::canonicalize(a), std::fs::canonicalize(b)) {
905 (Ok(x), Ok(y)) => x == y,
906 _ => a == b,
907 }
908}
909
910#[cfg(feature = "pkg")]
914pub fn project_skip_dirs(root: &Path) -> Vec<PathBuf> {
915 match pkg::Project::find(root) {
916 Some(p) => vec![p.pkgs_dir],
917 None => Vec::new(),
918 }
919}
920
921#[cfg(not(feature = "pkg"))]
922pub fn project_skip_dirs(_root: &Path) -> Vec<PathBuf> {
923 Vec::new()
924}
925
926pub fn collect_tl(paths: &[PathBuf]) -> Result<Vec<PathBuf>> {
930 let mut out = Vec::new();
931 for p in paths {
932 if p.is_dir() {
933 let extra = project_skip_dirs(p);
934 let root = p.clone();
935 let walker = walkdir::WalkDir::new(p)
936 .sort_by_file_name()
937 .into_iter()
938 .filter_entry(move |e| e.path() == root || !is_skipped_dir(e.path(), &extra));
939 for e in walker {
940 let e = e?;
941 if is_tl_source(e.path()) {
942 out.push(e.path().to_path_buf());
943 }
944 }
945 } else if p.is_file() {
946 out.push(p.clone());
947 } else {
948 bail!("no such file or directory: {}", p.display());
949 }
950 }
951 Ok(out)
952}
953
954pub fn module_name(root: &Path, file: &Path) -> Result<String> {
956 let rel = file.strip_prefix(root)?.with_extension("");
957 let mut parts: Vec<String> = rel
958 .components()
959 .map(|c| c.as_os_str().to_string_lossy().into_owned())
960 .collect();
961 if parts.last().map(|s| s == "init").unwrap_or(false) {
962 parts.pop();
963 }
964 if parts.is_empty() {
965 bail!("cannot derive module name for {}", file.display());
966 }
967 Ok(parts.join("."))
968}