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 if let Ok(t) = lua.named_registry_value::<Table>(PRELUDE_REGISTRY_KEY) {
232 return Ok(t);
233 }
234 if let Some(c) = lua.app_data_ref::<crate::CheckerHandle>() {
236 return Ok(c.0.clone());
237 }
238 Err(mlua::Error::external(
239 "htl::pkg::TealResolver: this Lua has no htl prelude (create it with Htl::new / Htl::from_lua)",
240 ))
241 }
242
243 fn ensure_checker_path(&self, lua: &Lua, h: &Table) -> mlua::Result<()> {
246 if self.path_added.swap(true, Ordering::Relaxed) {
247 return Ok(());
248 }
249 let f: Function = h.get("add_path")?;
250 if let Some(root) = &self.root {
251 f.call::<()>(root.to_string_lossy().as_ref())?;
252 }
253 for p in &self.checker_paths {
254 if p.is_dir() {
255 f.call::<()>(p.to_string_lossy().as_ref())?;
256 }
257 }
258 let _ = lua;
259 Ok(())
260 }
261
262 fn has_lua_sibling(&self, relative: &str) -> bool {
263 for cand in [format!("{relative}.lua"), format!("{relative}/init.lua")] {
264 if let Ok(Some(_)) = self.sandbox.read(Path::new(&cand)) {
265 return true;
266 }
267 }
268 false
269 }
270
271 fn load_teal(&self, lua: &Lua, h: &Table, src: &str, resolved: &Path, name: &str) -> mlua::Result<Value> {
272 let gen_fn: Function = h.get("gen_string")?;
273 let (code, info): (Option<String>, Table) = gen_fn.call((src, resolved.to_string_lossy().as_ref()))?;
274 let Some(code) = code else {
275 let errors: Table = info.get("errors")?;
276 let msgs: Vec<String> = errors.sequence_values::<String>().collect::<mlua::Result<_>>()?;
277 return Err(mlua::Error::external(TealResolveError::TypeCheck {
278 module: name.to_string(),
279 errors: msgs,
280 }));
281 };
282 if self.held(name)
283 && let Some(errs) = self.expectation_errors(h, name)?
284 {
285 return Err(mlua::Error::external(TealResolveError::Expectation {
286 module: name.to_string(),
287 expected: self.expect_type.clone().unwrap_or_default(),
288 errors: errs,
289 }));
290 }
291 let chunk = lua
292 .load(code)
293 .set_name(format!("@{}", resolved.display()))
294 .into_function()?;
295 chunk.call::<Value>((name, resolved.to_string_lossy().as_ref()))
296 }
297}
298
299#[derive(Debug, Clone)]
307pub struct Project {
308 pub root: PathBuf,
309 pub manifest: PathBuf,
310 pub lockfile: PathBuf,
311 pub pkgs_dir: PathBuf,
312 pub vendored: PathBuf,
313 pub target_dirs: Vec<PathBuf>,
317}
318
319pub const MANIFEST_NAME: &str = "mlua-pkg.toml";
320pub const LOCKFILE_NAME: &str = "mlua-pkg.lock";
321
322impl Project {
323 pub fn find(start: &Path) -> Option<Self> {
325 let mut dir = if start.is_dir() { start.to_path_buf() } else { crate::parent_dir(start) };
326 if let Ok(abs) = std::fs::canonicalize(&dir) {
327 dir = abs;
328 }
329 loop {
330 let manifest = dir.join(MANIFEST_NAME);
331 if manifest.is_file() {
332 return Some(Self::at(&dir));
333 }
334 if !dir.pop() {
335 return None;
336 }
337 }
338 }
339
340 pub fn at(root: &Path) -> Self {
342 let pkgs_dir = match std::env::var("MLUA_PKG_DIR") {
343 Ok(p) if !p.is_empty() => PathBuf::from(p),
344 _ if root.join("target").is_dir() => root.join("target").join("mlua-pkgs"),
345 _ => root.join(".mlua-pkgs"),
346 };
347 let manifest = root.join(MANIFEST_NAME);
348 let mut target_dirs: Vec<PathBuf> = Vec::new();
351 if let Ok(m) = mlua_pkg::manifest::Manifest::from_path(&manifest) {
352 for dep in m.deps.values() {
353 if let Some(td) = &dep.target_dir {
354 let abs = root.join(td);
355 let parent = abs.parent().map(Path::to_path_buf).unwrap_or_else(|| root.to_path_buf());
356 if !target_dirs.contains(&parent) {
357 target_dirs.push(parent);
358 }
359 }
360 }
361 }
362 Self {
363 root: root.to_path_buf(),
364 manifest,
365 lockfile: root.join(LOCKFILE_NAME),
366 vendored: pkgs_dir.join("vendored"),
367 pkgs_dir,
368 target_dirs,
369 }
370 }
371
372 pub fn installed(&self) -> bool {
374 self.lockfile.is_file()
375 }
376
377 pub fn teal_resolver(&self) -> Result<TealResolver, InitError> {
380 let _ = std::fs::create_dir_all(&self.vendored);
381 TealResolver::new_symlink_aware(&self.vendored)
382 }
383
384 pub fn vendored_resolver(&self) -> anyhow::Result<mlua_pkg::resolvers::VendoredResolver> {
386 if self.installed() {
387 Ok(mlua_pkg::resolvers::VendoredResolver::from_lockfile(&self.lockfile, &self.vendored)?)
388 } else {
389 let _ = std::fs::create_dir_all(&self.vendored);
390 Ok(mlua_pkg::resolvers::VendoredResolver::new(&self.vendored)?)
391 }
392 }
393
394 pub fn registry(&self) -> anyhow::Result<mlua_pkg::Registry> {
397 let mut reg = mlua_pkg::Registry::new();
398 reg.add(self.teal_resolver()?);
399 reg.add(self.vendored_resolver()?);
400 for d in &self.target_dirs {
401 if d.is_dir() {
402 reg.add(TealResolver::new(d)?);
403 reg.add(mlua_pkg::resolvers::FsResolver::new(d)?);
404 }
405 }
406 Ok(reg)
407 }
408}
409
410pub fn contract_resolvers(root: &Path, cfg: &crate::config::HtlConfig) -> Result<Vec<TealResolver>, InitError> {
415 let mut out = Vec::new();
416 for c in &cfg.contract {
417 for mut r in TealResolver::for_contract(root, c)? {
418 for p in cfg.search_paths(root) {
419 r = r.with_checker_path(p);
420 }
421 out.push(r);
422 }
423 }
424 Ok(out)
425}
426
427impl crate::Htl {
428 pub fn apply_project(&self, p: &Project) -> anyhow::Result<()> {
431 let _ = std::fs::create_dir_all(&p.vendored);
432 self.add_path(&p.vendored)?;
433 for d in &p.target_dirs {
434 self.add_path(d)?;
435 }
436 let src = p.root.join("src");
439 if src.is_dir() {
440 self.add_path(&src)?;
441 }
442 Ok(())
443 }
444}
445
446#[derive(Debug)]
448pub enum TealResolveError {
449 TypeCheck { module: String, errors: Vec<String> },
450 Expectation { module: String, expected: String, errors: Vec<String> },
453 MissingFields { module: String, expected: String, fields: Vec<String> },
455 Read { module: String, source: ReadError },
456}
457
458impl std::fmt::Display for TealResolveError {
459 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
460 match self {
461 Self::TypeCheck { module, errors } => {
462 write!(f, "Teal type check failed for module '{module}':")?;
463 for e in errors {
464 write!(f, "\n {e}")?;
465 }
466 Ok(())
467 }
468 Self::Expectation { module, expected, errors } => {
469 write!(f, "module '{module}' does not satisfy {expected}:")?;
470 for e in errors {
471 write!(f, "\n {e}")?;
472 }
473 write!(
474 f,
475 "\n hint: annotate the returned table in the module (`local m: {expected} = {{ ... }} return m`) \
476 to get field-level errors with line numbers"
477 )
478 }
479 Self::MissingFields { module, expected, fields } => write!(
480 f,
481 "module '{module}' is missing required field(s) of {expected}: {} (every field of that record must be non-nil)",
482 fields.join(", ")
483 ),
484 Self::Read { module, source } => write!(f, "reading module '{module}': {source}"),
485 }
486 }
487}
488
489impl std::error::Error for TealResolveError {}
490
491fn preloaded(lua: &Lua, name: &str) -> mlua::Result<bool> {
493 let package: Table = lua.globals().get("package")?;
494 let preload: Table = package.get("preload")?;
495 Ok(!matches!(preload.get::<Value>(name)?, Value::Nil))
496}
497
498impl Resolver for TealResolver {
499 fn resolve(&self, lua: &Lua, name: &str) -> Option<mlua::Result<Value>> {
500 let relative = name.replace(self.module_separator, "/");
501 let last = relative.rsplit('/').next().unwrap_or(&relative).to_string();
503 let candidates = [
504 (format!("{relative}.tl"), false),
505 (format!("{relative}/init.tl"), false),
506 (format!("{relative}/{last}.tl"), false),
507 (format!("{relative}.d.tl"), true),
508 ];
509 let h = match Self::prelude(lua) {
510 Ok(h) => h,
511 Err(e) => return Some(Err(e)),
512 };
513 if let Err(e) = self.ensure_checker_path(lua, &h) {
514 return Some(Err(e));
515 }
516 for (candidate, type_only) in &candidates {
517 match self.sandbox.read(Path::new(candidate)) {
518 Ok(Some(file)) => {
519 if *type_only {
520 if self.has_lua_sibling(&relative) {
524 return None;
525 }
526 match preloaded(lua, name) {
530 Ok(true) => return None,
531 Ok(false) => {}
532 Err(e) => return Some(Err(e)),
533 }
534 return Some(
537 h.get::<Function>("type_only_module")
538 .and_then(|f| f.call::<Value>((name, file.resolved_path.to_string_lossy().as_ref()))),
539 );
540 }
541 let loaded = match self.load_teal(lua, &h, &file.content, &file.resolved_path, name) {
542 Ok(v) => v,
543 Err(e) => return Some(Err(e)),
544 };
545 if !self.held(name) {
546 return Some(Ok(loaded));
547 }
548 match self.missing_fields(&h, &loaded) {
549 Ok(m) if m.is_empty() => return Some(Ok(loaded)),
550 Ok(missing) => {
551 return Some(Err(mlua::Error::external(TealResolveError::MissingFields {
552 module: name.to_string(),
553 expected: self.expect_type.clone().unwrap_or_default(),
554 fields: missing,
555 })));
556 }
557 Err(e) => return Some(Err(e)),
558 }
559 }
560 Ok(None) => continue,
561 Err(source) => {
562 return Some(Err(mlua::Error::external(TealResolveError::Read {
563 module: name.to_string(),
564 source,
565 })));
566 }
567 }
568 }
569 None
570 }
571}