use std::path::{Path, PathBuf};
use std::sync::Arc;
use wasmtime::component::{Component, HasSelf, Linker, ResourceTable};
use wasmtime::{Engine, Store};
use harness::pipeline::T0Fn;
use llm_lib::edit_plan::{AnchoredEdit, EditPlan, FileEditPlan};
use crate::engine::new_engine;
use crate::host_state::HostState;
use crate::wasi_ctx::read_only_repo_ctx;
mod bindings {
wasmtime::component::bindgen!({
path: "../../wit",
world: "t0-plugin",
});
}
impl From<bindings::n7n::fix_deps::types::AnchoredEdit> for AnchoredEdit {
fn from(e: bindings::n7n::fix_deps::types::AnchoredEdit) -> Self {
AnchoredEdit {
anchor: e.anchor,
line: e.line as usize,
new_text: e.new_text,
}
}
}
impl From<bindings::n7n::fix_deps::types::FileEditPlan> for FileEditPlan {
fn from(plan: bindings::n7n::fix_deps::types::FileEditPlan) -> Self {
use bindings::n7n::fix_deps::types::FileEditPlan as Wit;
match plan {
Wit::Anchored(a) => FileEditPlan::Anchored {
path: PathBuf::from(a.path),
edits: a.edits.into_iter().map(AnchoredEdit::from).collect(),
},
Wit::Create(c) => FileEditPlan::Create {
path: PathBuf::from(c.path),
content: c.content,
},
Wit::Delete(d) => FileEditPlan::Delete {
path: PathBuf::from(d.path),
},
}
}
}
impl From<bindings::exports::n7n::fix_deps::t0::EditPlan> for EditPlan {
fn from(plan: bindings::exports::n7n::fix_deps::t0::EditPlan) -> Self {
EditPlan {
files: plan.files.into_iter().map(FileEditPlan::from).collect(),
}
}
}
impl bindings::n7n::fix_deps::host_fs::Host for HostState {
fn read_file(&mut self, path: String) -> Result<String, String> {
std::fs::read_to_string(self.repo_root.join(path)).map_err(|e| e.to_string())
}
}
pub struct WasmT0 {
engine: Engine,
component: Component,
linker: Linker<HostState>,
repo_root: PathBuf,
}
impl WasmT0 {
pub fn from_file(
repo_root: impl Into<PathBuf>,
wasm_path: impl AsRef<Path>,
) -> wasmtime::Result<Self> {
let engine = new_engine()?;
let component = Component::from_file(&engine, wasm_path.as_ref())?;
let mut linker = Linker::<HostState>::new(&engine);
wasmtime_wasi::p2::add_to_linker_sync(&mut linker)?;
bindings::n7n::fix_deps::host_fs::add_to_linker::<_, HasSelf<HostState>>(
&mut linker,
|s| s,
)?;
Ok(Self {
engine,
component,
linker,
repo_root: repo_root.into(),
})
}
fn call(&self) -> wasmtime::Result<EditPlan> {
let wasi = read_only_repo_ctx(&self.repo_root)?;
let mut store = Store::new(
&self.engine,
HostState {
wasi,
table: ResourceTable::new(),
repo_root: self.repo_root.clone(),
},
);
let instance = bindings::T0Plugin::instantiate(&mut store, &self.component, &self.linker)?;
let plan = instance.n7n_fix_deps_t0().call_plan(&mut store)?;
Ok(EditPlan::from(plan))
}
#[must_use]
pub fn into_t0_fn(self) -> T0Fn {
let this = Arc::new(self);
Arc::new(move || {
let this = Arc::clone(&this);
Box::pin(async move {
this.call().unwrap_or_else(|err| {
eprintln!("wasm-компонент t0: {err}");
EditPlan::empty()
})
})
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bindgen_edit_plan_converts_all_three_file_edit_plan_variants() {
let wit_plan = bindings::exports::n7n::fix_deps::t0::EditPlan {
files: vec![
bindings::n7n::fix_deps::types::FileEditPlan::Anchored(
bindings::n7n::fix_deps::types::AnchoredFileEdit {
path: "src/a.rs".to_string(),
edits: vec![bindings::n7n::fix_deps::types::AnchoredEdit {
anchor: "1a2".to_string(),
line: 3,
new_text: Some("новий рядок".to_string()),
}],
},
),
bindings::n7n::fix_deps::types::FileEditPlan::Create(
bindings::n7n::fix_deps::types::CreateFileEdit {
path: "src/b.rs".to_string(),
content: "вміст\n".to_string(),
},
),
bindings::n7n::fix_deps::types::FileEditPlan::Delete(
bindings::n7n::fix_deps::types::DeleteFileEdit {
path: "src/c.rs".to_string(),
},
),
],
};
let plan = EditPlan::from(wit_plan);
assert_eq!(
plan,
EditPlan {
files: vec![
FileEditPlan::Anchored {
path: PathBuf::from("src/a.rs"),
edits: vec![AnchoredEdit {
anchor: "1a2".to_string(),
line: 3,
new_text: Some("новий рядок".to_string()),
}],
},
FileEditPlan::Create {
path: PathBuf::from("src/b.rs"),
content: "вміст\n".to_string(),
},
FileEditPlan::Delete {
path: PathBuf::from("src/c.rs"),
},
],
}
);
}
#[test]
fn bindgen_anchored_edit_deletion_sentinel_survives_conversion() {
let wit_edit = bindings::n7n::fix_deps::types::AnchoredEdit {
anchor: "xyz".to_string(),
line: 7,
new_text: None,
};
assert_eq!(AnchoredEdit::from(wit_edit).new_text, None);
}
}