1use crate::PRELUDE_REGISTRY_KEY;
21use mlua::{Function, Lua, Table, Value};
22use mlua_pkg::Resolver;
23use mlua_pkg::sandbox::{FsSandbox, InitError, ReadError, SandboxedFs, SymlinkAwareSandbox};
24use std::path::{Path, PathBuf};
25use std::sync::atomic::{AtomicBool, Ordering};
26
27pub use mlua_pkg;
28
29pub struct TealResolver {
32 sandbox: Box<dyn SandboxedFs>,
33 root: Option<PathBuf>,
34 path_added: AtomicBool,
35 module_separator: char,
36 expect_type: Option<String>,
38 require_fields: bool,
40 checker_paths: Vec<PathBuf>,
42 exclude: Vec<String>,
44 only_module: Option<String>,
46}
47
48impl TealResolver {
49 pub fn new(root: impl Into<PathBuf>) -> Result<Self, InitError> {
51 let root = root.into();
52 Ok(Self {
53 sandbox: Box::new(FsSandbox::new(&root)?),
54 root: Some(root),
55 path_added: AtomicBool::new(false),
56 module_separator: '.',
57 expect_type: None,
58 require_fields: false,
59 checker_paths: Vec::new(),
60 exclude: Vec::new(),
61 only_module: None,
62 })
63 }
64
65 pub fn new_symlink_aware(root: impl Into<PathBuf>) -> Result<Self, InitError> {
67 let root = root.into();
68 Ok(Self {
69 sandbox: Box::new(SymlinkAwareSandbox::new(&root)?),
70 root: Some(root),
71 path_added: AtomicBool::new(false),
72 module_separator: '.',
73 expect_type: None,
74 require_fields: false,
75 checker_paths: Vec::new(),
76 exclude: Vec::new(),
77 only_module: None,
78 })
79 }
80
81 pub fn with_sandbox(sandbox: impl SandboxedFs + 'static, root: Option<PathBuf>) -> Self {
84 Self {
85 sandbox: Box::new(sandbox),
86 root,
87 path_added: AtomicBool::new(false),
88 module_separator: '.',
89 expect_type: None,
90 require_fields: false,
91 checker_paths: Vec::new(),
92 exclude: Vec::new(),
93 only_module: None,
94 }
95 }
96
97 pub fn with_module_separator(mut self, sep: char) -> Self {
98 self.module_separator = sep;
99 self
100 }
101
102 pub fn expect_type(mut self, type_path: impl Into<String>) -> Self {
113 self.expect_type = Some(type_path.into());
114 self
115 }
116
117 pub fn require_fields(mut self) -> Self {
122 self.require_fields = true;
123 self
124 }
125
126 pub fn with_checker_path(mut self, dir: impl Into<PathBuf>) -> Self {
130 self.checker_paths.push(dir.into());
131 self
132 }
133
134 pub fn exclude_modules(mut self, names: impl IntoIterator<Item = impl Into<String>>) -> Self {
138 self.exclude.extend(names.into_iter().map(Into::into));
139 self
140 }
141
142 pub fn only_module(mut self, name: impl Into<String>) -> Self {
145 self.only_module = Some(name.into());
146 self
147 }
148
149 fn held(&self, name: &str) -> bool {
151 if self.expect_type.is_none() || self.exclude.iter().any(|e| e == name) {
152 return false;
153 }
154 self.only_module.as_deref().is_none_or(|m| m == name)
155 }
156
157 pub fn for_contract(root: &Path, c: &crate::config::Contract) -> Result<Vec<Self>, InitError> {
163 c.dirs(root).into_iter().map(|d| Self::for_contract_dir(root, &d, c)).collect()
164 }
165
166 pub fn for_contract_dir(root: &Path, dir: &Path, c: &crate::config::Contract) -> Result<Self, InitError> {
168 let mut r = Self::new_symlink_aware(dir)?
169 .expect_type(c.type_path.clone())
170 .exclude_modules(c.exclude.iter().cloned())
171 .with_checker_path(root)
172 .with_checker_path(root.join("src"));
173 if let Some(m) = &c.module {
174 r = r.only_module(m.clone());
175 }
176 if c.require_fields {
177 r = r.require_fields();
178 }
179 Ok(r)
180 }
181
182 fn missing_fields(&self, h: &Table, value: &Value) -> mlua::Result<Vec<String>> {
184 let (Some(tp), true) = (&self.expect_type, self.require_fields) else { return Ok(Vec::new()) };
185 let f: Function = h.get("record_fields")?;
186 let names: Option<Vec<String>> = f
187 .call::<Option<Table>>(tp.as_str())?
188 .map(|t| t.sequence_values::<String>().collect::<mlua::Result<_>>())
189 .transpose()?;
190 let Some(names) = names else {
191 return Err(mlua::Error::external(format!(
192 "TealResolver::require_fields: record type {tp:?} not found by the checker"
193 )));
194 };
195 let Value::Table(t) = value else {
196 return Ok(names); };
198 let mut missing = Vec::new();
199 for n in names {
200 if matches!(t.get::<Value>(n.as_str())?, Value::Nil) {
201 missing.push(n);
202 }
203 }
204 Ok(missing)
205 }
206
207 fn expectation_errors(&self, h: &Table, name: &str) -> mlua::Result<Option<Vec<String>>> {
209 let Some(tp) = &self.expect_type else { return Ok(None) };
210 let (module, _) = tp.split_once('.').ok_or_else(|| {
211 mlua::Error::external(format!(
212 "TealResolver::expect_type: expected \"<module>.<Type>\", got {tp:?}"
213 ))
214 })?;
215 if name == module {
217 return Ok(None);
218 }
219 let stub = format!(
220 "local {module} = require(\"{module}\")\nlocal m: {tp} = require(\"{name}\")\nreturn m\n"
221 );
222 let check: Function = h.get("check_stub")?;
225 let errors: Table = check.call((stub.as_str(), format!("<expect {tp} for module '{name}'>")))?;
226 let msgs: Vec<String> = errors.sequence_values::<String>().collect::<mlua::Result<_>>()?;
227 Ok(if msgs.is_empty() { None } else { Some(msgs) })
228 }
229
230 fn prelude(lua: &Lua) -> mlua::Result<Table> {
231 lua.named_registry_value::<Table>(PRELUDE_REGISTRY_KEY)
232 .map_err(|_| mlua::Error::external(
233 "htl::pkg::TealResolver: this Lua has no htl prelude (create it with Htl::new / Htl::from_lua)",
234 ))
235 }
236
237 fn ensure_checker_path(&self, lua: &Lua, h: &Table) -> mlua::Result<()> {
240 if self.path_added.swap(true, Ordering::Relaxed) {
241 return Ok(());
242 }
243 let f: Function = h.get("add_path")?;
244 if let Some(root) = &self.root {
245 f.call::<()>(root.to_string_lossy().as_ref())?;
246 }
247 for p in &self.checker_paths {
248 if p.is_dir() {
249 f.call::<()>(p.to_string_lossy().as_ref())?;
250 }
251 }
252 let _ = lua;
253 Ok(())
254 }
255
256 fn has_lua_sibling(&self, relative: &str) -> bool {
257 for cand in [format!("{relative}.lua"), format!("{relative}/init.lua")] {
258 if let Ok(Some(_)) = self.sandbox.read(Path::new(&cand)) {
259 return true;
260 }
261 }
262 false
263 }
264
265 fn load_teal(&self, lua: &Lua, h: &Table, src: &str, resolved: &Path, name: &str) -> mlua::Result<Value> {
266 let gen_fn: Function = h.get("gen_string")?;
267 let (code, info): (Option<String>, Table) = gen_fn.call((src, resolved.to_string_lossy().as_ref()))?;
268 let Some(code) = code else {
269 let errors: Table = info.get("errors")?;
270 let msgs: Vec<String> = errors.sequence_values::<String>().collect::<mlua::Result<_>>()?;
271 return Err(mlua::Error::external(TealResolveError::TypeCheck {
272 module: name.to_string(),
273 errors: msgs,
274 }));
275 };
276 if self.held(name)
277 && let Some(errs) = self.expectation_errors(h, name)?
278 {
279 return Err(mlua::Error::external(TealResolveError::Expectation {
280 module: name.to_string(),
281 expected: self.expect_type.clone().unwrap_or_default(),
282 errors: errs,
283 }));
284 }
285 let chunk = lua
286 .load(code)
287 .set_name(format!("@{}", resolved.display()))
288 .into_function()?;
289 chunk.call::<Value>((name, resolved.to_string_lossy().as_ref()))
290 }
291}
292
293#[derive(Debug, Clone)]
301pub struct Project {
302 pub root: PathBuf,
303 pub manifest: PathBuf,
304 pub lockfile: PathBuf,
305 pub pkgs_dir: PathBuf,
306 pub vendored: PathBuf,
307 pub target_dirs: Vec<PathBuf>,
311}
312
313pub const MANIFEST_NAME: &str = "mlua-pkg.toml";
314pub const LOCKFILE_NAME: &str = "mlua-pkg.lock";
315
316impl Project {
317 pub fn find(start: &Path) -> Option<Self> {
319 let mut dir = if start.is_dir() { start.to_path_buf() } else { crate::parent_dir(start) };
320 if let Ok(abs) = std::fs::canonicalize(&dir) {
321 dir = abs;
322 }
323 loop {
324 let manifest = dir.join(MANIFEST_NAME);
325 if manifest.is_file() {
326 return Some(Self::at(&dir));
327 }
328 if !dir.pop() {
329 return None;
330 }
331 }
332 }
333
334 pub fn at(root: &Path) -> Self {
336 let pkgs_dir = match std::env::var("MLUA_PKG_DIR") {
337 Ok(p) if !p.is_empty() => PathBuf::from(p),
338 _ if root.join("target").is_dir() => root.join("target").join("mlua-pkgs"),
339 _ => root.join(".mlua-pkgs"),
340 };
341 let manifest = root.join(MANIFEST_NAME);
342 let mut target_dirs: Vec<PathBuf> = Vec::new();
345 if let Ok(m) = mlua_pkg::manifest::Manifest::from_path(&manifest) {
346 for dep in m.deps.values() {
347 if let Some(td) = &dep.target_dir {
348 let abs = root.join(td);
349 let parent = abs.parent().map(Path::to_path_buf).unwrap_or_else(|| root.to_path_buf());
350 if !target_dirs.contains(&parent) {
351 target_dirs.push(parent);
352 }
353 }
354 }
355 }
356 Self {
357 root: root.to_path_buf(),
358 manifest,
359 lockfile: root.join(LOCKFILE_NAME),
360 vendored: pkgs_dir.join("vendored"),
361 pkgs_dir,
362 target_dirs,
363 }
364 }
365
366 pub fn installed(&self) -> bool {
368 self.lockfile.is_file()
369 }
370
371 pub fn teal_resolver(&self) -> Result<TealResolver, InitError> {
374 let _ = std::fs::create_dir_all(&self.vendored);
375 TealResolver::new_symlink_aware(&self.vendored)
376 }
377
378 pub fn vendored_resolver(&self) -> anyhow::Result<mlua_pkg::resolvers::VendoredResolver> {
380 if self.installed() {
381 Ok(mlua_pkg::resolvers::VendoredResolver::from_lockfile(&self.lockfile, &self.vendored)?)
382 } else {
383 let _ = std::fs::create_dir_all(&self.vendored);
384 Ok(mlua_pkg::resolvers::VendoredResolver::new(&self.vendored)?)
385 }
386 }
387
388 pub fn registry(&self) -> anyhow::Result<mlua_pkg::Registry> {
391 let mut reg = mlua_pkg::Registry::new();
392 reg.add(self.teal_resolver()?);
393 reg.add(self.vendored_resolver()?);
394 for d in &self.target_dirs {
395 if d.is_dir() {
396 reg.add(TealResolver::new(d)?);
397 reg.add(mlua_pkg::resolvers::FsResolver::new(d)?);
398 }
399 }
400 Ok(reg)
401 }
402}
403
404pub fn contract_resolvers(root: &Path, cfg: &crate::config::HtlConfig) -> Result<Vec<TealResolver>, InitError> {
409 let mut out = Vec::new();
410 for c in &cfg.contract {
411 for mut r in TealResolver::for_contract(root, c)? {
412 for p in cfg.search_paths(root) {
413 r = r.with_checker_path(p);
414 }
415 out.push(r);
416 }
417 }
418 Ok(out)
419}
420
421impl crate::Htl {
422 pub fn apply_project(&self, p: &Project) -> anyhow::Result<()> {
425 let _ = std::fs::create_dir_all(&p.vendored);
426 self.add_path(&p.vendored)?;
427 for d in &p.target_dirs {
428 self.add_path(d)?;
429 }
430 let src = p.root.join("src");
433 if src.is_dir() {
434 self.add_path(&src)?;
435 }
436 Ok(())
437 }
438}
439
440#[derive(Debug)]
442pub enum TealResolveError {
443 TypeCheck { module: String, errors: Vec<String> },
444 Expectation { module: String, expected: String, errors: Vec<String> },
447 MissingFields { module: String, expected: String, fields: Vec<String> },
449 Read { module: String, source: ReadError },
450}
451
452impl std::fmt::Display for TealResolveError {
453 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
454 match self {
455 Self::TypeCheck { module, errors } => {
456 write!(f, "Teal type check failed for module '{module}':")?;
457 for e in errors {
458 write!(f, "\n {e}")?;
459 }
460 Ok(())
461 }
462 Self::Expectation { module, expected, errors } => {
463 write!(f, "module '{module}' does not satisfy {expected}:")?;
464 for e in errors {
465 write!(f, "\n {e}")?;
466 }
467 write!(
468 f,
469 "\n hint: annotate the returned table in the module (`local m: {expected} = {{ ... }} return m`) \
470 to get field-level errors with line numbers"
471 )
472 }
473 Self::MissingFields { module, expected, fields } => write!(
474 f,
475 "module '{module}' is missing required field(s) of {expected}: {} (every field of that record must be non-nil)",
476 fields.join(", ")
477 ),
478 Self::Read { module, source } => write!(f, "reading module '{module}': {source}"),
479 }
480 }
481}
482
483impl std::error::Error for TealResolveError {}
484
485fn preloaded(lua: &Lua, name: &str) -> mlua::Result<bool> {
487 let package: Table = lua.globals().get("package")?;
488 let preload: Table = package.get("preload")?;
489 Ok(!matches!(preload.get::<Value>(name)?, Value::Nil))
490}
491
492impl Resolver for TealResolver {
493 fn resolve(&self, lua: &Lua, name: &str) -> Option<mlua::Result<Value>> {
494 let relative = name.replace(self.module_separator, "/");
495 let last = relative.rsplit('/').next().unwrap_or(&relative).to_string();
497 let candidates = [
498 (format!("{relative}.tl"), false),
499 (format!("{relative}/init.tl"), false),
500 (format!("{relative}/{last}.tl"), false),
501 (format!("{relative}.d.tl"), true),
502 ];
503 let h = match Self::prelude(lua) {
504 Ok(h) => h,
505 Err(e) => return Some(Err(e)),
506 };
507 if let Err(e) = self.ensure_checker_path(lua, &h) {
508 return Some(Err(e));
509 }
510 for (candidate, type_only) in &candidates {
511 match self.sandbox.read(Path::new(candidate)) {
512 Ok(Some(file)) => {
513 if *type_only {
514 if self.has_lua_sibling(&relative) {
518 return None;
519 }
520 match preloaded(lua, name) {
524 Ok(true) => return None,
525 Ok(false) => {}
526 Err(e) => return Some(Err(e)),
527 }
528 return Some(
531 h.get::<Function>("type_only_module")
532 .and_then(|f| f.call::<Value>((name, file.resolved_path.to_string_lossy().as_ref()))),
533 );
534 }
535 let loaded = match self.load_teal(lua, &h, &file.content, &file.resolved_path, name) {
536 Ok(v) => v,
537 Err(e) => return Some(Err(e)),
538 };
539 if !self.held(name) {
540 return Some(Ok(loaded));
541 }
542 match self.missing_fields(&h, &loaded) {
543 Ok(m) if m.is_empty() => return Some(Ok(loaded)),
544 Ok(missing) => {
545 return Some(Err(mlua::Error::external(TealResolveError::MissingFields {
546 module: name.to_string(),
547 expected: self.expect_type.clone().unwrap_or_default(),
548 fields: missing,
549 })));
550 }
551 Err(e) => return Some(Err(e)),
552 }
553 }
554 Ok(None) => continue,
555 Err(source) => {
556 return Some(Err(mlua::Error::external(TealResolveError::Read {
557 module: name.to_string(),
558 source,
559 })));
560 }
561 }
562 }
563 None
564 }
565}