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 contract;
21#[cfg(feature = "dts")]
22pub mod dts;
23pub mod fix;
24pub mod link;
25#[cfg(feature = "pkg")]
26pub mod pkg;
27pub mod teal;
28pub mod testing;
29
30pub(crate) const PRELUDE_REGISTRY_KEY: &str = "htl.prelude";
33
34const TL_SRC: &str = include_str!("../vendor/tl.lua");
35const LINT_SRC: &str = include_str!("lint.lua");
36const FMT_SRC: &str = include_str!("fmt.lua");
37const PRELUDE: &str = include_str!("prelude.lua");
38
39pub const TEAL_VERSION: &str = "0.24.8";
41
42#[derive(Debug, Clone, Default)]
44pub struct CheckInfo {
45 pub errors: Vec<String>,
47 pub warnings: Vec<String>,
49 pub deps: Vec<PathBuf>,
51 pub lints: Vec<String>,
54 pub requires: Vec<RequireSite>,
57 pub error_fixes: Vec<Option<Fix>>,
59 pub lint_fixes: Vec<Option<Fix>>,
61 pub dependency_errors: Vec<DependencyError>,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct DependencyError {
79 pub file: PathBuf,
81 pub required_by: PathBuf,
84 pub text: String,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum Applicability {
91 Safe,
93 Unsafe,
95 Suggest,
97}
98
99impl Applicability {
100 pub fn as_str(self) -> &'static str {
101 match self {
102 Applicability::Safe => "safe",
103 Applicability::Unsafe => "unsafe",
104 Applicability::Suggest => "suggest",
105 }
106 }
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct Edit {
113 pub line: usize,
114 pub col: usize,
115 pub end_line: usize,
116 pub end_col: usize,
117 pub text: String,
118}
119
120#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct Fix {
123 pub applicability: Applicability,
124 pub edits: Vec<Edit>,
125}
126
127#[derive(Debug, Clone)]
129pub struct RequireSite {
130 pub module: String,
131 pub path: Option<PathBuf>,
133 pub line: usize,
134 pub col: usize,
135}
136
137#[derive(Debug, Clone)]
145pub struct FunctionSpan {
146 pub name: String,
148 pub line: usize,
150 pub last: usize,
152}
153
154pub type CoverageSpans = (Vec<(usize, usize)>, Vec<FunctionSpan>);
157
158#[derive(Debug, Clone, Default)]
160pub struct ContractResult {
161 pub errors: Vec<String>,
163 pub missing: Option<Vec<String>>,
166 pub missing_at: (usize, usize),
167 pub bad_require_fields: Vec<String>,
171}
172
173impl Htl {
174 pub fn apply_config(&self, root: &Path, cfg: &config::HtlConfig) -> Result<()> {
177 self.add_search_paths(&cfg.search_paths(root))
178 }
179
180 pub fn add_search_paths(&self, dirs: &[PathBuf]) -> Result<()> {
189 for p in dirs.iter().rev() {
190 self.add_path(p)?;
191 }
192 Ok(())
193 }
194
195 pub fn contract_check(
198 &self,
199 file: &Path,
200 modname: &str,
201 type_path: &str,
202 require_fields: &config::RequireFields,
203 ) -> Result<ContractResult> {
204 let f: Function = self.h.get("contract_check")?;
205 let wanted = match require_fields.named() {
207 Some(names) => mlua::Value::Table(self.lua().create_sequence_from(names.to_vec())?),
208 None => mlua::Value::Boolean(require_fields.is_on()),
209 };
210 let t: Table = f.call((path_str(file), modname, type_path, wanted))?;
211 let errors: Table = t.get("errors")?;
212 let errors = errors
213 .sequence_values::<String>()
214 .collect::<mlua::Result<_>>()?;
215 let missing = match t.get::<Option<Table>>("missing")? {
216 Some(m) => Some(
217 m.sequence_values::<String>()
218 .collect::<mlua::Result<Vec<_>>>()?,
219 ),
220 None => None,
221 };
222 let missing_at = (
223 t.get::<Option<usize>>("missing_y")?.unwrap_or(1),
224 t.get::<Option<usize>>("missing_x")?.unwrap_or(1),
225 );
226 let bad_require_fields = match t.get::<Option<Table>>("bad_require_fields")? {
227 Some(b) => b
228 .sequence_values::<String>()
229 .collect::<mlua::Result<Vec<_>>>()?,
230 None => Vec::new(),
231 };
232 Ok(ContractResult {
233 errors,
234 missing,
235 missing_at,
236 bad_require_fields,
237 })
238 }
239}
240
241pub fn contract_lints(
248 h: &Htl,
249 root: &Path,
250 cfg: &config::HtlConfig,
251 contracts: &[contract::Resolved],
252 file: &Path,
253) -> Result<Vec<String>> {
254 let canon = |p: &Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
255 let file_abs = canon(file);
256 let mut out = Vec::new();
257 if !is_tl_source(&file_abs) {
258 return Ok(out);
259 }
260 let modname = file_abs
261 .file_stem()
262 .and_then(|s| s.to_str())
263 .unwrap_or("")
264 .to_string();
265 for c in contracts {
266 let Some(dir) = c
267 .dirs(root)
268 .into_iter()
269 .map(|d| canon(&d))
270 .find(|d| file_abs.parent() == Some(d.as_path()))
271 else {
272 continue;
273 };
274 if !c.applies_to(&modname) {
275 continue;
276 }
277 h.add_path(&dir)?;
281 h.apply_config(root, cfg)?;
282 let r = h.contract_check(&file_abs, &modname, &c.type_path, &c.require_fields)?;
283 if !r.bad_require_fields.is_empty() {
284 out.push(format!(
287 "{}:{}:1: ---@required on field(s) {} does not declare: {} [htl contract]",
288 c.declared_in.display(),
289 c.declared_at,
290 c.type_path,
291 r.bad_require_fields.join(", ")
292 ));
293 continue;
294 }
295 for e in &r.errors {
296 let msg = e.splitn(4, ':').last().unwrap_or(e).trim();
298 out.push(format!(
299 "{}:1:1: does not satisfy contract {} ({}): {msg} [htl contract]",
300 file.display(),
301 c.type_path,
302 c.dir
303 ));
304 }
305 if let Some(missing) = &r.missing
306 && !missing.is_empty()
307 {
308 out.push(format!(
309 "{}:{}:{}: returned table lacks declared field(s) of {}: {} [htl contract]",
310 file.display(),
311 r.missing_at.0,
312 r.missing_at.1,
313 c.type_path,
314 missing.join(", ")
315 ));
316 }
317 }
318 Ok(out)
319}
320
321pub fn declaration_conflict_lints(h: &Htl, file: &Path, info: &CheckInfo) -> Result<Vec<String>> {
333 let f: Function = h.h.get("declaration_sites")?;
334 let mut out = Vec::new();
335 let mut seen: Vec<&str> = Vec::new();
336 for site in &info.requires {
337 let Some(read) = site.path.as_ref().filter(|p| is_declaration(p)) else {
338 continue;
339 };
340 if seen.contains(&site.module.as_str()) {
342 continue;
343 }
344 let sites: Vec<String> = f
345 .call::<Table>(site.module.as_str())?
346 .sequence_values::<String>()
347 .collect::<mlua::Result<_>>()?;
348 let shadowed: Vec<&str> = sites
349 .iter()
350 .map(|s| s.as_str())
351 .filter(|s| !same_file(Path::new(s), read))
352 .collect();
353 if shadowed.is_empty() {
354 continue;
355 }
356 seen.push(&site.module);
357 out.push(format!(
358 "{}:{}:{}: {} is declared more than once on the search path: {} is read, {} {} not [htl duplicate-declaration]",
359 file.display(),
360 site.line,
361 site.col,
362 site.module,
363 read.display(),
364 shadowed.join(" and "),
365 if shadowed.len() == 1 { "is" } else { "are" },
366 ));
367 }
368 Ok(out)
369}
370
371pub fn contract_enforcement_lints(
387 cfg_path: &Path,
388 contracts: &[contract::Resolved],
389 cargo_root: Option<&Path>,
390) -> Vec<String> {
391 let mut out = Vec::new();
392 if contracts.is_empty() {
393 return out;
394 }
395 let Some(root) = cargo_root else { return out };
396 let mut sources = String::new();
397 for sub in ["src", "examples", "tests", "benches"] {
398 let dir = root.join(sub);
399 if !dir.is_dir() {
400 continue;
401 }
402 for e in walkdir::WalkDir::new(&dir).into_iter().flatten() {
403 let p = e.path();
404 if p.is_file()
405 && p.extension().and_then(|s| s.to_str()) == Some("rs")
406 && let Ok(t) = std::fs::read_to_string(p)
407 {
408 sources.push_str(&t);
409 sources.push('\n');
410 }
411 }
412 }
413 let by_config = sources.contains("contract_resolvers(");
414 for c in contracts {
415 if c.dirs(root_of(cfg_path)).is_empty() {
418 continue;
419 }
420 match &c.enforced_by {
421 Some(p) => {
425 let at = config::resolve_path(root_of(cfg_path), p);
426 if !at.exists() {
427 out.push(format!(
428 "{}:1:1: contract {} -> {} says it is enforced by {:?}, and there \
429 is no such file: name where the enforcement lives, or drop the \
430 key and let the scan look for \
431 htl::pkg::contract_resolvers(root, &config) \
432 [htl contract-unenforced]",
433 cfg_path.display(),
434 c.dir,
435 c.type_path,
436 p,
437 ));
438 }
439 }
440 None if !by_config => out.push(format!(
441 "{}:{}:1: contract {} -> {} is declared but the host does not enforce it: \
442 build resolvers with htl::pkg::contract_resolvers(root, &config), or say \
443 where it is enforced with [[contract]] enforced_by \
444 [htl contract-unenforced]",
445 c.declared_in.display(),
446 c.declared_at,
447 c.dir,
448 c.type_path,
449 )),
450 None => {}
451 }
452 }
453 out
454}
455
456fn root_of(cfg_path: &Path) -> &Path {
457 cfg_path.parent().unwrap_or(Path::new("."))
458}
459
460pub fn require_cycles(infos: &[(PathBuf, CheckInfo)]) -> Vec<String> {
465 use std::collections::{HashMap, HashSet};
466 let canon = |p: &Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
467 let mut edges: HashMap<PathBuf, Vec<(PathBuf, &RequireSite)>> = HashMap::new();
468 let mut display: HashMap<PathBuf, PathBuf> = HashMap::new();
469 for (file, ci) in infos {
470 let from = canon(file);
471 display.insert(from.clone(), file.clone());
472 let list = edges.entry(from).or_default();
473 for r in &ci.requires {
474 if let Some(p) = &r.path {
475 list.push((canon(p), r));
476 }
477 }
478 }
479 let nodes: Vec<PathBuf> = {
480 let mut v: Vec<PathBuf> = edges.keys().cloned().collect();
481 v.sort();
482 v
483 };
484 let mut out = Vec::new();
485 let mut reported: HashSet<Vec<PathBuf>> = HashSet::new();
486 let mut state: HashMap<PathBuf, u8> = HashMap::new(); let mut stack: Vec<(PathBuf, Option<&RequireSite>)> = Vec::new();
488
489 fn dfs<'a>(
490 node: PathBuf,
491 edges: &HashMap<PathBuf, Vec<(PathBuf, &'a RequireSite)>>,
492 state: &mut HashMap<PathBuf, u8>,
493 stack: &mut Vec<(PathBuf, Option<&'a RequireSite>)>,
494 reported: &mut HashSet<Vec<PathBuf>>,
495 display: &HashMap<PathBuf, PathBuf>,
496 out: &mut Vec<String>,
497 ) {
498 state.insert(node.clone(), 1);
499 if let Some(list) = edges.get(&node) {
500 for (to, site) in list {
501 match state.get(to).copied() {
502 Some(1) => {
503 let start = stack.iter().position(|(n, _)| n == to).unwrap_or(0);
505 let mut members: Vec<PathBuf> = stack[start..]
506 .iter()
507 .map(|(n, _)| n.clone())
508 .chain(std::iter::once(node.clone()))
509 .collect();
510 members.dedup();
511 let mut key = members.clone();
512 key.sort();
513 if reported.insert(key) {
514 let name = |p: &PathBuf| {
515 display
516 .get(p)
517 .unwrap_or(p)
518 .file_name()
519 .map(|s| s.to_string_lossy().into_owned())
520 .unwrap_or_else(|| p.display().to_string())
521 };
522 let chain: Vec<String> = members
523 .iter()
524 .map(name)
525 .chain(std::iter::once(name(to)))
526 .collect();
527 let first_file = display
528 .get(&members[0])
529 .cloned()
530 .unwrap_or_else(|| members[0].clone());
531 let anchor = stack.get(start + 1).and_then(|(_, s)| *s).unwrap_or(site);
533 out.push(format!(
534 "{}:{}:{}: require cycle: {} (Teal types the back edge as an opaque circular require; \
535 break it by moving shared types into a module both sides require) [htl require-cycle]",
536 first_file.display(),
537 anchor.line,
538 anchor.col,
539 chain.join(" -> ")
540 ));
541 }
542 }
543 Some(2) => {}
544 _ => {
545 stack.push((to.clone(), Some(site)));
546 dfs(to.clone(), edges, state, stack, reported, display, out);
547 stack.pop();
548 }
549 }
550 }
551 }
552 state.insert(node, 2);
553 }
554
555 for n in nodes {
556 if !state.contains_key(&n) {
557 stack.push((n.clone(), None));
558 dfs(
559 n,
560 &edges,
561 &mut state,
562 &mut stack,
563 &mut reported,
564 &display,
565 &mut out,
566 );
567 stack.pop();
568 }
569 }
570 out.sort();
571 out
572}
573
574impl CheckInfo {
575 pub fn ok(&self) -> bool {
576 self.errors.is_empty()
577 }
578
579 pub fn clean(&self) -> bool {
581 self.errors.is_empty() && self.warnings.is_empty() && self.lints.is_empty()
582 }
583}
584
585pub struct Htl {
587 lua: Lua,
589 h: Table,
592 split: bool,
594}
595
596pub(crate) struct CheckerHandle(pub(crate) Table);
599
600const RUNTIME_REGISTRY_KEY: &str = "htl.runtime";
601
602const RUNTIME_PRELUDE: &str = r#"
606local R = {}
607
608function R.type_only_module(module_name, decl_path)
609 return setmetatable({}, {
610 __index = function(_, key)
611 error(string.format(
612 "module '%s' is declaration-only here (%s): '%s' has no implementation on this path. " ..
613 "It must be provided by the host program (e.g. a Rust #[host_module] via cargo run) " ..
614 "or by a .tl/.lua module with that name.",
615 module_name, decl_path, tostring(key)), 2)
616 end,
617 })
618end
619
620-- gen(name) -> kind, a, b (see resolve_for_require in the checker prelude)
621function R.install_searcher(gen)
622 table.insert(package.searchers, 2, function(module_name)
623 local kind, a, b = gen(module_name)
624 if kind == "code" then
625 local chunk, lerr = load(a, "@" .. b, "t")
626 if not chunk then
627 error("htl: generated Lua failed to load: " .. tostring(lerr), 0)
628 end
629 return function(modname) return chunk(modname, b) end, b
630 elseif kind == "type_only" then
631 return function() return R.type_only_module(module_name, a) end, a
632 end
633 return a
634 end)
635end
636
637-- Put already-generated Lua in front of the searcher for one module name.
638--
639-- `package.preload` is searcher position 1 and R.install_searcher puts htl's at 2, so a
640-- preloaded module is never asked of the searcher — which is the point: asking would check
641-- and generate it again. Loaded the same way the searcher would have loaded it, so the
642-- module sees the same chunk name and the same arguments.
643function R.preload_generated(module_name, code, filename)
644 -- Never displace what is already there. The test library and anything a host preloads are
645 -- put in package.preload by whoever owns them, and Lua generated from a `.tl` of the same
646 -- name is not the same module: preloading over `htl.test` gives the file a stand-in whose
647 -- `run()` reports nothing, and every test silently stops counting.
648 if package.preload[module_name] ~= nil then return end
649 local chunk, lerr = load(code, "@" .. filename, "t")
650 if not chunk then
651 error("htl: cached Lua failed to load: " .. tostring(lerr), 0)
652 end
653 package.preload[module_name] = function(modname) return chunk(modname, filename) end
654end
655
656function R.add_path(dir)
657 local templates = dir .. "/?.lua;" .. dir .. "/?/init.lua;" .. dir .. "/?/?.lua"
658 if package.path == nil or package.path == "" then
659 package.path = templates
660 else
661 package.path = templates .. ";" .. package.path
662 end
663end
664
665function R.reset_path()
666 package.path = ""
667end
668
669-- Line coverage: which lines of which chunk ran. Lua's line hook is per thread, so
670-- code that runs inside a coroutine the test creates is not seen.
671local cov = nil
672function R.coverage_start()
673 cov = {}
674 -- The line event is the hot path. One "S" lookup per function (cached by the
675 -- function object) instead of per line; a call/return-event stack was measured
676 -- slower on a call-heavy suite, since calls are almost as frequent as lines there.
677 local srcs = setmetatable({}, { __mode = "k" })
678 local getinfo = debug.getinfo
679 debug.sethook(function(_, line)
680 local fi = getinfo(2, "f")
681 local func = fi and fi.func
682 if func == nil then return end
683 local t = srcs[func]
684 if t == nil then
685 local si = getinfo(2, "S")
686 local src = si and si.source
687 t = false
688 if src then
689 t = cov[src]
690 if not t then
691 t = {}
692 cov[src] = t
693 end
694 end
695 srcs[func] = t
696 end
697 if t then t[line] = true end
698 end, "l")
699end
700
701function R.coverage_stop()
702 debug.sethook()
703 local out = {}
704 for src, lines in pairs(cov or {}) do
705 local list = {}
706 for l in pairs(lines) do list[#list + 1] = l end
707 table.sort(list)
708 out[#out + 1] = { source = src, lines = list }
709 end
710 cov = nil
711 return out
712end
713
714return R
715"#;
716
717impl Htl {
718 pub fn new() -> Result<Self> {
720 let lua = unsafe { Lua::unsafe_new() };
722 Self::from_lua(lua)
723 }
724
725 pub fn with_checker(checker: &Htl) -> Result<Self> {
732 let lua = unsafe { Lua::unsafe_new() };
734 let r: Table = lua
735 .load(RUNTIME_PRELUDE)
736 .set_name("=htl-runtime")
737 .eval()
738 .context("loading htl runtime prelude")?;
739 lua.set_named_registry_value(RUNTIME_REGISTRY_KEY, r)?;
740 lua.set_app_data(CheckerHandle(checker.h.clone()));
741 let begin: Function = checker.h.get("begin_program")?;
742 begin.call::<()>(())?;
743 Ok(Self {
744 lua,
745 h: checker.h.clone(),
746 split: true,
747 })
748 }
749
750 fn runtime(&self) -> Result<Table> {
751 Ok(self
752 .lua
753 .named_registry_value::<Table>(RUNTIME_REGISTRY_KEY)?)
754 }
755
756 pub fn preload_generated(&self, name: &str, code: &str, file: &Path) -> Result<()> {
769 let f: Function = self.runtime()?.get("preload_generated")?;
770 f.call::<()>((name, code, path_str(file)))?;
771 Ok(())
772 }
773
774 pub fn coverage_start(&self) -> Result<()> {
778 let f: Function = self.runtime()?.get("coverage_start")?;
779 f.call::<()>(())?;
780 Ok(())
781 }
782
783 pub fn coverage_stop(&self) -> Result<Vec<(String, Vec<usize>)>> {
786 let f: Function = self.runtime()?.get("coverage_stop")?;
787 let t: Table = f.call(())?;
788 let mut out = Vec::new();
789 for e in t.sequence_values::<Table>() {
790 let e = e?;
791 let source: String = e.get("source")?;
792 let lines: Table = e.get("lines")?;
793 out.push((
794 source,
795 lines
796 .sequence_values::<usize>()
797 .collect::<mlua::Result<_>>()?,
798 ));
799 }
800 Ok(out)
801 }
802
803 pub fn executable_ranges(&self, file: &Path) -> Result<Vec<(usize, usize)>> {
807 Ok(self.coverage_spans(file)?.0)
808 }
809
810 pub fn coverage_spans(&self, file: &Path) -> Result<CoverageSpans> {
814 let f: Function = self.h.get("executable_ranges")?;
815 let (ranges, funcs): (Option<Table>, Option<Table>) = f.call(path_str(file))?;
816 let Some(ranges) = ranges else {
817 return Ok((Vec::new(), Vec::new()));
818 };
819 let mut out = Vec::new();
820 for r in ranges.sequence_values::<Table>() {
821 let r = r?;
822 out.push((r.get::<usize>(1)?, r.get::<usize>(2)?));
823 }
824 let mut fns = Vec::new();
825 if let Some(funcs) = funcs {
826 for f in funcs.sequence_values::<Table>() {
827 let f = f?;
828 fns.push(FunctionSpan {
829 name: f.get("name")?,
830 line: f.get("y")?,
831 last: f.get("last")?,
832 });
833 }
834 }
835 Ok((out, fns))
836 }
837
838 pub fn search_path(&self) -> Result<String> {
840 let f: Function = self.h.get("get_path")?;
841 Ok(f.call(())?)
842 }
843
844 pub fn set_search_path(&self, path: &str) -> Result<()> {
846 let f: Function = self.h.get("set_path")?;
847 f.call::<()>(path)?;
848 Ok(())
849 }
850
851 pub fn from_lua(lua: Lua) -> Result<Self> {
853 let tl_loader: Function = lua
854 .load(TL_SRC)
855 .set_name("=tl.lua")
856 .into_function()
857 .context("compiling vendored tl.lua")?;
858 let lint_loader: Function = lua
859 .load(LINT_SRC)
860 .set_name("=htl-lint")
861 .into_function()
862 .context("compiling htl lint.lua")?;
863 let package: Table = lua.globals().get("package")?;
864 let preload: Table = package.get("preload")?;
865 let fmt_loader: Function = lua
866 .load(FMT_SRC)
867 .set_name("=htl-fmt")
868 .into_function()
869 .context("compiling htl fmt.lua")?;
870 preload.set("tl", tl_loader)?;
871 preload.set("htl.lint", lint_loader)?;
872 preload.set("htl.fmt", fmt_loader)?;
873 let h: Table = lua
874 .load(PRELUDE)
875 .set_name("=htl-prelude")
876 .eval()
877 .context("loading htl prelude")?;
878 lua.set_named_registry_value(PRELUDE_REGISTRY_KEY, h.clone())?;
879 Ok(Self {
880 lua,
881 h,
882 split: false,
883 })
884 }
885
886 pub fn lua(&self) -> &Lua {
887 &self.lua
888 }
889
890 pub fn check(&self, file: &Path) -> Result<CheckInfo> {
892 let f: Function = self.h.get("check")?;
893 let t: Table = f.call(path_str(file))?;
894 read_checkinfo(&t)
895 }
896
897 pub fn check_written(&self, file: &Path) -> Result<CheckInfo> {
911 let f: Function = self.h.get("check")?;
912 let opts = self.lua.create_table()?;
913 opts.set("seed", false)?;
914 opts.set("store", false)?;
915 let t: Table = f.call((path_str(file), mlua::Value::Nil, opts))?;
917 read_checkinfo(&t)
918 }
919
920 pub fn gen_lua(&self, file: &Path) -> Result<(Option<String>, CheckInfo)> {
922 let f: Function = self.h.get("gen")?;
923 let (code, t): (Option<String>, Table) = f.call(path_str(file))?;
924 Ok((code, read_checkinfo(&t)?))
925 }
926
927 pub fn configure_lints(&self, spec: &str) -> Result<()> {
929 let f: Function = self.h.get("set_lints")?;
930 let (ok, err): (Option<bool>, Option<String>) = f.call(spec)?;
931 if ok.unwrap_or(false) {
932 Ok(())
933 } else {
934 bail!("{}", err.unwrap_or_else(|| "invalid lint spec".into()))
935 }
936 }
937
938 pub fn lint_rules(&self) -> Result<Vec<String>> {
940 let f: Function = self.h.get("lint_rules")?;
941 let t: Table = f.call(())?;
942 Ok(t.sequence_values::<String>().collect::<mlua::Result<_>>()?)
943 }
944
945 pub fn format_file(&self, file: &Path, indent: usize) -> Result<String> {
947 let f: Function = self.h.get("format")?;
948 let (out, err): (Option<String>, Option<String>) = f.call((path_str(file), indent))?;
949 out.ok_or_else(|| anyhow!("{}", err.unwrap_or_else(|| "format failed".into())))
950 }
951
952 pub fn reset_search_path(&self) -> Result<()> {
955 let f: Function = self.h.get("reset_path")?;
956 f.call::<()>(())?;
957 if self.split {
958 let f: Function = self.runtime()?.get("reset_path")?;
959 f.call::<()>(())?;
960 }
961 Ok(())
962 }
963
964 pub fn add_layout_paths(&self, file: &Path) -> Result<()> {
969 let dir = parent_dir(file);
970 let mut dirs = vec![dir.clone()];
971 if dir.file_name().is_some_and(|n| n == "tests")
972 && let Some(root) = dir.parent()
973 {
974 dirs.push(root.to_path_buf());
975 let src = root.join("src");
976 if src.is_dir() {
977 dirs.push(src);
978 }
979 }
980 self.add_search_paths(&dirs)
981 }
982
983 pub fn add_path(&self, dir: &Path) -> Result<()> {
985 let f: Function = self.h.get("add_path")?;
986 f.call::<()>(path_str(dir))?;
987 if self.split {
988 let f: Function = self.runtime()?.get("add_path")?;
990 f.call::<()>(path_str(dir))?;
991 }
992 Ok(())
993 }
994
995 pub fn install_searcher(&self) -> Result<()> {
997 if self.split {
998 let gen_fn: Function = self.h.get("gen_for_require")?;
1000 let bridge = self.lua.create_function(move |_, name: String| {
1001 let (kind, a, b): (String, Option<String>, Option<String>) = gen_fn.call(name)?;
1002 Ok((kind, a, b))
1003 })?;
1004 let f: Function = self.runtime()?.get("install_searcher")?;
1005 f.call::<()>(bridge)?;
1006 return Ok(());
1007 }
1008 let f: Function = self.h.get("install_searcher")?;
1009 f.call::<()>(())?;
1010 Ok(())
1011 }
1012
1013 pub fn preload(&self, name: &str, lua_src: &str) -> Result<()> {
1015 let loader = self
1016 .lua
1017 .load(lua_src)
1018 .set_name(format!("={name}"))
1019 .into_function()
1020 .with_context(|| format!("compiling preloaded module {name}"))?;
1021 self.preload_table()?.set(name, loader)?;
1022 Ok(())
1023 }
1024
1025 pub fn preload_bytes(&self, name: &str, bytecode: &[u8]) -> Result<()> {
1027 let loader = self
1028 .lua
1029 .load(bytecode)
1030 .set_name(format!("={name}"))
1031 .set_mode(ChunkMode::Binary)
1032 .into_function()
1033 .with_context(|| format!("loading bytecode for module {name}"))?;
1034 self.preload_table()?.set(name, loader)?;
1035 Ok(())
1036 }
1037
1038 pub fn exec_bytes(&self, bytecode: &[u8], chunk_name: &str, args: &[String]) -> Result<()> {
1040 let f = self
1041 .lua
1042 .load(bytecode)
1043 .set_name(chunk_name)
1044 .set_mode(ChunkMode::Binary)
1045 .into_function()?;
1046 let va: Variadic<String> = args.iter().cloned().collect();
1047 f.call::<()>(va)?;
1048 Ok(())
1049 }
1050
1051 pub fn preload_value(&self, name: &str, value: impl mlua::IntoLua) -> Result<()> {
1053 let value = value.into_lua(&self.lua)?;
1054 let loader = self.lua.create_function(move |_, ()| Ok(value.clone()))?;
1055 self.preload_table()?.set(name, loader)?;
1056 Ok(())
1057 }
1058
1059 fn preload_table(&self) -> Result<Table> {
1060 let package: Table = self.lua.globals().get("package")?;
1061 Ok(package.get("preload")?)
1062 }
1063
1064 pub fn set_arg(&self, script: &str, args: &[String]) -> Result<()> {
1066 let t = self.lua.create_table()?;
1067 t.set(0, script)?;
1068 for (i, a) in args.iter().enumerate() {
1069 t.set(i + 1, a.as_str())?;
1070 }
1071 self.lua.globals().set("arg", t)?;
1072 Ok(())
1073 }
1074
1075 pub fn exec(&self, lua_src: &str, chunk_name: &str, args: &[String]) -> Result<()> {
1077 let f = self
1078 .lua
1079 .load(lua_src)
1080 .set_name(chunk_name)
1081 .into_function()?;
1082 let va: Variadic<String> = args.iter().cloned().collect();
1083 f.call::<()>(va)?;
1084 Ok(())
1085 }
1086
1087 pub fn run_file(&self, file: &Path, args: &[String]) -> Result<CheckInfo> {
1090 self.add_path(&parent_dir(file))?;
1091 self.install_searcher()?;
1092 self.set_arg(&file.to_string_lossy(), args)?;
1093 let (code, ci) = self.gen_lua(file)?;
1094 let Some(code) = code else { return Ok(ci) };
1095 self.exec(&code, &format!("@{}", file.display()), args)?;
1096 Ok(ci)
1097 }
1098
1099 pub fn compile(&self, name: &str, lua_src: &str) -> Result<Vec<u8>> {
1101 self.compile_with(name, lua_src, true)
1102 }
1103
1104 pub fn compile_with(&self, name: &str, lua_src: &str, strip: bool) -> Result<Vec<u8>> {
1107 let f = self
1108 .lua
1109 .load(lua_src)
1110 .set_name(format!("={name}"))
1111 .into_function()
1112 .with_context(|| format!("compiling generated Lua for {name}"))?;
1113 Ok(f.dump(strip))
1114 }
1115
1116 pub fn fingerprint(&self) -> Result<Vec<u8>> {
1121 let bc = self.compile_with("fp", "return 0", true)?;
1122 Ok(bc.iter().take(31).copied().collect())
1124 }
1125
1126 pub fn lua_requires(&self, src: &str, file: &Path) -> Result<Vec<RequireSite>> {
1128 let f: Function = self.h.get("lua_requires")?;
1129 let t: Table = f.call((src, path_str(file)))?;
1130 read_requires(&t)
1131 }
1132
1133 pub fn resolve_module(&self, name: &str) -> Result<(Option<PathBuf>, Option<PathBuf>)> {
1137 let f: Function = self.h.get("resolve_module")?;
1138 let (found, lua): (Option<String>, Option<String>) = f.call(name)?;
1139 Ok((found.map(PathBuf::from), lua.map(PathBuf::from)))
1140 }
1141
1142 pub fn install_bundle(&self, b: &bundle::Bundle) -> Result<()> {
1144 if b.modules.iter().any(|m| m.kind == bundle::Kind::Bytecode) && !b.fingerprint.is_empty() {
1149 let mine = self.fingerprint()?;
1150 if mine != b.fingerprint {
1151 let built_by = if b.htl_version.is_empty() {
1152 "an htl that did not record its version".to_string()
1153 } else {
1154 format!("htl {}", b.htl_version)
1155 };
1156 bail!(
1157 "bundle bytecode was compiled for {} by {built_by}, but this host runs {} on htl {}; \
1158 rebuild the bundle here, or build it with --source",
1159 bundle::describe_fingerprint(&b.fingerprint),
1160 bundle::describe_fingerprint(&mine),
1161 env!("CARGO_PKG_VERSION")
1162 );
1163 }
1164 }
1165 let package: Table = self.lua.globals().get("package")?;
1168 let preload: Table = package.get("preload")?;
1169 let loaded: Table = package.get("loaded")?;
1170 let missing: Vec<&String> = b
1171 .host_modules
1172 .iter()
1173 .filter(|n| {
1174 matches!(preload.get::<Value>(n.as_str()), Ok(Value::Nil))
1175 && matches!(loaded.get::<Value>(n.as_str()), Ok(Value::Nil))
1176 })
1177 .collect();
1178 if !missing.is_empty() {
1179 bail!(
1180 "bundle expects host-provided module(s) {} (declared only by a .d.tl or [build] host at link \
1181 time): register them with preload / preload_value / htl_preload before running",
1182 missing
1183 .iter()
1184 .map(|m| format!("'{m}'"))
1185 .collect::<Vec<_>>()
1186 .join(", ")
1187 );
1188 }
1189 for m in &b.modules {
1196 if !matches!(preload.get::<Value>(m.name.as_str())?, Value::Nil) {
1197 continue;
1198 }
1199 let payload = m.payload.clone();
1200 let kind = m.kind;
1201 let name = m.name.clone();
1202 let loader =
1203 self.lua
1204 .create_function(move |lua, (modname, origin): (String, Value)| {
1205 let chunk = lua.load(payload.as_slice()).set_name(format!("={name}"));
1206 let f = match kind {
1207 bundle::Kind::Bytecode => {
1208 chunk.set_mode(ChunkMode::Binary).into_function()?
1209 }
1210 bundle::Kind::Source => {
1211 chunk.set_mode(ChunkMode::Text).into_function()?
1212 }
1213 };
1214 f.call::<Value>((modname, origin))
1215 })?;
1216 preload.set(m.name.as_str(), loader)?;
1217 }
1218 Ok(())
1219 }
1220
1221 pub fn run_bundle(&self, b: &bundle::Bundle, args: &[String]) -> Result<()> {
1223 let entry = b
1224 .module(&b.entry)
1225 .cloned()
1226 .ok_or_else(|| anyhow!("entry module '{}' not in bundle", b.entry))?;
1227 self.install_bundle(b)?;
1228 self.set_arg(&b.entry, args)?;
1229 let chunk = self
1230 .lua
1231 .load(entry.payload.as_slice())
1232 .set_name(format!("={}", b.entry));
1233 let main: Function = match entry.kind {
1234 bundle::Kind::Bytecode => chunk.set_mode(ChunkMode::Binary).into_function()?,
1235 bundle::Kind::Source => chunk.set_mode(ChunkMode::Text).into_function()?,
1236 };
1237 let va: Variadic<String> = args.iter().cloned().collect();
1238 main.call::<()>(va)?;
1239 Ok(())
1240 }
1241}
1242
1243fn path_str(p: &Path) -> String {
1244 p.to_string_lossy().into_owned()
1245}
1246
1247pub fn user_message(err: &anyhow::Error) -> String {
1256 fn from_mlua(e: &mlua::Error) -> String {
1257 match e {
1258 mlua::Error::CallbackError { cause, .. } => from_mlua(cause),
1259 mlua::Error::ExternalError(ext) => ext.to_string(),
1260 mlua::Error::WithContext { cause, .. } => from_mlua(cause),
1261 other => strip_traceback(&other.to_string()),
1262 }
1263 }
1264 if let Some(e) = err.downcast_ref::<mlua::Error>() {
1265 return from_mlua(e);
1266 }
1267 strip_traceback(&format!("{err:#}"))
1268}
1269
1270pub fn strip_traceback(text: &str) -> String {
1272 let cut = text.find("\nstack traceback:").unwrap_or(text.len());
1273 text[..cut].trim_end().to_string()
1274}
1275
1276pub fn write_if_changed(path: &Path, text: &str) -> std::io::Result<bool> {
1279 if let Ok(cur) = std::fs::read_to_string(path)
1280 && cur == text
1281 {
1282 return Ok(false);
1283 }
1284 if let Some(dir) = path.parent() {
1285 std::fs::create_dir_all(dir)?;
1286 }
1287 std::fs::write(path, text)?;
1288 Ok(true)
1289}
1290
1291pub fn parent_dir(file: &Path) -> PathBuf {
1293 let dir = file.parent().unwrap_or(Path::new("."));
1294 if dir.as_os_str().is_empty() {
1295 PathBuf::from(".")
1296 } else {
1297 dir.to_path_buf()
1298 }
1299}
1300
1301fn read_checkinfo(t: &Table) -> Result<CheckInfo> {
1302 let seq = |key: &str| -> Result<Vec<String>> {
1303 let inner: Table = t.get(key)?;
1304 Ok(inner
1305 .sequence_values::<String>()
1306 .collect::<mlua::Result<_>>()?)
1307 };
1308 let requires = match t.get::<Table>("requires") {
1309 Ok(list) => read_requires(&list)?,
1310 Err(_) => Vec::new(),
1311 };
1312 let errors = seq("errors")?;
1313 let lints = seq("lints")?;
1314 let error_fixes = read_fixes(t, "error_fixes", errors.len())?;
1315 let lint_fixes = read_fixes(t, "lint_fixes", lints.len())?;
1316 let dependency_errors = match t.get::<Table>("dependency_errors") {
1317 Ok(list) => read_dependency_errors(&list)?,
1318 Err(_) => Vec::new(),
1319 };
1320 Ok(CheckInfo {
1321 errors,
1322 warnings: seq("warnings")?,
1323 deps: seq("deps")?.into_iter().map(PathBuf::from).collect(),
1324 lints,
1325 requires,
1326 error_fixes,
1327 lint_fixes,
1328 dependency_errors,
1329 })
1330}
1331
1332fn read_dependency_errors(list: &Table) -> Result<Vec<DependencyError>> {
1333 let mut out = Vec::new();
1334 for e in list.sequence_values::<Table>() {
1335 let e = e?;
1336 out.push(DependencyError {
1337 file: PathBuf::from(e.get::<String>("file")?),
1338 required_by: PathBuf::from(e.get::<String>("required_by")?),
1339 text: e.get::<String>("text")?,
1340 });
1341 }
1342 Ok(out)
1343}
1344
1345fn read_fixes(t: &Table, key: &str, len: usize) -> Result<Vec<Option<Fix>>> {
1347 let mut out = vec![None; len];
1348 let Ok(list) = t.get::<Table>(key) else {
1349 return Ok(out);
1350 };
1351 for (i, slot) in out.iter_mut().enumerate() {
1352 let v: Value = list.get(i + 1)?;
1353 if let Value::Table(f) = v {
1354 let applicability = match f.get::<Option<String>>("applicability")?.as_deref() {
1355 Some("unsafe") => Applicability::Unsafe,
1356 Some("suggest") => Applicability::Suggest,
1357 _ => Applicability::Safe,
1358 };
1359 let mut edits = Vec::new();
1360 if let Ok(es) = f.get::<Table>("edits") {
1361 for e in es.sequence_values::<Table>() {
1362 let e = e?;
1363 edits.push(Edit {
1364 line: e.get("line")?,
1365 col: e.get("col")?,
1366 end_line: e.get("end_line")?,
1367 end_col: e.get("end_col")?,
1368 text: e.get::<Option<String>>("text")?.unwrap_or_default(),
1369 });
1370 }
1371 }
1372 *slot = Some(Fix {
1373 applicability,
1374 edits,
1375 });
1376 }
1377 }
1378 Ok(out)
1379}
1380
1381fn read_requires(list: &Table) -> Result<Vec<RequireSite>> {
1382 let mut requires = Vec::new();
1383 for r in list.sequence_values::<Table>() {
1384 let r = r?;
1385 requires.push(RequireSite {
1386 module: r.get::<String>("name")?,
1387 path: r.get::<Option<String>>("path")?.map(PathBuf::from),
1388 line: r.get::<Option<usize>>("y")?.unwrap_or(0),
1389 col: r.get::<Option<usize>>("x")?.unwrap_or(0),
1390 });
1391 }
1392 Ok(requires)
1393}
1394
1395pub fn is_tl_source(p: &Path) -> bool {
1397 let name = p.file_name().and_then(|s| s.to_str()).unwrap_or("");
1398 p.is_file() && name.ends_with(".tl") && !name.ends_with(".d.tl")
1399}
1400
1401pub fn is_declaration(p: &Path) -> bool {
1403 p.file_name()
1404 .and_then(|s| s.to_str())
1405 .is_some_and(|n| n.ends_with(".d.tl"))
1406}
1407
1408pub const SKIP_DIRS: &[&str] = &["target", "node_modules", ".mlua-pkgs", ".git"];
1411
1412pub fn is_skipped_dir(path: &Path, extra: &[PathBuf]) -> bool {
1416 if !path.is_dir() {
1417 return false;
1418 }
1419 let name = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
1420 if SKIP_DIRS.contains(&name) || (name.starts_with('.') && name.len() > 1) {
1421 return true;
1422 }
1423 extra.iter().any(|e| same_file(path, e))
1424}
1425
1426pub(crate) fn same_file(a: &Path, b: &Path) -> bool {
1429 match (std::fs::canonicalize(a), std::fs::canonicalize(b)) {
1430 (Ok(x), Ok(y)) => x == y,
1431 _ => a == b,
1432 }
1433}
1434
1435#[cfg(feature = "pkg")]
1449pub fn project_skip_dirs(root: &Path) -> Vec<PathBuf> {
1450 match pkg::Project::find(root) {
1451 Some(p) => {
1452 let mut out = vec![p.pkgs_dir];
1453 out.extend(p.vendored_copies);
1454 out
1455 }
1456 None => Vec::new(),
1457 }
1458}
1459
1460#[cfg(not(feature = "pkg"))]
1461pub fn project_skip_dirs(_root: &Path) -> Vec<PathBuf> {
1462 Vec::new()
1463}
1464
1465#[cfg(feature = "pkg")]
1475pub fn patched_dirs(root: &Path) -> Vec<PathBuf> {
1476 match pkg::Project::find(root) {
1477 Some(p) => p.patch_dirs(),
1478 None => Vec::new(),
1479 }
1480}
1481
1482#[cfg(not(feature = "pkg"))]
1483pub fn patched_dirs(_root: &Path) -> Vec<PathBuf> {
1484 Vec::new()
1485}
1486
1487pub fn collect_tl(paths: &[PathBuf]) -> Result<Vec<PathBuf>> {
1491 collect_tl_skipping(paths, &[])
1492}
1493
1494pub fn collect_tl_skipping(paths: &[PathBuf], skip: &[PathBuf]) -> Result<Vec<PathBuf>> {
1497 let mut out = Vec::new();
1498 for p in paths {
1499 if p.is_dir() {
1500 let mut extra = project_skip_dirs(p);
1501 extra.extend(skip.iter().cloned());
1502 let root = p.clone();
1503 let walker = walkdir::WalkDir::new(p)
1504 .sort_by_file_name()
1505 .into_iter()
1506 .filter_entry(move |e| e.path() == root || !is_skipped_dir(e.path(), &extra));
1507 for e in walker {
1508 let e = e?;
1509 if is_tl_source(e.path()) {
1510 out.push(e.path().to_path_buf());
1511 }
1512 }
1513 } else if p.is_file() {
1514 out.push(p.clone());
1515 } else {
1516 bail!("no such file or directory: {}", p.display());
1517 }
1518 }
1519 Ok(out)
1520}
1521
1522pub fn module_name(root: &Path, file: &Path) -> Result<String> {
1524 let rel = file.strip_prefix(root)?.with_extension("");
1525 let mut parts: Vec<String> = rel
1526 .components()
1527 .map(|c| c.as_os_str().to_string_lossy().into_owned())
1528 .collect();
1529 if parts.last().map(|s| s == "init").unwrap_or(false) {
1530 parts.pop();
1531 }
1532 if parts.is_empty() {
1533 bail!("cannot derive module name for {}", file.display());
1534 }
1535 Ok(parts.join("."))
1536}