#![cfg_attr(
feature = "std",
doc = "
//! That method is [`Htl::install_std`]."
)]
#![deny(missing_docs)]
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};
use std::sync::OnceLock;
pub mod build_target;
pub mod bundle;
pub mod cache;
#[cfg(feature = "dts")]
pub mod cexport;
pub mod config;
pub mod contract;
#[cfg(feature = "dts")]
pub mod dep_dts;
pub mod diagnostic;
#[cfg(feature = "dts")]
pub mod dts;
#[cfg(feature = "ffi")]
pub mod ffi;
pub mod fix;
pub mod link;
pub mod lint;
#[cfg(feature = "pkg")]
pub mod pkg;
#[cfg(all(feature = "pkg", feature = "dts"))]
pub mod project;
#[cfg(all(feature = "pkg", feature = "dts"))]
pub mod resolve;
#[cfg(feature = "std")]
pub mod batteries;
pub mod teal;
pub mod testing;
#[cfg(all(feature = "pkg", feature = "dts"))]
pub mod unused;
pub use build_target::BuildTarget;
pub use diagnostic::{Diagnostic, Severity};
pub use teal::Strict;
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 fn checker_identity() -> &'static str {
static ID: std::sync::OnceLock<String> = std::sync::OnceLock::new();
ID.get_or_init(|| {
let mut h = blake3::Hasher::new();
for src in [TL_SRC, LINT_SRC, FMT_SRC, PRELUDE] {
h.update(src.as_bytes());
h.update(b"\0");
}
h.finalize().to_hex().to_string()
})
}
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>,
pub requires: Vec<RequireSite>,
pub error_fixes: Vec<Option<Fix>>,
pub lint_fixes: Vec<Option<Fix>>,
pub dependency_errors: Vec<DependencyError>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DependencyError {
pub file: PathBuf,
pub required_by: PathBuf,
pub text: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Applicability {
Safe,
Unsafe,
Suggest,
}
impl Applicability {
pub fn as_str(self) -> &'static str {
match self {
Applicability::Safe => "safe",
Applicability::Unsafe => "unsafe",
Applicability::Suggest => "suggest",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct Edit {
pub line: usize,
pub col: usize,
pub end_line: usize,
pub end_col: usize,
pub text: String,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct Fix {
pub applicability: Applicability,
pub edits: Vec<Edit>,
}
#[derive(Debug, Clone)]
pub struct RequireSite {
pub module: String,
pub path: Option<PathBuf>,
pub line: usize,
pub col: usize,
}
#[derive(Debug, Clone)]
pub struct FunctionSpan {
pub name: String,
pub line: usize,
pub last: usize,
}
pub type CoverageSpans = (Vec<(usize, usize)>, Vec<FunctionSpan>);
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ModuleKind {
Source,
Declaration,
Lua,
}
impl ModuleKind {
fn of(s: &str) -> Self {
match s {
"source" => Self::Source,
"declaration" => Self::Declaration,
_ => Self::Lua,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Source => "source",
Self::Declaration => "declaration",
Self::Lua => "lua",
}
}
}
impl std::fmt::Display for ModuleKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModuleCandidate {
pub path: PathBuf,
pub kind: ModuleKind,
pub dir: PathBuf,
}
#[derive(Debug, Clone, Default)]
pub struct ContractResult {
pub errors: Vec<String>,
pub missing: Option<Vec<String>>,
pub missing_at: (usize, usize),
pub bad_require_fields: Vec<String>,
}
impl Htl {
pub fn apply_config(&self, root: &Path, cfg: &config::HtlConfig) -> Result<()> {
self.add_search_paths(&cfg.search_paths(root))
}
pub fn add_search_paths(&self, dirs: &[PathBuf]) -> Result<()> {
for p in dirs.iter().rev() {
self.add_path(p)?;
}
Ok(())
}
pub fn contract_check(
&self,
file: &Path,
modname: &str,
type_path: &str,
require_fields: &config::RequireFields,
) -> Result<ContractResult> {
let f: Function = self.h.get("contract_check")?;
let wanted = match require_fields.named() {
Some(names) => mlua::Value::Table(self.lua().create_sequence_from(names.to_vec())?),
None => mlua::Value::Boolean(require_fields.is_on()),
};
let t: Table = f.call((path_str(file), modname, type_path, wanted))?;
let errors: Table = t.get("errors")?;
let errors = errors
.sequence_values::<String>()
.collect::<mlua::Result<_>>()?;
let missing = match t.get::<Option<Table>>("missing")? {
Some(m) => Some(
m.sequence_values::<String>()
.collect::<mlua::Result<Vec<_>>>()?,
),
None => None,
};
let missing_at = (
t.get::<Option<usize>>("missing_y")?.unwrap_or(1),
t.get::<Option<usize>>("missing_x")?.unwrap_or(1),
);
let bad_require_fields = match t.get::<Option<Table>>("bad_require_fields")? {
Some(b) => b
.sequence_values::<String>()
.collect::<mlua::Result<Vec<_>>>()?,
None => Vec::new(),
};
Ok(ContractResult {
errors,
missing,
missing_at,
bad_require_fields,
})
}
}
pub fn contract_lints(
h: &Htl,
root: &Path,
cfg: &config::HtlConfig,
contracts: &[contract::Resolved],
file: &Path,
) -> Result<Vec<String>> {
let canon = |p: &Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
let file_abs = canon(file);
let mut out = Vec::new();
if !is_tl_source(&file_abs) {
return Ok(out);
}
let modname = file_abs
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_string();
for c in contracts {
let Some(dir) = c
.dirs(root)
.into_iter()
.map(|d| canon(&d))
.find(|d| file_abs.parent() == Some(d.as_path()))
else {
continue;
};
if !c.applies_to(&modname) {
continue;
}
h.add_path(&dir)?;
h.apply_config(root, cfg)?;
let r = h.contract_check(&file_abs, &modname, &c.type_path, &c.require_fields)?;
if !r.bad_require_fields.is_empty() {
out.push(format!(
"{}:{}:1: ---@required on field(s) {} does not declare: {} [htl contract]",
c.declared_in.display(),
c.declared_at,
c.type_path,
r.bad_require_fields.join(", ")
));
continue;
}
for e in &r.errors {
let msg = diagnostic::position(e)
.map_or(e.as_str(), |(_, _, _, msg)| msg)
.trim();
out.push(format!(
"{}:1:1: does not satisfy contract {} ({}): {msg} [htl contract]",
file.display(),
c.type_path,
c.dir
));
}
if let Some(missing) = &r.missing
&& !missing.is_empty()
{
out.push(format!(
"{}:{}:{}: returned table lacks declared field(s) of {}: {} [htl contract]",
file.display(),
r.missing_at.0,
r.missing_at.1,
c.type_path,
missing.join(", ")
));
}
}
Ok(out)
}
pub fn declaration_conflict_lints(
h: &Htl,
file: &Path,
info: &CheckInfo,
host_modules: &[String],
) -> Result<Vec<String>> {
let f: Function = h.h.get("declaration_sites")?;
let mut out = Vec::new();
let mut seen: Vec<&str> = Vec::new();
for site in &info.requires {
let Some(read) = site.path.as_ref() else {
continue;
};
if seen.contains(&site.module.as_str()) {
continue;
}
if !is_declaration(read) {
if host_modules.contains(&site.module) {
seen.push(&site.module);
out.push(format!(
"{}:{}:{}: {} is a host module of this crate and also {}: the check \
reads the file, the run loads the host — package.preload is consulted \
before any path searcher, so what is checked here is not what runs \
[htl host-module-shadowed]",
file.display(),
site.line,
site.col,
site.module,
read.display(),
));
}
continue;
}
let sites: Vec<String> = f
.call::<Table>(site.module.as_str())?
.sequence_values::<String>()
.collect::<mlua::Result<_>>()?;
let shadowed: Vec<&str> = sites
.iter()
.map(|s| s.as_str())
.filter(|s| !same_file(Path::new(s), read))
.collect();
if shadowed.is_empty() {
continue;
}
seen.push(&site.module);
out.push(format!(
"{}:{}:{}: {} is declared more than once on the search path: {} is read, {} {} not [htl duplicate-declaration]",
file.display(),
site.line,
site.col,
site.module,
read.display(),
shadowed.join(" and "),
if shadowed.len() == 1 { "is" } else { "are" },
));
}
Ok(out)
}
pub fn contract_enforcement_lints(
cfg_path: &Path,
contracts: &[contract::Resolved],
cargo_root: Option<&Path>,
) -> Vec<String> {
let mut out = Vec::new();
if contracts.is_empty() {
return out;
}
let Some(root) = cargo_root else { return out };
let mut sources = String::new();
for sub in ["src", "examples", "tests", "benches"] {
let dir = root.join(sub);
if !dir.is_dir() {
continue;
}
for e in walkdir::WalkDir::new(&dir).into_iter().flatten() {
let p = e.path();
if p.is_file()
&& p.extension().and_then(|s| s.to_str()) == Some("rs")
&& let Ok(t) = std::fs::read_to_string(p)
{
sources.push_str(&t);
sources.push('\n');
}
}
}
let by_config = sources.contains("contract_resolvers(");
for c in contracts {
if c.dirs(root_of(cfg_path)).is_empty() {
continue;
}
match &c.enforced_by {
Some(p) => {
let at = config::resolve_path(root_of(cfg_path), p);
if !at.exists() {
out.push(format!(
"{}:1:1: contract {} -> {} says it is enforced by {:?}, and there \
is no such file: name where the enforcement lives, or drop the \
key and let the scan look for \
htl::pkg::contract_resolvers(root, &config) \
[htl contract-unenforced]",
cfg_path.display(),
c.dir,
c.type_path,
p,
));
}
}
None if !by_config => out.push(format!(
"{}:{}:1: contract {} -> {} is declared but the host does not enforce it: \
build resolvers with htl::pkg::contract_resolvers(root, &config), or say \
where it is enforced with [[contract]] enforced_by \
[htl contract-unenforced]",
c.declared_in.display(),
c.declared_at,
c.dir,
c.type_path,
)),
None => {}
}
}
out
}
fn root_of(cfg_path: &Path) -> &Path {
cfg_path.parent().unwrap_or(Path::new("."))
}
pub fn require_cycles(infos: &[(PathBuf, CheckInfo)]) -> Vec<String> {
use std::collections::{HashMap, HashSet};
let canon = |p: &Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
let mut edges: HashMap<PathBuf, Vec<(PathBuf, &RequireSite)>> = HashMap::new();
let mut display: HashMap<PathBuf, PathBuf> = HashMap::new();
for (file, ci) in infos {
let from = canon(file);
display.insert(from.clone(), file.clone());
let list = edges.entry(from).or_default();
for r in &ci.requires {
if let Some(p) = &r.path {
list.push((canon(p), r));
}
}
}
let nodes: Vec<PathBuf> = {
let mut v: Vec<PathBuf> = edges.keys().cloned().collect();
v.sort();
v
};
let mut out = Vec::new();
let mut reported: HashSet<Vec<PathBuf>> = HashSet::new();
let mut state: HashMap<PathBuf, u8> = HashMap::new(); let mut stack: Vec<(PathBuf, Option<&RequireSite>)> = Vec::new();
fn dfs<'a>(
node: PathBuf,
edges: &HashMap<PathBuf, Vec<(PathBuf, &'a RequireSite)>>,
state: &mut HashMap<PathBuf, u8>,
stack: &mut Vec<(PathBuf, Option<&'a RequireSite>)>,
reported: &mut HashSet<Vec<PathBuf>>,
display: &HashMap<PathBuf, PathBuf>,
out: &mut Vec<String>,
) {
state.insert(node.clone(), 1);
if let Some(list) = edges.get(&node) {
for (to, site) in list {
match state.get(to).copied() {
Some(1) => {
let start = stack.iter().position(|(n, _)| n == to).unwrap_or(0);
let mut members: Vec<PathBuf> = stack[start..]
.iter()
.map(|(n, _)| n.clone())
.chain(std::iter::once(node.clone()))
.collect();
members.dedup();
let mut key = members.clone();
key.sort();
if reported.insert(key) {
let name = |p: &PathBuf| {
display
.get(p)
.unwrap_or(p)
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| p.display().to_string())
};
let chain: Vec<String> = members
.iter()
.map(name)
.chain(std::iter::once(name(to)))
.collect();
let first_file = display
.get(&members[0])
.cloned()
.unwrap_or_else(|| members[0].clone());
let anchor = stack.get(start + 1).and_then(|(_, s)| *s).unwrap_or(site);
out.push(format!(
"{}:{}:{}: require cycle: {} (Teal types the back edge as an opaque circular require; \
break it by moving shared types into a module both sides require) [htl require-cycle]",
first_file.display(),
anchor.line,
anchor.col,
chain.join(" -> ")
));
}
}
Some(2) => {}
_ => {
stack.push((to.clone(), Some(site)));
dfs(to.clone(), edges, state, stack, reported, display, out);
stack.pop();
}
}
}
}
state.insert(node, 2);
}
for n in nodes {
if !state.contains_key(&n) {
stack.push((n.clone(), None));
dfs(
n,
&edges,
&mut state,
&mut stack,
&mut reported,
&display,
&mut out,
);
stack.pop();
}
}
out.sort();
out
}
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 fn diagnostics(&self) -> Vec<Diagnostic> {
let mut out = self.warning_diagnostics();
out.extend(self.lint_diagnostics());
out.extend(self.error_diagnostics());
out
}
pub fn error_diagnostics(&self) -> Vec<Diagnostic> {
parsed(Severity::Error, &self.errors, &self.error_fixes)
}
pub fn warning_diagnostics(&self) -> Vec<Diagnostic> {
parsed(Severity::Warning, &self.warnings, &[])
}
pub fn lint_diagnostics(&self) -> Vec<Diagnostic> {
parsed(Severity::Lint, &self.lints, &self.lint_fixes)
}
}
fn parsed(severity: Severity, texts: &[String], fixes: &[Option<Fix>]) -> Vec<Diagnostic> {
texts
.iter()
.enumerate()
.map(|(i, text)| {
let mut d = Diagnostic::parse(severity, text);
d.fix = fixes.get(i).and_then(|f| f.clone());
d
})
.collect()
}
pub struct Htl {
lua: Lua,
h: Table,
split: bool,
}
pub(crate) struct CheckerHandle(pub(crate) Table);
const RUNTIME_REGISTRY_KEY: &str = "htl.runtime";
const BUNDLE_REGISTRY_KEY: &str = "htl.bundles";
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Replaced {
pub dropped: Vec<String>,
pub kept: Vec<String>,
pub added: Vec<String>,
}
const RUNTIME_PRELUDE: &str = r#"
local R = {}
function R.type_only_module(module_name, decl_path)
return setmetatable({}, {
__index = function(_, key)
error(string.format(
"module '%s' is declaration-only here (%s): '%s' has no implementation on this path. " ..
"It must be provided by the host program (e.g. a Rust #[host_module] via cargo run) " ..
"or by a .tl/.lua module with that name.",
module_name, decl_path, tostring(key)), 2)
end,
})
end
-- gen(name) -> kind, a, b (see resolve_for_require in the checker prelude)
function R.install_searcher(gen)
table.insert(package.searchers, 2, function(module_name)
local kind, a, b = gen(module_name)
if kind == "code" then
local chunk, lerr = load(a, "@" .. b, "t")
if not chunk then
error("htl: generated Lua failed to load: " .. tostring(lerr), 0)
end
return function(modname) return chunk(modname, b) end, b
elseif kind == "type_only" then
return function() return R.type_only_module(module_name, a) end, a
end
return a
end)
end
-- Put already-generated Lua in front of the searcher for one module name.
--
-- `package.preload` is searcher position 1 and R.install_searcher puts htl's at 2, so a
-- preloaded module is never asked of the searcher — which is the point: asking would check
-- and generate it again. Loaded the same way the searcher would have loaded it, so the
-- module sees the same chunk name and the same arguments.
function R.preload_generated(module_name, code, filename)
-- Never displace what is already there. The test library and anything a host preloads are
-- put in package.preload by whoever owns them, and Lua generated from a `.tl` of the same
-- name is not the same module: preloading over `htl.test` gives the file a stand-in whose
-- `run()` reports nothing, and every test silently stops counting.
if package.preload[module_name] ~= nil then return end
local chunk, lerr = load(code, "@" .. filename, "t")
if not chunk then
error("htl: cached Lua failed to load: " .. tostring(lerr), 0)
end
package.preload[module_name] = function(modname) return chunk(modname, filename) end
end
function R.add_path(dir)
local templates = dir .. "/?.lua;" .. dir .. "/?/init.lua;" .. dir .. "/?/?.lua"
if package.path == nil or package.path == "" then
package.path = templates
else
package.path = templates .. ";" .. package.path
end
end
function R.reset_path()
package.path = ""
end
-- Line coverage: which lines of which chunk ran. Lua's line hook is per thread, so
-- code that runs inside a coroutine the test creates is not seen.
local cov = nil
function R.coverage_start()
cov = {}
-- The line event is the hot path. One "S" lookup per function (cached by the
-- function object) instead of per line; a call/return-event stack was measured
-- slower on a call-heavy suite, since calls are almost as frequent as lines there.
local srcs = setmetatable({}, { __mode = "k" })
local getinfo = debug.getinfo
debug.sethook(function(_, line)
local fi = getinfo(2, "f")
local func = fi and fi.func
if func == nil then return end
local t = srcs[func]
if t == nil then
local si = getinfo(2, "S")
local src = si and si.source
t = false
if src then
t = cov[src]
if not t then
t = {}
cov[src] = t
end
end
srcs[func] = t
end
if t then t[line] = true end
end, "l")
end
function R.coverage_stop()
debug.sethook()
local out = {}
for src, lines in pairs(cov or {}) do
local list = {}
for l in pairs(lines) do list[#list + 1] = l end
table.sort(list)
out[#out + 1] = { source = src, lines = list }
end
cov = nil
return out
end
return R
"#;
impl Htl {
pub fn new() -> Result<Self> {
let lua = unsafe { Lua::unsafe_new() };
Self::from_lua(lua)
}
pub fn with_checker(checker: &Htl) -> Result<Self> {
let lua = unsafe { Lua::unsafe_new() };
Self::with_checker_lua(checker, lua)
}
pub fn with_checker_lua(checker: &Htl, lua: Lua) -> Result<Self> {
let r: Table = lua
.load(RUNTIME_PRELUDE)
.set_name("=htl-runtime")
.eval()
.context("loading htl runtime prelude")?;
lua.set_named_registry_value(RUNTIME_REGISTRY_KEY, r)?;
lua.set_app_data(CheckerHandle(checker.h.clone()));
let begin: Function = checker.h.get("begin_program")?;
begin.call::<()>(())?;
Ok(Self {
lua,
h: checker.h.clone(),
split: true,
})
}
fn runtime(&self) -> Result<Table> {
Ok(self
.lua
.named_registry_value::<Table>(RUNTIME_REGISTRY_KEY)?)
}
pub fn preload_generated(&self, name: &str, code: &str, file: &Path) -> Result<()> {
let f: Function = self.runtime()?.get("preload_generated")?;
f.call::<()>((name, code, path_str(file)))?;
Ok(())
}
pub fn coverage_start(&self) -> Result<()> {
let f: Function = self.runtime()?.get("coverage_start")?;
f.call::<()>(())?;
Ok(())
}
pub fn coverage_stop(&self) -> Result<Vec<(String, Vec<usize>)>> {
let f: Function = self.runtime()?.get("coverage_stop")?;
let t: Table = f.call(())?;
let mut out = Vec::new();
for e in t.sequence_values::<Table>() {
let e = e?;
let source: String = e.get("source")?;
let lines: Table = e.get("lines")?;
out.push((
source,
lines
.sequence_values::<usize>()
.collect::<mlua::Result<_>>()?,
));
}
Ok(out)
}
pub fn executable_ranges(&self, file: &Path) -> Result<Vec<(usize, usize)>> {
Ok(self.coverage_spans(file)?.0)
}
pub fn coverage_spans(&self, file: &Path) -> Result<CoverageSpans> {
let f: Function = self.h.get("executable_ranges")?;
let (ranges, funcs): (Option<Table>, Option<Table>) = f.call(path_str(file))?;
let Some(ranges) = ranges else {
return Ok((Vec::new(), Vec::new()));
};
let mut out = Vec::new();
for r in ranges.sequence_values::<Table>() {
let r = r?;
out.push((r.get::<usize>(1)?, r.get::<usize>(2)?));
}
let mut fns = Vec::new();
if let Some(funcs) = funcs {
for f in funcs.sequence_values::<Table>() {
let f = f?;
fns.push(FunctionSpan {
name: f.get("name")?,
line: f.get("y")?,
last: f.get("last")?,
});
}
}
Ok((out, fns))
}
pub fn search_path(&self) -> Result<String> {
let f: Function = self.h.get("get_path")?;
Ok(f.call(())?)
}
pub fn set_search_path(&self, path: &str) -> Result<()> {
let f: Function = self.h.get("set_path")?;
f.call::<()>(path)?;
Ok(())
}
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())?;
let this = Self {
lua,
h,
split: false,
};
this.select_lints(&lint::Selection::default())?;
Ok(this)
}
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 check_written(&self, file: &Path) -> Result<CheckInfo> {
let f: Function = self.h.get("check_written")?;
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<()> {
self.select_lints(&lint::Selection::parse(spec)?)
}
pub fn select_lints(&self, sel: &lint::Selection) -> Result<()> {
let split = |side| {
let (mut on, mut off) = (Vec::new(), Vec::new());
for (name, is_on) in sel.of_side(side) {
if is_on { &mut on } else { &mut off }.push(name.to_string());
}
(on, off)
};
let (lua_on, lua_off) = split(lint::Side::Lua);
let (tl_on, tl_off) = split(lint::Side::Tl);
let f: Function = self.h.get("set_lints")?;
f.call::<()>((lua_on, lua_off, tl_on, tl_off))?;
Ok(())
}
pub fn set_deps(&self, names: &[String]) -> Result<()> {
let f: Function = self.h.get("set_deps")?;
f.call::<()>(names.to_vec())?;
Ok(())
}
pub fn lint_rules(&self) -> Result<Vec<String>> {
Ok(lint::rule_names().into_iter().map(str::to_string).collect())
}
pub fn lua_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 reset_search_path(&self) -> Result<()> {
let f: Function = self.h.get("reset_path")?;
f.call::<()>(())?;
if self.split {
let f: Function = self.runtime()?.get("reset_path")?;
f.call::<()>(())?;
}
Ok(())
}
pub fn add_layout_paths(&self, file: &Path) -> Result<()> {
let dir = parent_dir(file);
let mut dirs = vec![dir.clone()];
if dir.file_name().is_some_and(|n| n == "tests")
&& let Some(root) = dir.parent()
{
dirs.push(root.to_path_buf());
let src = root.join("src");
if src.is_dir() {
dirs.push(src);
}
}
self.add_search_paths(&dirs)
}
pub fn add_path(&self, dir: &Path) -> Result<()> {
let f: Function = self.h.get("add_path")?;
f.call::<()>(path_str(dir))?;
if self.split {
let f: Function = self.runtime()?.get("add_path")?;
f.call::<()>(path_str(dir))?;
}
Ok(())
}
pub fn install_searcher(&self) -> Result<()> {
if self.split {
let gen_fn: Function = self.h.get("gen_for_require")?;
let bridge = self.lua.create_function(move |_, name: String| {
let (kind, a, b): (String, Option<String>, Option<String>) = gen_fn.call(name)?;
Ok((kind, a, b))
})?;
let f: Function = self.runtime()?.get("install_searcher")?;
f.call::<()>(bridge)?;
return Ok(());
}
let f: Function = self.h.get("install_searcher")?;
f.call::<()>(())?;
Ok(())
}
pub fn preload(&self, name: &str, lua_src: &str) -> Result<()> {
self.preload_at(name, &module_chunk_name(name), lua_src)
}
pub fn preload_at(&self, name: &str, chunk_name: &str, lua_src: &str) -> Result<()> {
let loader = self
.lua
.load(lua_src)
.set_name(chunk_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(module_chunk_name(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 strict_strings(&self) -> Result<()> {
self.lua
.load(
r#"
local mt = getmetatable("")
for _, k in ipairs { "__add", "__sub", "__mul", "__div", "__mod", "__pow", "__unm", "__idiv" } do
mt[k] = nil
end
"#,
)
.set_name("=strict_strings")
.exec()
.context("removing arithmetic metamethods from the string metatable")?;
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>> {
self.compile_with(name, lua_src, true)
}
pub fn compile_with(&self, name: &str, lua_src: &str, strip: bool) -> 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(strip))
}
pub fn fingerprint(&self) -> Result<Vec<u8>> {
let bc = self.compile_with("fp", "return 0", true)?;
Ok(bc.iter().take(31).copied().collect())
}
pub fn lua_requires(&self, src: &str, file: &Path) -> Result<Vec<RequireSite>> {
let f: Function = self.h.get("lua_requires")?;
let t: Table = f.call((src, path_str(file)))?;
read_requires(&t)
}
pub fn resolve_module(&self, name: &str) -> Result<(Option<PathBuf>, Option<PathBuf>)> {
let f: Function = self.h.get("resolve_module")?;
let (found, lua): (Option<String>, Option<String>) = f.call(name)?;
Ok((found.map(PathBuf::from), lua.map(PathBuf::from)))
}
pub fn module_candidates(&self, name: &str) -> Result<Vec<ModuleCandidate>> {
let f: Function = self.h.get("module_candidates")?;
let t: Table = f.call(name)?;
let mut out = Vec::new();
for c in t.sequence_values::<Table>() {
let c = c?;
out.push(ModuleCandidate {
path: PathBuf::from(c.get::<String>("path")?),
kind: ModuleKind::of(&c.get::<String>("kind")?),
dir: PathBuf::from(c.get::<String>("dir")?),
});
}
Ok(out)
}
pub fn search_path_dirs(&self) -> Result<Vec<PathBuf>> {
let f: Function = self.h.get("search_dirs")?;
let t: Table = f.call(())?;
Ok(t.sequence_values::<String>()
.collect::<mlua::Result<Vec<_>>>()?
.into_iter()
.map(PathBuf::from)
.collect())
}
fn bundle_record(&self) -> Result<Table> {
if let Value::Table(t) = self
.lua
.named_registry_value::<Value>(BUNDLE_REGISTRY_KEY)?
{
return Ok(t);
}
let t = self.lua.create_table()?;
self.lua
.set_named_registry_value(BUNDLE_REGISTRY_KEY, t.clone())?;
Ok(t)
}
fn check_installable(&self, b: &bundle::Bundle) -> Result<()> {
if b.modules.iter().any(|m| m.kind == bundle::Kind::Bytecode) && !b.fingerprint.is_empty() {
let mine = self.fingerprint()?;
if mine != b.fingerprint {
let built_by = if b.htl_version.is_empty() {
"an htl that did not record its version".to_string()
} else {
format!("htl {}", b.htl_version)
};
bail!(
"bundle bytecode was compiled for {} by {built_by}, but this host runs {} on htl {}; \
rebuild the bundle here, or build it with --source",
bundle::describe_fingerprint(&b.fingerprint),
bundle::describe_fingerprint(&mine),
env!("CARGO_PKG_VERSION")
);
}
}
let package: Table = self.lua.globals().get("package")?;
let preload: Table = package.get("preload")?;
let loaded: Table = package.get("loaded")?;
let missing: Vec<&String> = b
.host_modules
.iter()
.filter(|n| {
matches!(preload.get::<Value>(n.as_str()), Ok(Value::Nil))
&& matches!(loaded.get::<Value>(n.as_str()), Ok(Value::Nil))
})
.collect();
if !missing.is_empty() {
bail!(
"bundle expects host-provided module(s) {} (declared only by a .d.tl or [build] host at link \
time): register them with preload / preload_value / htl_preload before running",
missing
.iter()
.map(|m| format!("'{m}'"))
.collect::<Vec<_>>()
.join(", ")
);
}
Ok(())
}
pub fn install_bundle(&self, b: &bundle::Bundle) -> Result<()> {
self.check_installable(b)?;
let package: Table = self.lua.globals().get("package")?;
let preload: Table = package.get("preload")?;
let mut written: Vec<String> = Vec::new();
for m in &b.modules {
if !matches!(preload.get::<Value>(m.name.as_str())?, Value::Nil) {
continue;
}
let payload = m.payload.clone();
let kind = m.kind;
let name = m.name.clone();
let loader =
self.lua
.create_function(move |lua, (modname, origin): (String, Value)| {
let chunk = lua.load(payload.as_slice()).set_name(format!("={name}"));
let f = match kind {
bundle::Kind::Bytecode => {
chunk.set_mode(ChunkMode::Binary).into_function()?
}
bundle::Kind::Source => {
chunk.set_mode(ChunkMode::Text).into_function()?
}
};
f.call::<Value>((modname, origin))
})?;
preload.set(m.name.as_str(), loader)?;
written.push(m.name.clone());
}
let record = self.bundle_record()?;
let names: Table = match record.get::<Value>(b.entry.as_str())? {
Value::Table(t) => t,
_ => {
let t = self.lua.create_table()?;
record.set(b.entry.as_str(), t.clone())?;
t
}
};
let already: Vec<String> = names
.sequence_values::<String>()
.collect::<mlua::Result<Vec<_>>>()?;
for name in written {
if !already.contains(&name) {
names.push(name)?;
}
}
Ok(())
}
pub fn replace_bundle(&self, b: &bundle::Bundle, keep: &[&str]) -> Result<Replaced> {
self.check_installable(b)?;
let package: Table = self.lua.globals().get("package")?;
let preload: Table = package.get("preload")?;
let loaded: Table = package.get("loaded")?;
let record = self.bundle_record()?;
let previous: Vec<String> = match record.get::<Value>(b.entry.as_str())? {
Value::Table(t) => t
.sequence_values::<String>()
.collect::<mlua::Result<Vec<_>>>()?,
_ => Vec::new(),
};
let (mut dropped, mut kept) = (Vec::new(), Vec::new());
for name in &previous {
preload.set(name.as_str(), Value::Nil)?;
if keep.contains(&name.as_str()) {
kept.push(name.clone());
} else {
loaded.set(name.as_str(), Value::Nil)?;
dropped.push(name.clone());
}
}
record.set(b.entry.as_str(), Value::Nil)?;
self.install_bundle(b)?;
let added: Vec<String> = match record.get::<Value>(b.entry.as_str())? {
Value::Table(t) => t
.sequence_values::<String>()
.collect::<mlua::Result<Vec<_>>>()?,
_ => Vec::new(),
};
Ok(Replaced {
dropped,
kept,
added,
})
}
pub fn run_bundle(&self, b: &bundle::Bundle, args: &[String]) -> Result<()> {
let entry = b
.module(&b.entry)
.cloned()
.ok_or_else(|| anyhow!("entry module '{}' not in bundle", b.entry))?;
self.install_bundle(b)?;
self.set_arg(&b.entry, args)?;
let chunk = self
.lua
.load(entry.payload.as_slice())
.set_name(format!("={}", b.entry));
let main: Function = match entry.kind {
bundle::Kind::Bytecode => chunk.set_mode(ChunkMode::Binary).into_function()?,
bundle::Kind::Source => chunk.set_mode(ChunkMode::Text).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()
}
fn module_chunk_name(name: &str) -> String {
format!("@{}.tl", name.replace('.', "/"))
}
pub fn user_message(err: &anyhow::Error) -> String {
if let Some(e) = err.downcast_ref::<mlua::Error>() {
return user_message_lua(e);
}
strip_traceback(&format!("{err:#}"))
}
pub fn user_message_lua(e: &mlua::Error) -> String {
match e {
mlua::Error::CallbackError { cause, .. } => user_message_lua(cause),
mlua::Error::ExternalError(ext) => ext.to_string(),
mlua::Error::WithContext { cause, .. } => user_message_lua(cause),
other => strip_traceback(&other.to_string()),
}
}
pub fn developer_message(err: &anyhow::Error) -> String {
let head = user_message(err);
let full = match err.downcast_ref::<mlua::Error>() {
Some(e) => e.to_string(),
None => format!("{err:#}"),
};
match traceback_block(&full) {
Some(tb) => format!("{head}\n{tb}"),
None => head,
}
}
fn traceback_block(text: &str) -> Option<&str> {
let at = text.find("\nstack traceback:")?;
Some(text[at + 1..].trim_end())
}
pub fn strip_traceback(text: &str) -> String {
let cut = text.find("\nstack traceback:").unwrap_or(text.len());
text[..cut].trim_end().to_string()
}
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)
}
fn bundled_declarations() -> Vec<(String, String)> {
let mut out = testing::declarations();
#[cfg(feature = "std")]
out.extend(batteries::declarations());
out.sort();
out
}
fn declarations_key(decls: &[(String, String)]) -> String {
let mut h = blake3::Hasher::new();
for (path, source) in decls {
for part in [path.as_str(), source.as_str()] {
h.update(&(part.len() as u64).to_le_bytes());
h.update(part.as_bytes());
}
}
h.finalize().to_hex()[..16].to_string()
}
pub fn lib_dir() -> PathBuf {
static DIR: OnceLock<PathBuf> = OnceLock::new();
DIR.get_or_init(|| {
let key = declarations_key(&bundled_declarations());
std::env::temp_dir().join(format!("htl-lib-{}-{key}", env!("CARGO_PKG_VERSION")))
})
.clone()
}
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<_>>()?)
};
let requires = match t.get::<Table>("requires") {
Ok(list) => read_requires(&list)?,
Err(_) => Vec::new(),
};
let errors = seq("errors")?;
let lints = seq("lints")?;
let error_fixes = read_fixes(t, "error_fixes", errors.len())?;
let lint_fixes = read_fixes(t, "lint_fixes", lints.len())?;
let dependency_errors = match t.get::<Table>("dependency_errors") {
Ok(list) => read_dependency_errors(&list)?,
Err(_) => Vec::new(),
};
Ok(CheckInfo {
errors,
warnings: seq("warnings")?,
deps: seq("deps")?.into_iter().map(PathBuf::from).collect(),
lints,
requires,
error_fixes,
lint_fixes,
dependency_errors,
})
}
fn read_dependency_errors(list: &Table) -> Result<Vec<DependencyError>> {
let mut out = Vec::new();
for e in list.sequence_values::<Table>() {
let e = e?;
out.push(DependencyError {
file: PathBuf::from(e.get::<String>("file")?),
required_by: PathBuf::from(e.get::<String>("required_by")?),
text: e.get::<String>("text")?,
});
}
Ok(out)
}
fn read_fixes(t: &Table, key: &str, len: usize) -> Result<Vec<Option<Fix>>> {
let mut out = vec![None; len];
let Ok(list) = t.get::<Table>(key) else {
return Ok(out);
};
for (i, slot) in out.iter_mut().enumerate() {
let v: Value = list.get(i + 1)?;
if let Value::Table(f) = v {
let applicability = match f.get::<Option<String>>("applicability")?.as_deref() {
Some("unsafe") => Applicability::Unsafe,
Some("suggest") => Applicability::Suggest,
_ => Applicability::Safe,
};
let mut edits = Vec::new();
if let Ok(es) = f.get::<Table>("edits") {
for e in es.sequence_values::<Table>() {
let e = e?;
edits.push(Edit {
line: e.get("line")?,
col: e.get("col")?,
end_line: e.get("end_line")?,
end_col: e.get("end_col")?,
text: e.get::<Option<String>>("text")?.unwrap_or_default(),
});
}
}
*slot = Some(Fix {
applicability,
edits,
});
}
}
Ok(out)
}
fn read_requires(list: &Table) -> Result<Vec<RequireSite>> {
let mut requires = Vec::new();
for r in list.sequence_values::<Table>() {
let r = r?;
requires.push(RequireSite {
module: r.get::<String>("name")?,
path: r.get::<Option<String>>("path")?.map(PathBuf::from),
line: r.get::<Option<usize>>("y")?.unwrap_or(0),
col: r.get::<Option<usize>>("x")?.unwrap_or(0),
});
}
Ok(requires)
}
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 const DEP_TYPES_NOTE: &str = ".htl-dts";
pub fn materialised_types_dirs(types: &Path) -> Vec<PathBuf> {
let Ok(entries) = std::fs::read_dir(types) else {
return Vec::new();
};
let mut out: Vec<PathBuf> = entries
.filter_map(Result::ok)
.map(|e| e.path())
.filter(|p| p.is_dir() && p.join(DEP_TYPES_NOTE).is_file())
.collect();
out.sort();
out
}
pub fn is_declaration(p: &Path) -> bool {
p.file_name()
.and_then(|s| s.to_str())
.is_some_and(|n| n.ends_with(".d.tl"))
}
pub const SKIP_DIRS: &[&str] = &["target", "node_modules", ".mlua-pkgs", ".git"];
pub fn is_skipped_dir(path: &Path, extra: &[PathBuf]) -> bool {
if !path.is_dir() {
return false;
}
let name = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
if SKIP_DIRS.contains(&name) || (name.starts_with('.') && name.len() > 1) {
return true;
}
extra.iter().any(|e| same_file(path, e))
}
pub(crate) fn same_file(a: &Path, b: &Path) -> bool {
match (std::fs::canonicalize(a), std::fs::canonicalize(b)) {
(Ok(x), Ok(y)) => x == y,
_ => a == b,
}
}
#[cfg(feature = "pkg")]
pub fn project_skip_dirs(root: &Path) -> Vec<PathBuf> {
match pkg::Project::find(root) {
Some(p) => {
let mut out = vec![p.pkgs_dir];
out.extend(p.vendored_copies);
out
}
None => Vec::new(),
}
}
#[cfg(not(feature = "pkg"))]
pub fn project_skip_dirs(_root: &Path) -> Vec<PathBuf> {
Vec::new()
}
#[cfg(feature = "pkg")]
pub fn patched_dirs(root: &Path) -> Vec<PathBuf> {
match pkg::Project::find(root) {
Some(p) => p.patch_dirs(),
None => Vec::new(),
}
}
#[cfg(not(feature = "pkg"))]
pub fn patched_dirs(_root: &Path) -> Vec<PathBuf> {
Vec::new()
}
#[cfg(feature = "pkg")]
pub fn dependency_dirs(root: &Path) -> Vec<PathBuf> {
match pkg::Project::find(root) {
Some(p) => {
let mut out = p.patch_search_dirs();
out.push(p.entries);
out.extend(p.target_dirs);
out
}
None => Vec::new(),
}
}
#[cfg(not(feature = "pkg"))]
pub fn dependency_dirs(_root: &Path) -> Vec<PathBuf> {
Vec::new()
}
pub fn collect_tl(paths: &[PathBuf]) -> Result<Vec<PathBuf>> {
collect_tl_skipping(paths, &[])
}
pub fn collect_tl_skipping(paths: &[PathBuf], skip: &[PathBuf]) -> Result<Vec<PathBuf>> {
let mut out = Vec::new();
for p in paths {
if p.is_dir() {
let mut extra = project_skip_dirs(p);
extra.extend(skip.iter().cloned());
let root = p.clone();
let walker = walkdir::WalkDir::new(p)
.sort_by_file_name()
.into_iter()
.filter_entry(move |e| e.path() == root || !is_skipped_dir(e.path(), &extra));
for e in walker {
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("."))
}
#[cfg(test)]
mod tests {
use super::*;
fn decl(path: &str, source: &str) -> (String, String) {
(path.to_string(), source.to_string())
}
#[test]
fn a_different_set_of_declarations_is_a_different_key() {
let base = vec![decl("htl/test.d.tl", "local record t end\nreturn t\n")];
let mut more = base.clone();
more.push(decl(
"std/json.d.tl",
"local record json end\nreturn json\n",
));
assert_ne!(declarations_key(&base), declarations_key(&more));
let mut edited = base.clone();
edited[0].1.push('\n');
assert_ne!(declarations_key(&base), declarations_key(&edited));
}
#[test]
fn the_same_set_is_the_same_key() {
let decls = vec![
decl("htl/test.d.tl", "local record t end\nreturn t\n"),
decl("std/json.d.tl", "local record json end\nreturn json\n"),
];
assert_eq!(declarations_key(&decls), declarations_key(&decls.clone()));
}
#[test]
fn the_parts_cannot_run_together() {
assert_ne!(
declarations_key(&[decl("ab", "c")]),
declarations_key(&[decl("a", "bc")])
);
}
#[test]
fn the_list_holds_what_this_build_writes() {
let decls = bundled_declarations();
assert!(decls.iter().any(|(p, _)| p == "htl/test.d.tl"), "{decls:?}");
assert_eq!(
decls.iter().any(|(p, _)| p.starts_with("std/")),
cfg!(feature = "std")
);
}
#[test]
fn the_directory_carries_the_version_and_the_key() {
let dir = lib_dir();
let name = dir.file_name().unwrap().to_string_lossy().into_owned();
let prefix = format!("htl-lib-{}-", env!("CARGO_PKG_VERSION"));
assert!(name.starts_with(&prefix), "{name}");
assert_eq!(
name[prefix.len()..],
declarations_key(&bundled_declarations())
);
assert_eq!(dir, lib_dir());
}
}