From 559ae16cf7a68916092c4515c074edcd041faab0 Mon Sep 17 00:00:00 2001 From: Enrico Zandomeni Borba Date: Thu, 30 Apr 2026 16:38:50 +0200 Subject: [PATCH 01/10] derive(Debug) for EntryKind --- vine/src/components/loader.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vine/src/components/loader.rs b/vine/src/components/loader.rs index 0f35cd44d..50c207ea4 100644 --- a/vine/src/components/loader.rs +++ b/vine/src/components/loader.rs @@ -259,7 +259,7 @@ impl RealFS { } } -#[derive(PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq)] pub enum EntryKind { File, Dir, From 6185932c64b61996bc716be62b6d629c2c1e1fc8 Mon Sep 17 00:00:00 2001 From: Enrico Zandomeni Borba Date: Thu, 30 Apr 2026 16:41:45 +0200 Subject: [PATCH 02/10] Compiler::format_diags --- cli/src/vine_cli.rs | 53 +++++++++----------- vine/src/structures/diag.rs | 98 ++++++++++++++++++++++++++++++++++++- 2 files changed, 119 insertions(+), 32 deletions(-) diff --git a/cli/src/vine_cli.rs b/cli/src/vine_cli.rs index 8c8d08595..681d05d90 100644 --- a/cli/src/vine_cli.rs +++ b/cli/src/vine_cli.rs @@ -37,6 +37,7 @@ use vine::{ structures::{ ast::Ident, content::{Color, Element, Length, Output, Surround, Writer}, + diag::Color as DiagColor, resolutions::FragmentId, }, tools::{ @@ -733,39 +734,29 @@ fn print_diags(compiler: &Compiler) { } fn _print_diags(mut w: impl io::Write + IsTerminal, compiler: &Compiler) -> io::Result<()> { - let Colors { reset, bold, underline, grey, red, yellow, .. } = colors(&w); - - let errors = compiler.diags.errors.iter().map(|x| (red, "error", x)); - let warnings = compiler.diags.warnings.iter().map(|x| (yellow, "warning", x)); - - for (color, severity, diag) in errors.chain(warnings) { - let Some(span) = diag.span() else { continue }; - writeln!(w, "{color}{severity}{grey}:{reset} {bold}{diag}{reset}")?; - if let Some(span_str) = compiler.show_span(span) { - let file = &compiler.files[span.file]; - let start = file.get_pos(span.start); - let end = file.get_pos(span.end); - let line_width = (end.line + 1).ilog10() as usize + 1; - writeln!(w, " {:>line_width$}{grey} @ {span_str}", "")?; - for line in start.line..=end.line { - let line_start = file.line_starts[line]; - let line_end = file.line_starts.get(line + 1).copied().unwrap_or(file.src.len()); - let line_str = &file.src[line_start..line_end]; - let line_str = - line_str.strip_suffix("\r\n").or(line_str.strip_suffix("\n")).unwrap_or(line_str); - let start = if line == start.line { start.col } else { 0 }; - let end = if line == end.line { end.col } else { line_str.len() }; - writeln!( - w, - " {grey}{:>line_width$} |{reset} {}{color}{underline}{}{reset}{}", - line + 1, - &line_str[..start], - &line_str[start..end], - &line_str[end..], - )?; + let Colors { reset, bold, underline, grey, red, yellow, green } = colors(&w); + + for diag_lines in compiler.format_diags() { + for diag_span in diag_lines { + if diag_span.bold { + write!(w, "{bold}")?; + } + match diag_span.color { + Some(DiagColor::Grey) => write!(w, "{grey}")?, + Some(DiagColor::Red) => write!(w, "{red}")?, + Some(DiagColor::Yellow) => write!(w, "{yellow}")?, + Some(DiagColor::Green) => write!(w, "{green}")?, + _ => (), + } + if diag_span.underline { + write!(w, "{underline}")?; + } + write!(w, "{}", diag_span.content)?; + if diag_span.is_styled() { + write!(w, "{reset}")?; } } - writeln!(w,)?; + writeln!(w)?; } Ok(()) diff --git a/vine/src/structures/diag.rs b/vine/src/structures/diag.rs index ed77f3b8d..408cec84c 100644 --- a/vine/src/structures/diag.rs +++ b/vine/src/structures/diag.rs @@ -1,4 +1,7 @@ -use std::fmt::{self, Display}; +use std::{ + borrow::Cow, + fmt::{self, Display}, +}; use ivy::text::ast::Diag as IvyDiag; use vine_util::lexer::TokenSet; @@ -354,10 +357,103 @@ fn plural<'a>(n: usize, plural: &'a str, singular: &'a str) -> &'a str { if n == 1 { singular } else { plural } } +#[derive(Default)] +pub struct DiagSpan<'a> { + pub color: Option, + pub underline: bool, + pub bold: bool, + pub content: Cow<'a, str>, +} + +impl<'a> DiagSpan<'a> { + fn new>>(content: S) -> Self { + Self { content: content.into(), ..Self::default() } + } + + pub fn is_styled(&self) -> bool { + self.color.is_some() || self.underline || self.bold + } + + fn color(self, color: Color) -> Self { + Self { color: Some(color), ..self } + } + + fn underline(self) -> Self { + Self { underline: true, ..self } + } + + fn bold(self) -> Self { + Self { bold: true, ..self } + } +} + +#[derive(Clone, Copy)] +pub enum Color { + Grey, + Red, + Yellow, + Green, +} + impl Compiler { pub fn show_span(&self, span: Span) -> Option { (span != Span::NONE).then(|| format!("{}", self.files[span.file].get_pos(span.start))) } + + pub fn format_diags(&self) -> impl Iterator>> { + use Color::*; + + let errors = self.diags.errors.iter().map(|x| (Red, "error", x)); + let warnings = self.diags.warnings.iter().map(|x| (Yellow, "warning", x)); + + errors + .chain(warnings) + .flat_map(|(color, severity, diag)| { + let span = diag.span()?; + let mut diag_lines = vec![vec![ + DiagSpan::new(severity).color(color), + DiagSpan::new(": ").color(Grey), + DiagSpan::new(diag.to_string()).bold(), + ]]; + + if let Some(span_str) = self.show_span(span) { + let file = &self.files[span.file]; + let start = file.get_pos(span.start); + let end = file.get_pos(span.end); + let line_width = (end.line + 1).ilog10() as usize + 1; + diag_lines.push(vec![ + DiagSpan::new(format!(" {:>line_width$}", "")), + DiagSpan::new(" @ ").color(Grey), + DiagSpan::new(span_str).color(Grey), + ]); + + for line in start.line..=end.line { + let line_start = file.line_starts[line]; + let line_end = file.line_starts.get(line + 1).copied().unwrap_or(file.src.len()); + let line_str = &file.src[line_start..line_end]; + let line_str = + line_str.strip_suffix("\r\n").or(line_str.strip_suffix("\n")).unwrap_or(line_str); + let start = if line == start.line { start.col } else { 0 }; + let end = if line == end.line { end.col } else { line_str.len() }; + let mut diag = + vec![DiagSpan::new(format!(" {:>line_width$} | ", line + 1)).color(Grey)]; + if start > line_str.len() { + diag.extend([DiagSpan::new(line_str), DiagSpan::new(" ").color(color).underline()]) + } else { + diag.extend([ + DiagSpan::new(&line_str[..start]), + DiagSpan::new(&line_str[start..end]).color(color).underline(), + DiagSpan::new(&line_str[end..]), + ]); + } + diag_lines.push(diag); + } + } + diag_lines.push(vec![]); + Some(diag_lines) + }) + .flatten() + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] From 2db8f3870ea3befeebfa3dd695263c6fe2a29fbd Mon Sep 17 00:00:00 2001 From: Enrico Zandomeni Borba Date: Thu, 30 Apr 2026 17:16:30 +0200 Subject: [PATCH 03/10] lsp::Doc --- lsp/src/lib.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/lsp/src/lib.rs b/lsp/src/lib.rs index 473ad8879..5670cda06 100644 --- a/lsp/src/lib.rs +++ b/lsp/src/lib.rs @@ -28,7 +28,7 @@ use vine_util::idx::IdxVec; struct Backend { client: Client, entrypoints: Vec, - docs: RwLock>, + docs: RwLock>, lsp: RwLock, checkpoint: Checkpoint, } @@ -338,15 +338,17 @@ impl LanguageServer for Backend { async fn did_open(&self, params: DidOpenTextDocumentParams) { let uri = params.text_document.uri; + let version = params.text_document.version; let text = params.text_document.text; - self.docs.write().await.insert(uri, text); + self.docs.write().await.insert(uri, Doc::new(version, text)); } async fn did_change(&self, params: DidChangeTextDocumentParams) { let uri = params.text_document.uri; + let version = params.text_document.version; if let Some(change) = params.content_changes.into_iter().last() { - self.docs.write().await.insert(uri, change.text); + self.docs.write().await.insert(uri, Doc::new(version, change.text)); } } @@ -394,7 +396,7 @@ impl LanguageServer for Backend { let src = { let docs = self.docs.read().await; match docs.get(&uri) { - Some(text) => text.clone(), + Some(doc) => doc.text.clone(), None => return Ok(None), } }; @@ -416,6 +418,18 @@ impl LanguageServer for Backend { } } +#[derive(Clone)] +pub struct Doc { + pub version: i32, + pub text: String, +} + +impl Doc { + pub fn new(version: i32, text: String) -> Self { + Self { version, text } + } +} + #[allow(clippy::absolute_paths)] #[tokio::main] pub async fn lsp( From 7b99813d13c7d36fb17cdce3adcaec6295b5d361 Mon Sep 17 00:00:00 2001 From: Enrico Zandomeni Borba Date: Thu, 30 Apr 2026 17:18:32 +0200 Subject: [PATCH 04/10] lsp::Hooks Logic for loading modules for the Lsp is now external to the lsp crate. The playground (running on wasm) can't use `glob` for entrypoints, thus this change lets it provide its own `Loader` logic. --- Cargo.lock | 2 +- cli/Cargo.toml | 1 + cli/src/vine_cli.rs | 35 ++++++++++++++- lsp/Cargo.toml | 1 - lsp/src/lib.rs | 104 +++++++++++++++++++++++++++++--------------- 5 files changed, 103 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1613b2a7f..c77f4656e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1331,6 +1331,7 @@ dependencies = [ "ivy", "notify", "rustyline", + "url", "vine", "vine-lsp", "vine-util", @@ -1341,7 +1342,6 @@ name = "vine-lsp" version = "0.0.0" dependencies = [ "futures", - "glob", "tokio", "tower-lsp", "vine", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 98e70a2fb..03852e4d7 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -10,6 +10,7 @@ clap_complete = "4.5.52" glob = "0.3.3" notify = "8.2.0" rustyline = "14.0.0" +url = "2.5.3" ivm = { path = "../ivm" } ivy = { path = "../ivy" } diff --git a/cli/src/vine_cli.rs b/cli/src/vine_cli.rs index 681d05d90..bfaa80024 100644 --- a/cli/src/vine_cli.rs +++ b/cli/src/vine_cli.rs @@ -30,6 +30,7 @@ use ivy::{ }; use notify::{RecommendedWatcher, RecursiveMode, Watcher}; use rustyline::DefaultEditor; +use url::Url; use vine::{ backend::{BackendConfig, Target, backend, vi_to_ivm}, compiler::Compiler, @@ -46,7 +47,7 @@ use vine::{ repl::{self, Repl}, }, }; -use vine_lsp::lsp; +use vine_lsp::{Doc, lsp_stdio}; use vine_util::idx::IdxVec; use crate::common::RunArgs; @@ -681,11 +682,41 @@ impl VineLspCommand { pub fn execute(self) -> Result<()> { let mut compiler = Compiler::default(); let file_paths = self.check.libs.initialize(&mut compiler); - lsp(compiler, file_paths, self.check.entrypoints); + let hooks = VineCliLspHooks::new(self.check.entrypoints); + lsp_stdio(compiler, file_paths, hooks); Ok(()) } } +struct VineCliLspHooks { + entrypoints: Vec, +} + +impl VineCliLspHooks { + fn new(entrypoints: Vec) -> Self { + Self { entrypoints } + } +} + +impl vine_lsp::Hooks for VineCliLspHooks { + fn load_modules( + &self, + compiler: &mut Compiler, + file_paths: &mut IdxVec, + _docs: &HashMap, + ) { + let mut loader = Loader::new(compiler, RealFS, Some(file_paths)); + for glob in &self.entrypoints { + for entry in glob::glob(glob).unwrap() { + let path = entry.unwrap(); + if let Some(name) = RealFS::detect_name(&path) { + loader.load_mod(name, path); + } + } + } + } +} + #[derive(Debug, Args)] pub struct VineCompletionCommand { pub shell: clap_complete::Shell, diff --git a/lsp/Cargo.toml b/lsp/Cargo.toml index fd4367d47..7576eb85f 100644 --- a/lsp/Cargo.toml +++ b/lsp/Cargo.toml @@ -5,7 +5,6 @@ edition = "2024" [dependencies] futures = "0.3.31" -glob = "0.3.1" tokio = { version = "1.41.1", features = ["full"] } tower-lsp = "0.20.0" vine = { path = "../vine" } diff --git a/lsp/src/lib.rs b/lsp/src/lib.rs index 5670cda06..c1b0a0e4f 100644 --- a/lsp/src/lib.rs +++ b/lsp/src/lib.rs @@ -3,19 +3,20 @@ use std::{ env::current_dir, fs, path::{Path, PathBuf}, + sync::Arc, time::Instant, }; use futures::{StreamExt, stream::FuturesUnordered}; -use tokio::sync::RwLock; +use tokio::{ + io::{AsyncRead, AsyncWrite}, + sync::RwLock, +}; use tower_lsp::{Client, LanguageServer, LspService, Server, jsonrpc::Result, lsp_types::*}; use vine::{ compiler::Compiler, - components::{ - loader::{FileId, Loader, RealFS}, - parser::Parser, - }, + components::{loader::FileId, parser::Parser}, structures::{ ast::{Ident, Item, ItemKind, Span, visit::VisitMut}, checkpoint::Checkpoint, @@ -25,30 +26,23 @@ use vine::{ }; use vine_util::idx::IdxVec; -struct Backend { +struct Backend { client: Client, - entrypoints: Vec, docs: RwLock>, - lsp: RwLock, + lsp: Arc>, checkpoint: Checkpoint, + hooks: H, } -impl Backend { +impl Backend { async fn refresh(&self) { let mut lsp = self.lsp.write().await; let lsp = &mut *lsp; lsp.compiler.revert(&self.checkpoint); lsp.file_paths.truncate(self.checkpoint.files.0); + let docs = self.docs.read().await; - let mut loader = Loader::new(&mut lsp.compiler, RealFS, Some(&mut lsp.file_paths)); - for glob in &self.entrypoints { - for entry in glob::glob(glob).unwrap() { - let path = entry.unwrap(); - if let Some(name) = RealFS::detect_name(&path) { - loader.load_mod(name, path); - } - } - } + self.hooks.load_modules(&mut lsp.compiler, &mut lsp.file_paths, &docs); for id in lsp.file_paths.keys_from(self.checkpoint.files) { let path = &mut lsp.file_paths[id]; @@ -58,6 +52,7 @@ impl Backend { } let start = Instant::now(); + self.hooks.pre_check(lsp.compiler.checkpoint()); _ = lsp.compiler.check(()); eprintln!("compiled in {:?}", start.elapsed()); @@ -86,15 +81,21 @@ impl Backend { futures.push(self.client.publish_diagnostics(uri, out, None)); } futures.collect::<()>().await; + + self.hooks.refresh(&mut lsp.compiler, &*self.docs.read().await); } } -struct Lsp { - compiler: Compiler, - file_paths: IdxVec, +pub struct Lsp { + pub compiler: Compiler, + pub file_paths: IdxVec, } impl Lsp { + pub fn new(compiler: Compiler, file_paths: IdxVec) -> Self { + Self { compiler, file_paths } + } + fn workspace_symbols(&self, params: WorkspaceSymbolParams) -> Option> { let query = params.query.to_lowercase(); let chart = &self.compiler.chart; @@ -294,7 +295,7 @@ impl Lsp { } #[tower_lsp::async_trait] -impl LanguageServer for Backend { +impl LanguageServer for Backend { async fn initialize(&self, params: InitializeParams) -> Result { let supports_utf8 = params.capabilities.general.is_some_and(|x| { x.position_encodings.is_some_and(|x| x.contains(&PositionEncodingKind::UTF8)) @@ -430,31 +431,62 @@ impl Doc { } } +pub trait Hooks: Send + Sync + 'static { + fn load_modules( + &self, + compiler: &mut Compiler, + file_paths: &mut IdxVec, + docs: &HashMap, + ); + + fn pre_check(&self, _checkpoint: Checkpoint) {} + + fn refresh(&self, _compiler: &mut Compiler, _docs: &HashMap) {} +} + #[allow(clippy::absolute_paths)] #[tokio::main] -pub async fn lsp( - mut compiler: Compiler, - mut file_paths: IdxVec, - entrypoints: Vec, +pub async fn lsp_stdio( + compiler: Compiler, + file_paths: IdxVec, + hooks: H, ) { - let stdin = tokio::io::stdin(); - let stdout = tokio::io::stdout(); + lsp( + Arc::new(RwLock::new(Lsp { compiler, file_paths })), + hooks, + tokio::io::stdin(), + tokio::io::stdout(), + ) + .await; +} - _ = compiler.check(()); - let checkpoint = compiler.checkpoint(); +pub async fn lsp( + lsp: Arc>, + hooks: H, + stdin: impl AsyncRead + Unpin, + stdout: impl AsyncWrite, +) { + let checkpoint = { + let Lsp { compiler, file_paths } = &mut *lsp.write().await; + _ = compiler.check(()); + let checkpoint = compiler.checkpoint(); - for path in file_paths.values_mut() { - if let Ok(path_) = fs::canonicalize(&path) { - *path = path_; + for path in file_paths.values_mut() { + if let Ok(path_) = fs::canonicalize(&path) { + *path = path_; + } } - } + + checkpoint + }; let (service, socket) = LspService::new(|client| Backend { client, - entrypoints, docs: RwLock::new(HashMap::new()), - lsp: RwLock::new(Lsp { compiler, file_paths }), + lsp, checkpoint, + hooks, }); + Server::new(stdin, stdout, socket).serve(service).await; } From 26cecd4a5aa35c22f3cd338931e2ad06c85551cb Mon Sep 17 00:00:00 2001 From: Enrico Zandomeni Borba Date: Thu, 30 Apr 2026 17:21:12 +0200 Subject: [PATCH 05/10] fix hanging lexer fixes lexer hangs on unterminated block comment --- util/src/lexer.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/util/src/lexer.rs b/util/src/lexer.rs index f83c05f8d..2a2dba2ba 100644 --- a/util/src/lexer.rs +++ b/util/src/lexer.rs @@ -120,9 +120,10 @@ pub trait Lex<'src> { depth -= 1; } } - _ => { + Some(_) => { self.bump(); } + None => break, } } } From a7e42803ae672028e6cd6461031006f6e5351934 Mon Sep 17 00:00:00 2001 From: Enrico Zandomeni Borba Date: Thu, 30 Apr 2026 17:26:05 +0200 Subject: [PATCH 06/10] Checkpoint::templates Only Compiler::nets_from instantiates templates, so storing a checkpoint after Compiler::check and using it for Compiler::nets_from resulted in a panic. This should probably be tracked by the Compiler internally anyways. --- vine/src/compiler.rs | 2 +- vine/src/structures/checkpoint.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/vine/src/compiler.rs b/vine/src/compiler.rs index 26d144f47..2a41e666c 100644 --- a/vine/src/compiler.rs +++ b/vine/src/compiler.rs @@ -149,7 +149,7 @@ impl Compiler { ) { let guide = &Guide::build(table); - for fragment_id in self.fragments.keys_from(checkpoint.fragments) { + for fragment_id in self.fragments.keys_from(checkpoint.templates) { let fragment = &self.fragments[fragment_id]; let vir = &self.vir[fragment_id]; let template = emit(self.debug, &self.chart, fragment, vir, &mut self.specs, table, guide); diff --git a/vine/src/structures/checkpoint.rs b/vine/src/structures/checkpoint.rs index fcf474643..2f89d9ab6 100644 --- a/vine/src/structures/checkpoint.rs +++ b/vine/src/structures/checkpoint.rs @@ -35,6 +35,7 @@ pub struct Checkpoint { pub traits: TraitId, pub impls: ImplId, pub fragments: FragmentId, + pub templates: FragmentId, pub specs: SpecId, pub impl_trees: ImplTreeId, pub files: FileId, @@ -59,6 +60,7 @@ impl Compiler { traits: self.chart.traits.next_index(), impls: self.chart.impls.next_index(), fragments: self.fragments.next_index(), + templates: self.templates.next_index(), specs: self.specs.specs.next_index(), impl_trees: self.specs.impls.next_index(), files: self.files.next_index(), @@ -95,7 +97,7 @@ impl Compiler { files.truncate(checkpoint.files.0); fragments.truncate(checkpoint.fragments.0); vir.truncate(checkpoint.fragments.0); - templates.truncate(checkpoint.fragments.0); + templates.truncate(checkpoint.templates.0); } } From 3255d66808559dd12f0e0d75821dad57778549a9 Mon Sep 17 00:00:00 2001 From: Enrico Zandomeni Borba Date: Mon, 4 May 2026 10:46:42 -0400 Subject: [PATCH 07/10] Registry API (remove 'r lifetime in extfn rettys) --- ivm/src/host/ext.rs | 33 ++++++++++++------------ ivm/src/host/ext/common.rs | 48 ++++++++++++++++++++++++---------- ivy/src/engine.rs | 12 ++++++++- ivy/src/translate.rs | 15 ++++++++++- util/src/arithmetic.rs | 12 ++++----- util/src/register.rs | 17 +++++++++---- vine/src/backend.rs | 51 ++++++++++++++++++++++++------------- vine/src/tools/repl/show.rs | 7 +++-- 8 files changed, 132 insertions(+), 63 deletions(-) diff --git a/ivm/src/host/ext.rs b/ivm/src/host/ext.rs index 133c906fc..b23707ed6 100644 --- a/ivm/src/host/ext.rs +++ b/ivm/src/host/ext.rs @@ -5,7 +5,7 @@ use std::{ }; use ivy::name::{NameId, Table}; -use vine_util::register::Register; +use vine_util::register::{Register, Registry}; pub mod common; @@ -22,18 +22,19 @@ use crate::{ }, }; -pub struct HostTable<'ivm, 'r> { - host: &'r mut Host<'ivm>, - table: &'r mut Table, +impl<'ivm> Registry for Host<'ivm> { + type Mut<'r> + = (&'r mut Host<'ivm>, &'r mut Table) + where + Self: 'r; + fn fork<'a>((host, table): &'a mut Self::Mut<'_>) -> Self::Mut<'a> { + (host, table) + } } impl<'ivm> Host<'ivm> { - pub fn register<'r>( - &'r mut self, - table: &'r mut Table, - value: impl Register>, - ) { - value.register(&mut HostTable { host: self, table }); + pub fn register<'r>(&'r mut self, table: &'r mut Table, value: impl Register>) { + value.register((self, table)); } pub fn get_ext_ty>(&self) -> Option> { @@ -136,21 +137,21 @@ impl<'ivm> Host<'ivm> { } #[allow(nonstandard_style)] -pub fn ExtFn<'ivm, 'r, const I: usize, const O: usize, W, F: ExtFn<'ivm, I, O, W>>( +pub fn ExtFn<'ivm, const I: usize, const O: usize, W, F: ExtFn<'ivm, I, O, W>>( path: &str, f: F, -) -> impl Register> { +) -> impl Register> { pub struct _ExtFn<'p, const I: usize, const O: usize, W, F> { path: &'p str, f: F, phantom: PhantomData<([(); I], [(); O], W)>, } - impl<'r, 'p, 'ivm, const I: usize, const O: usize, W, F: ExtFn<'ivm, I, O, W>> - Register> for _ExtFn<'p, I, O, W, F> + impl<'p, 'ivm, const I: usize, const O: usize, W, F: ExtFn<'ivm, I, O, W>> Register> + for _ExtFn<'p, I, O, W, F> { - fn register(self, registry: &mut HostTable<'ivm, 'r>) { - registry.host.register_ext_fn(registry.table, self.path, self.f); + fn register(self, (host, table): (&mut Host<'ivm>, &mut Table)) { + host.register_ext_fn(table, self.path, self.f); } } diff --git a/ivm/src/host/ext/common.rs b/ivm/src/host/ext/common.rs index 06406c409..f32395a60 100644 --- a/ivm/src/host/ext/common.rs +++ b/ivm/src/host/ext/common.rs @@ -12,8 +12,8 @@ use crate::{ host::{ Host, ext::{ - ExtFn, ExtInput, ExtInputs, ExtOutput, ExtOutputs, ExtTyBoxed, ExtTyRegister, HostTable, - Invalid, Live, error, + ExtFn, ExtInput, ExtInputs, ExtOutput, ExtOutputs, ExtTyBoxed, ExtTyRegister, Invalid, Live, + error, }, }, runtime::{ @@ -27,7 +27,23 @@ use crate::{ }, }; -pub fn fundamental<'ivm, 'r>() -> impl Register> { +pub fn all<'ivm, R: Read, W: Write>( + args: &'ivm [String], + stdin: impl Copy + Fn() -> R + Send + Sync + 'ivm, + stdout: impl Copy + Fn() -> W + Send + Sync + 'ivm, +) -> impl Register> { + ( + fundamental(), + arithmetic(), + io_meta(), + io_stdio_with(stdin, stdout), + io_args(args), + io_error(), + io_file(), + ) +} + +pub fn fundamental<'ivm>() -> impl Register> { ( ExtFn("ivm:pair", |(a, b): (ExtVal<'ivm>, ExtVal<'ivm>)| Pair(a, b)), ExtFn("ivm:unpair", |Pair(a, b)| (a, b)), @@ -66,7 +82,7 @@ pub fn fundamental<'ivm, 'r>() -> impl Register> { ) } -pub fn arithmetic<'ivm: 'r, 'r>() -> impl Register> { +pub fn arithmetic<'ivm>() -> impl Register> { use vine_util::arithmetic::{Define, arithmetic}; struct Arithmetic(PhantomData); @@ -79,17 +95,21 @@ pub fn arithmetic<'ivm: 'r, 'r>() -> impl Register> { O: ExtOutputs<'ivm, ON, OW>, const ON: usize, OW, - > Define> for HostTable<'ivm, '_> + > Define> for Host<'ivm> { - fn define(&mut self, name: &'static str, f: impl 'static + Send + Sync + Fn(I) -> O) { - self.host.register_ext_fn(self.table, name, f); + fn define( + (host, table): (&mut Host<'ivm>, &mut Table), + name: &'static str, + f: impl 'static + Send + Sync + Fn(I) -> O, + ) { + host.register_ext_fn(table, name, f); } } arithmetic(Invalid) } -pub fn io_meta<'ivm, 'r>() -> impl Register> { +pub fn io_meta<'ivm>() -> impl Register> { ( ExtFn("vi:io:split", |IO| (IO, IO)), ExtFn("vi:io:merge", |(IO, IO)| IO), @@ -97,14 +117,14 @@ pub fn io_meta<'ivm, 'r>() -> impl Register> { ) } -pub fn io_stdio<'ivm, 'r>() -> impl Register> { +pub fn io_stdio<'ivm>() -> impl Register> { io_stdio_with(io::stdin, io::stdout) } -pub fn io_stdio_with<'ivm, 'r, R: Read, W: Write>( +pub fn io_stdio_with<'ivm, R: Read, W: Write>( stdin: impl Copy + Fn() -> R + Send + Sync + 'ivm, stdout: impl Copy + Fn() -> W + Send + Sync + 'ivm, -) -> impl Register> { +) -> impl Register> { ( ExtFn("root:io:print_char", move |(IO, n): (IO, u32)| -> IO { write!(stdout(), "{}", char::try_from(n).unwrap()).unwrap(); @@ -134,11 +154,11 @@ pub fn io_stdio_with<'ivm, 'r, R: Read, W: Write>( ) } -pub fn io_args<'ivm, 'r>(args: &'ivm [String]) -> impl Register> { +pub fn io_args<'ivm>(args: &'ivm [String]) -> impl Register> { ExtFn("root:io:args", |IO| (List(args.iter().map(|str| List(str.chars()))), IO)) } -pub fn io_file<'ivm, 'r>() -> impl Register> { +pub fn io_file<'ivm>() -> impl Register> { ( ExtFn("root:io:file:open", |(_, path): (IO, String)| { (OpenOptions::new().read(true).append(true).open(path), IO) @@ -169,7 +189,7 @@ pub fn io_file<'ivm, 'r>() -> impl Register> { ) } -pub fn io_error<'ivm, 'r>() -> impl Register> { +pub fn io_error<'ivm>() -> impl Register> { ( ExtFn("root:io:error:code", |error: Boxed| { let code = match error.kind() { diff --git a/ivy/src/engine.rs b/ivy/src/engine.rs index b95273d98..6c8ee9669 100644 --- a/ivy/src/engine.rs +++ b/ivy/src/engine.rs @@ -10,7 +10,7 @@ use vine_util::{ exact_size, idx::{IdxSlab, IdxVec}, new_idx, - register::Register, + register::{Register, Registry}, }; use crate::{ @@ -594,6 +594,16 @@ pub struct Rules<'a> { >, } +impl Registry for Rules<'_> { + type Mut<'a> + = &'a mut Self + where + Self: 'a; + fn fork<'a>(ref_: &'a mut Self::Mut<'_>) -> Self::Mut<'a> { + ref_ + } +} + impl<'a> Rules<'a> { pub fn new(rules: impl Register>) -> Self { let mut self_ = Self::default(); diff --git a/ivy/src/translate.rs b/ivy/src/translate.rs index d9e5b05ca..9a5d383fe 100644 --- a/ivy/src/translate.rs +++ b/ivy/src/translate.rs @@ -1,6 +1,10 @@ use std::{collections::HashMap, rc::Rc}; -use vine_util::{idx::Counter, new_idx, register::Register}; +use vine_util::{ + idx::Counter, + new_idx, + register::{Register, Registry}, +}; use crate::{ name::{NameId, PathId, Table}, @@ -77,6 +81,15 @@ impl<'a> Translator<'a> { } } +impl Registry for Translator<'_> { + type Mut<'a> + = &'a mut Self + where + Self: 'a; + fn fork<'a>(ref_: &'a mut Self::Mut<'_>) -> Self::Mut<'a> { + ref_ + } +} pub struct Translate, F: Fn(FlatNode, &mut Table, &mut FlatNet)>( pub I, pub F, diff --git a/util/src/arithmetic.rs b/util/src/arithmetic.rs index b96cbabe3..511860546 100644 --- a/util/src/arithmetic.rs +++ b/util/src/arithmetic.rs @@ -1,9 +1,9 @@ use std::marker::PhantomData; -use crate::register::Register; +use crate::register::{Register, Registry}; -pub trait Define { - fn define(&mut self, name: &'static str, f: impl 'static + Send + Sync + Fn(I) -> O); +pub trait Define: Registry { + fn define(self_: Self::Mut<'_>, name: &'static str, f: impl 'static + Send + Sync + Fn(I) -> O); } #[allow(nonstandard_style)] @@ -13,11 +13,11 @@ fn Fn, I, O, W>( ) -> impl Register { struct Def(&'static str, F, PhantomData<(I, O, W)>); - impl O, I, O, R: Define, W> Register + impl, F: 'static + Send + Sync + Fn(I) -> O, I, O, W> Register for Def { - fn register(self, registry: &mut R) { - registry.define(self.0, self.1); + fn register(self, registry: R::Mut<'_>) { + R::define(registry, self.0, self.1); } } diff --git a/util/src/register.rs b/util/src/register.rs index afe237e32..5b7da543d 100644 --- a/util/src/register.rs +++ b/util/src/register.rs @@ -1,5 +1,12 @@ -pub trait Register: Sized { - fn register(self, registry: &mut R); +pub trait Registry { + type Mut<'a> + where + Self: 'a; + fn fork<'a>(ref_: &'a mut Self::Mut<'_>) -> Self::Mut<'a>; +} + +pub trait Register: Sized { + fn register(self, registry: R::Mut<'_>); } macro_rules! impl_tuple { @@ -9,10 +16,10 @@ macro_rules! impl_tuple { }; ($($T:ident)* |) => { #[allow(warnings)] - impl,)*> Register for ($($T,)*) { - fn register(self, registry: &mut R) { + impl,)*> Register for ($($T,)*) { + fn register(self, mut registry: R::Mut<'_>) { let ($($T,)*) = self; - $($T.register(registry);)* + $($T.register(R::fork(&mut registry));)* } } }; diff --git a/vine/src/backend.rs b/vine/src/backend.rs index 81b05e0d9..7fce2ccbd 100644 --- a/vine/src/backend.rs +++ b/vine/src/backend.rs @@ -21,7 +21,11 @@ use ivy::{ common::{chain_binary, elide_unary, map_name, replace_name, replace_nilary, replace_path}, }, }; -use vine_util::{exact_size, nat::Nat, register::Register}; +use vine_util::{ + exact_size, + nat::Nat, + register::{Register, Registry}, +}; use crate::compiler::Guide; @@ -365,17 +369,29 @@ pub fn arithmetic<'r>(vi: &'r Guide, table: &mut Table) -> impl use<'r> + Regist } } - struct Registry<'a> { - table: &'a mut Table, - fns: &'a mut HashMap bool>>, + type Fns = HashMap bool>>; + + struct Arithmetic; + impl Registry for Arithmetic { + type Mut<'r> + = (&'r mut Fns, &'r mut Table) + where + Self: 'r; + fn fork<'a>((registry, table): &'a mut Self::Mut<'_>) -> Self::Mut<'a> { + (registry, table) + } } impl, O: Outputs> Define - for Registry<'_> + for Arithmetic { - fn define(&mut self, name: &'static str, f: impl 'static + Send + Sync + Fn(I) -> O) { - let name = self.table.add_path_name(name); - self.fns.insert( + fn define( + (fns, table): Self::Mut<'_>, + name: &'static str, + f: impl 'static + Send + Sync + Fn(I) -> O, + ) { + let name = table.add_path_name(name); + fns.insert( name, Box::new(move |engine, vi, net, node| { let name = engine.name(node); @@ -400,16 +416,15 @@ pub fn arithmetic<'r>(vi: &'r Guide, table: &mut Table) -> impl use<'r> + Regist } let mut fns = HashMap::new(); - let mut registry = Registry { table, fns: &mut fns }; - arithmetic(()).register(&mut registry); - registry.define("vi:bool:and", |(a, b): (bool, bool)| a & b); - registry.define("vi:bool:or", |(a, b): (bool, bool)| a | b); - registry.define("vi:bool:xor", |(a, b): (bool, bool)| a ^ b); - registry.define("vi:bool:eq", |(a, b): (bool, bool)| a == b); - registry.define("vi:bool:le", |(a, b): (bool, bool)| a <= b); - registry.define("vi:bool:lt", |(a, b): (bool, bool)| !a & b); - registry.define("vi:bool:not", |a: bool| !a); - registry.define("vi:bool:to_n32", |a: bool| a as u32); + Register::::register(arithmetic(()), (&mut fns, table)); + Arithmetic::define((&mut fns, table), "vi:bool:and", |(a, b): (bool, bool)| a & b); + Arithmetic::define((&mut fns, table), "vi:bool:or", |(a, b): (bool, bool)| a | b); + Arithmetic::define((&mut fns, table), "vi:bool:xor", |(a, b): (bool, bool)| a ^ b); + Arithmetic::define((&mut fns, table), "vi:bool:eq", |(a, b): (bool, bool)| a == b); + Arithmetic::define((&mut fns, table), "vi:bool:le", |(a, b): (bool, bool)| a <= b); + Arithmetic::define((&mut fns, table), "vi:bool:lt", |(a, b): (bool, bool)| !a & b); + Arithmetic::define((&mut fns, table), "vi:bool:not", |a: bool| !a); + Arithmetic::define((&mut fns, table), "vi:bool:to_n32", |a: bool| a as u32); Rewrite([vi.ext], move |engine, _, net, node| { let name = engine.name(node); diff --git a/vine/src/tools/repl/show.rs b/vine/src/tools/repl/show.rs index 0c227c19b..087fe592a 100644 --- a/vine/src/tools/repl/show.rs +++ b/vine/src/tools/repl/show.rs @@ -5,7 +5,10 @@ use std::{ }; use ivm::{ - host::ext::{ExtFn, ExtTyBoxed, HostTable}, + host::{ + Host, + ext::{ExtFn, ExtTyBoxed}, + }, runtime::{ Runtime, ext::Boxed, @@ -120,7 +123,7 @@ impl<'ivm> ExtTyBoxed<'ivm> for Slot { type With<'x> = Slot; } -pub fn extrinsics<'ivm, 'r>() -> impl Register> { +pub fn extrinsics<'ivm>() -> impl Register> { ExtFn("vi:repl:show", |(slot, str): (Slot, String)| { *slot.0.lock().unwrap() = Some(str); }) From f9752de75dc6d4c98db0104b59e500f185334b04 Mon Sep 17 00:00:00 2001 From: Enrico Zandomeni Borba Date: Wed, 6 May 2026 00:06:16 +0200 Subject: [PATCH 08/10] runtime hooks --- ivm/src/runtime.rs | 43 ++++++++++++++++++++++++++++++------- vine/src/tools/repl.rs | 4 ++-- vine/src/tools/repl/show.rs | 2 +- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/ivm/src/runtime.rs b/ivm/src/runtime.rs index 202d7c83c..d070fa10e 100644 --- a/ivm/src/runtime.rs +++ b/ivm/src/runtime.rs @@ -11,6 +11,7 @@ pub mod flags; pub mod graft; pub mod heap; pub mod port; +pub mod runner; pub mod stats; pub mod wire; pub mod word; @@ -60,22 +61,31 @@ impl<'ivm, 'ext> Runtime<'ivm, 'ext> { } /// Normalize all nets in this IVM. - pub fn normalize(&mut self) { - let start = Instant::now(); + pub fn normalize(&mut self, mut hooks: impl Hooks) { + let start = hooks.start(); loop { + hooks.tick(&mut self.stats); + self.do_fast(); + if let Some((a, b)) = self.active_slow.pop() { - self.interact(a, b) + self.interact(a, b); } else { break; } } - self.stats.time_clock += start.elapsed(); + + hooks.tick(&mut self.stats); + + if let Some(start) = start { + self.stats.time_clock += start.elapsed(); + } } + /// Reduce all "fast" active pairs, returning the number of interactions. pub(crate) fn do_fast(&mut self) { while let Some((a, b)) = self.active_fast.pop() { - self.interact(a, b) + self.interact(a, b); } } @@ -83,10 +93,12 @@ impl<'ivm, 'ext> Runtime<'ivm, 'ext> { /// /// This is useful to get the depth (longest critical path) of the computation /// to understand the parallelism of the program. - pub fn normalize_breadth_first(&mut self) { - let start = Instant::now(); + pub fn normalize_breadth_first(&mut self, mut hooks: impl Hooks) { + let start = hooks.start(); let mut work = vec![]; loop { + hooks.tick(&mut self.stats); + mem::swap(&mut work, &mut self.active_fast); work.append(&mut self.active_slow); if work.is_empty() { @@ -97,6 +109,21 @@ impl<'ivm, 'ext> Runtime<'ivm, 'ext> { } self.stats.depth += 1; } - self.stats.time_clock += start.elapsed(); + + hooks.tick(&mut self.stats); + + if let Some(start) = start { + self.stats.time_clock += start.elapsed(); + } } } + +pub trait Hooks { + fn start(&mut self) -> Option { + Some(Instant::now()) + } + + fn tick(&mut self, _stats: &mut Stats) {} +} + +impl Hooks for () {} diff --git a/vine/src/tools/repl.rs b/vine/src/tools/repl.rs index 0e05a09a2..0be072ec6 100644 --- a/vine/src/tools/repl.rs +++ b/vine/src/tools/repl.rs @@ -318,12 +318,12 @@ impl<'ctx, 'ivm, 'ext, 'comp> Repl<'ctx, 'ivm, 'ext, 'comp> { let name = self.table.add_path_name(format!("::{}", self.line)); self.rt.graft(self.program.graft(name).unwrap(), Port::new_wire(root)); - self.rt.normalize(); + self.rt.normalize(()); let mut result = Port::new_wire(result); let output = self.show(ty, &mut result); self.rt.link_wire(destroy, result); - self.rt.normalize(); + self.rt.normalize(()); if output != "()" { println!("{output}"); diff --git a/vine/src/tools/repl/show.rs b/vine/src/tools/repl/show.rs index 087fe592a..c1347328f 100644 --- a/vine/src/tools/repl/show.rs +++ b/vine/src/tools/repl/show.rs @@ -108,7 +108,7 @@ impl<'ctx, 'ivm, 'ext, 'comp> Repl<'ctx, 'ivm, 'ext, 'comp> { let slot_ext_ty = self.host.get_ext_ty().unwrap(); self.rt.link_wire(slot_, Port::new_ext_val(slot_ext_ty.wrap_static(Boxed::new(slot.clone())))); - self.rt.normalize(); + self.rt.normalize(()); let string = slot.0.lock().unwrap().take(); From d87b67f260439ed12cda4b3f9be17aa7dcba4f58 Mon Sep 17 00:00:00 2001 From: Enrico Zandomeni Borba Date: Wed, 6 May 2026 00:06:01 +0200 Subject: [PATCH 09/10] ivm runner --- cli/src/common.rs | 175 +++++++++------------------------------ cli/src/vine_cli.rs | 6 +- ivm/src/host.rs | 1 + ivm/src/host/runner.rs | 114 +++++++++++++++++++++++++ ivm/src/runtime.rs | 1 - ivm/src/runtime/flags.rs | 34 +++++++- 6 files changed, 188 insertions(+), 143 deletions(-) create mode 100644 ivm/src/host/runner.rs diff --git a/cli/src/common.rs b/cli/src/common.rs index 71ff3219c..865b97e00 100644 --- a/cli/src/common.rs +++ b/cli/src/common.rs @@ -1,20 +1,13 @@ -use std::{ - collections::HashMap, - io::{self, Read, Write}, - process::exit, - sync::{Mutex, MutexGuard}, -}; +use std::{collections::HashMap, io, process::exit}; use clap::Args; use ivm::{ - host::{Host, IVM, ext::common::IO}, - program::Program, - runtime::{ - flags::Flags, - heap::Heap, - port::{Port, Tag}, - stats::Stats, + host::{ + Host, IVM, + ext::common, + runner::{CaptureOutput, Runner}, }, + runtime::{flags::Flags, heap::Heap, stats::Stats}, }; use ivy::{ name::{NameId, Table}, @@ -37,149 +30,59 @@ pub struct RunArgs { } impl RunArgs { - /// Run the nets forwarding stdout and stderr, exiting if any errors occur. + /// Run the nets forwarding stdin and stdout, exiting if any errors occur. pub fn run(&self, table: &mut Table, nets: &HashMap, debug_hint: bool) { - self.run_with(table, nets, io::stdin, io::stdout).report(debug_hint); - } + let mut heap = self.heap(); + let mut ivm = IVM::new(); + let mut host = Host::new(&mut ivm); + let extrinsics = common::all(&self.args, io::stdin, io::stdout); + let runner = Runner::new(&mut heap, &mut host, extrinsics, table, nets); - /// Run the nets capturing any stdout and stderr. - pub fn run_capture( - &self, - table: &mut Table, - nets: &HashMap, - ) -> (RunResult, Vec) { - pub struct SharedWriter<'a>(pub MutexGuard<'a, Vec>); + let (mut stats, flags) = runner.normalize(self.breadth_first, self.workers, ()); - impl Write for SharedWriter<'_> { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.0.extend_from_slice(buf); - Ok(buf.len()) - } + if !flags.success() { + eprintln!("\n{}", flags.error_message(debug_hint)); + } - fn flush(&mut self) -> io::Result<()> { - Ok(()) + if !self.no_stats { + if self.no_perf { + stats.clear_perf(); } + eprintln!("{stats}"); } - let input: &[u8] = &[]; - let output = Mutex::default(); - let result = self.run_with(table, nets, || input, || SharedWriter(output.lock().unwrap())); - - (result, output.into_inner().unwrap()) + if !flags.success() { + exit(1); + } } - pub fn run_with( + /// Run the nets with empty stdin and capturing any stdout. + pub fn run_capture( &self, table: &mut Table, nets: &HashMap, - stdin: impl Copy + Fn() -> R + Send + Sync, - stdout: impl Copy + Fn() -> W + Send + Sync, - ) -> RunResult { - let mut heap = match self.heap { - Some(size) => Heap::with_size(size).expect("heap allocation failed"), - None => Heap::new(), - }; - + ) -> (Stats, Flags, Vec) { + let capture = CaptureOutput::default(); + let mut heap = self.heap(); let mut ivm = IVM::new(); - let mut host = Host::new(&mut ivm); - - let io = host.register_ext_ty::(); - - use ivm::host::ext::common; - host.register( - table, - ( - common::fundamental(), - common::arithmetic(), - common::io_meta(), - common::io_stdio_with(stdin, stdout), - common::io_args(&self.args), - common::io_error(), - common::io_file(), - ), - ); - - let program = Program::new(&host, table, nets); - let main = table.add_path_name("iv:main"); - - let mut runtime = host.init(&mut heap); - let main = program.graft(main).expect("missing main"); - let node = unsafe { runtime.new_node(Tag::Comb, 0) }; - runtime.link_wire(node.1, Port::new_ext_val(io.wrap_static(IO))); - runtime.link(Port::new_graft(main), node.0); + let (stats, flags) = { + let mut host = Host::new(&mut ivm); + let extrinsics = capture.extrinsics(&self.args); + let runner = Runner::new(&mut heap, &mut host, extrinsics, table, nets); - if self.breadth_first { - runtime.normalize_breadth_first(); - } else if self.workers > 0 { - runtime.normalize_parallel(self.workers) - } else { - runtime.normalize(); - } - - let out = runtime.follow(Port::new_wire(node.2)); - if self.no_perf { - runtime.stats.clear_perf(); - } + runner.normalize(self.breadth_first, self.workers, ()) + }; - RunResult { - stats: (!self.no_stats).then_some(runtime.stats), - flags: runtime.flags, - no_io: out.tag() != Tag::ExtVal || unsafe { out.as_ext_val() }.ty_id() != io.id(), - vicious: runtime.stats.mem_free < runtime.stats.mem_alloc, - } + (stats, flags, capture.into_output()) } -} -pub struct RunResult { - pub stats: Option, - pub flags: Flags, - pub no_io: bool, - pub vicious: bool, -} - -impl RunResult { - pub fn report(&self, debug_hint: bool) { - let RunResult { - stats, - no_io, - vicious, - flags: Flags { ext_copy, ext_erase, ext_generic, invalid_interaction }, - } = *self; - if no_io { - eprintln!("\nError: the net did not return its `IO` handle"); - if debug_hint { - eprintln!(" hint: try running the program in `--debug` mode to see error messages"); - } - } - if vicious { - eprintln!("\nError: the net created a vicious circle"); - } - if ext_copy { - eprintln!("\nError: a linear extrinsic was copied"); - } - if ext_erase { - eprintln!("\nError: a linear extrinsic was erased"); - } - if ext_generic { - eprintln!("\nError: an extrinsic function encountered an unspecified error"); - } - if invalid_interaction { - eprintln!("\nError: an invalid interaction was performed"); - } - - if let Some(stats) = &stats { - eprintln!("{}", stats); - } - - if !self.success() { - exit(1); + fn heap(&self) -> Box { + match self.heap { + Some(size) => Heap::with_size(size).expect("heap allocation failed"), + None => Heap::new(), } } - - pub fn success(&self) -> bool { - !self.no_io && !self.vicious && self.flags.success() - } } fn parse_size(size: &str) -> anyhow::Result { diff --git a/cli/src/vine_cli.rs b/cli/src/vine_cli.rs index bfaa80024..2c3eea325 100644 --- a/cli/src/vine_cli.rs +++ b/cli/src/vine_cli.rs @@ -266,14 +266,14 @@ impl VineTestCommand { compiler.insert_main_net(table, &mut nets, test_id, &translator); eprint!("{grey}test{reset} {bold}{path}{reset} {grey}...{reset} "); - let (result, output) = self.run_args.run_capture(table, &nets); - if result.success() { + let (_, flags, output) = self.run_args.run_capture(table, &nets); + if flags.success() { eprintln!("{green}ok{reset}"); } else { failed = true; eprintln!("{red}FAILED{reset}"); } - if self.no_capture || !result.success() { + if self.no_capture || !flags.success() { io::stderr().write_all(&output)?; eprintln!(); } diff --git a/ivm/src/host.rs b/ivm/src/host.rs index 7e5540b32..7954ddb31 100644 --- a/ivm/src/host.rs +++ b/ivm/src/host.rs @@ -14,6 +14,7 @@ use crate::{ }; pub mod ext; +pub mod runner; pub struct IVM<'ivm> { heaps: Arena, diff --git a/ivm/src/host/runner.rs b/ivm/src/host/runner.rs new file mode 100644 index 000000000..d4e57881d --- /dev/null +++ b/ivm/src/host/runner.rs @@ -0,0 +1,114 @@ +use std::{ + collections::HashMap, + io::{self, Write}, + sync::{Mutex, MutexGuard}, +}; + +use ivy::{ + name::{NameId, Table}, + net::FlatNet, +}; +use vine_util::register::Register; + +use crate::{ + host::{ + Host, + ext::common::{self, IO}, + }, + program::Program, + runtime::{ + Hooks, Runtime, + ext::ExtTy, + flags::Flags, + heap::Heap, + port::{Port, Tag}, + stats::Stats, + wire::Wire, + }, +}; + +pub struct Runner<'ivm, 'ext> { + io: ExtTy<'ivm, IO>, + runtime: Runtime<'ivm, 'ext>, + root: Wire<'ivm>, +} + +impl<'ivm, 'ext> Runner<'ivm, 'ext> { + pub fn new( + heap: &'ivm mut Heap, + host: &'ext mut Host<'ivm>, + extrinsics: impl Register>, + table: &mut Table, + nets: &HashMap, + ) -> Self { + let io = host.register_ext_ty::(); + + host.register(table, extrinsics); + + let program = Program::new(host, table, nets); + let main = table.add_path_name("iv:main"); + let main = program.graft(main).expect("missing main"); + + let mut runtime = host.init(heap); + + let node = unsafe { runtime.new_node(Tag::Comb, 0) }; + runtime.link_wire(node.1, Port::new_ext_val(io.wrap_static(IO))); + runtime.link(Port::new_graft(main), node.0); + + Self { io, root: node.2, runtime } + } + + pub fn normalize( + mut self, + breadth_first: bool, + workers: usize, + hooks: impl Hooks, + ) -> (Stats, Flags) { + if breadth_first { + self.runtime.normalize_breadth_first(hooks); + } else if workers > 0 { + self.runtime.normalize_parallel(workers) + } else { + self.runtime.normalize(hooks); + } + + let out = self.runtime.follow(Port::new_wire(self.root)); + + self.runtime.flags.no_io = + out.tag() != Tag::ExtVal || unsafe { out.as_ext_val() }.ty_id() != self.io.id(); + self.runtime.flags.vicious = self.runtime.stats.mem_free < self.runtime.stats.mem_alloc; + + (self.runtime.stats, self.runtime.flags) + } +} + +#[derive(Default)] +pub struct CaptureOutput { + pub output: Mutex>, +} + +impl CaptureOutput { + pub fn extrinsics<'a: 'ivm, 'b: 'ivm, 'ivm>( + &'a self, + args: &'b [String], + ) -> impl Register> where { + common::all(args, || &[][..], || SharedWriter(self.output.lock().unwrap())) + } + + pub fn into_output(self) -> Vec { + self.output.into_inner().unwrap() + } +} + +struct SharedWriter<'a>(MutexGuard<'a, Vec>); + +impl Write for SharedWriter<'_> { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.extend_from_slice(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} diff --git a/ivm/src/runtime.rs b/ivm/src/runtime.rs index d070fa10e..5b06c5c48 100644 --- a/ivm/src/runtime.rs +++ b/ivm/src/runtime.rs @@ -11,7 +11,6 @@ pub mod flags; pub mod graft; pub mod heap; pub mod port; -pub mod runner; pub mod stats; pub mod wire; pub mod word; diff --git a/ivm/src/runtime/flags.rs b/ivm/src/runtime/flags.rs index d52e91ff6..2e04811d2 100644 --- a/ivm/src/runtime/flags.rs +++ b/ivm/src/runtime/flags.rs @@ -1,11 +1,10 @@ /// Error flags set during interactions. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct Flags { - /// A non-copyable extrinsic was copied. + pub no_io: bool, + pub vicious: bool, pub ext_copy: bool, - /// A non-copyable extrinsic was erased. pub ext_erase: bool, - /// An extrinsic function encountered an unspecified error. pub ext_generic: bool, pub invalid_interaction: bool, } @@ -14,4 +13,33 @@ impl Flags { pub fn success(self) -> bool { self == Self::default() } + + pub fn error_message(&self, debug_hint: bool) -> String { + let Self { no_io, vicious, ext_copy, ext_erase, ext_generic, invalid_interaction } = self; + let mut errors = Vec::new(); + + if *no_io { + errors.push("Error: the net did not return its `IO` handle"); + if debug_hint { + errors.push(" hint: try running the program in `--debug` mode to see error messages"); + } + } + if *vicious { + errors.push("Error: the net created a vicious circle"); + } + if *ext_copy { + errors.push("Error: a linear extrinsic was copied"); + } + if *ext_erase { + errors.push("Error: a linear extrinsic was erased"); + } + if *ext_generic { + errors.push("Error: an extrinsic function encountered an unspecified error"); + } + if *invalid_interaction { + errors.push("Error: an invalid interaction occurred"); + } + + errors.join("\n\n") + } } From e90c8ed0fcac5414b9b9ae281bd441ec40c5ded1 Mon Sep 17 00:00:00 2001 From: Enrico Zandomeni Borba Date: Wed, 6 May 2026 12:57:07 +0200 Subject: [PATCH 10/10] lsp target_arch = "wasm32" cfgs --- lsp/Cargo.toml | 7 ++++++- lsp/src/lib.rs | 13 +++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/lsp/Cargo.toml b/lsp/Cargo.toml index 7576eb85f..7570149d7 100644 --- a/lsp/Cargo.toml +++ b/lsp/Cargo.toml @@ -5,10 +5,15 @@ edition = "2024" [dependencies] futures = "0.3.31" -tokio = { version = "1.41.1", features = ["full"] } tower-lsp = "0.20.0" vine = { path = "../vine" } vine-util = { path = "../util" } +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { version = "1.41.1", features = ["full"] } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +tokio = { version = "1.41.1", default-features = false, features = ["rt", "macros", "sync"] } + [lints.clippy] absolute_paths = "warn" diff --git a/lsp/src/lib.rs b/lsp/src/lib.rs index c1b0a0e4f..2da25214c 100644 --- a/lsp/src/lib.rs +++ b/lsp/src/lib.rs @@ -1,10 +1,8 @@ use std::{ collections::{BTreeMap, HashMap}, - env::current_dir, fs, path::{Path, PathBuf}, sync::Arc, - time::Instant, }; use futures::{StreamExt, stream::FuturesUnordered}; @@ -51,10 +49,8 @@ impl Backend { } } - let start = Instant::now(); self.hooks.pre_check(lsp.compiler.checkpoint()); _ = lsp.compiler.check(()); - eprintln!("compiled in {:?}", start.elapsed()); let mut diags_by_file = HashMap::, Vec<&Diag>)>::new(); for diag in &lsp.compiler.diags.errors { @@ -235,12 +231,20 @@ impl Lsp { (defs.len() == 1).then(|| (span, *defs.iter().next().unwrap())) } + #[cfg(not(target_arch = "wasm32"))] fn file_to_uri(&self, file: FileId) -> Url { + use std::env::current_dir; + let mut path = current_dir().unwrap(); path.push(&self.file_paths[file]); Url::from_file_path(&path).unwrap() } + #[cfg(target_arch = "wasm32")] + fn file_to_uri(&self, file: FileId) -> Url { + Url::parse(&format!("file://{}", self.file_paths[file].display())).unwrap() + } + fn uri_to_file_id(&self, uri: Url) -> Option { Some(self.file_paths.iter().find(|(_, path)| **path == AsRef::::as_ref(&uri.path()))?.0) } @@ -445,6 +449,7 @@ pub trait Hooks: Send + Sync + 'static { } #[allow(clippy::absolute_paths)] +#[cfg(not(target_arch = "wasm32"))] #[tokio::main] pub async fn lsp_stdio( compiler: Compiler,