difftreelog
feat replace nix eval with repl
in: trunk
2 files changed
cmds/fleet/src/better_nix_eval.rsdiffbeforeafterboth--- /dev/null
+++ b/cmds/fleet/src/better_nix_eval.rs
@@ -0,0 +1,469 @@
+use std::ffi::{OsStr, OsString};
+use std::process::Stdio;
+use std::sync::{Arc, Mutex, OnceLock};
+
+use abort_on_drop::ChildTask;
+use anyhow::{anyhow, bail, ensure, Context, Result};
+use futures::StreamExt;
+use r2d2::{Pool, PooledConnection};
+use serde::de::DeserializeOwned;
+use serde::Deserialize;
+use tokio::io::AsyncWriteExt;
+use tokio::process::{ChildStdin, ChildStdout, Command};
+use tokio::sync::oneshot;
+use tokio_util::codec::{FramedRead, LinesCodec};
+use tracing::debug;
+
+use crate::command::{process_child_stderr, ErrorRecorder, ErrorRecorderT, NixHandler};
+
+const REPL_DELIMITER: &str = "\"FLEET_MAGIC_REPL_DELIMITER\"";
+// To synchronize stderr and stdout. It works, yet I hate this.
+// There is no other way to catch errors, because they are coming from different streams, and they are not synchronized in tokio.
+const ERROR_DELIMITER: &str = "FLEET_MAGIC_ERROR_DELIMITER";
+
+pub struct NixSessionInner {
+ full_delimiter: String,
+ #[allow(dead_code)]
+ stderr_handler: ChildTask<Result<()>>,
+ error_recorder: ErrorRecorderT,
+ read: FramedRead<ChildStdout, LinesCodec>,
+ stdin: ChildStdin,
+ string_wrapping: (String, String),
+ number_wrapping: (String, String),
+ error_delimiter: String,
+
+ next_id: u32,
+ free_list: Vec<u32>,
+}
+const TRAIN_STRING: &str = "\"TRAIN_STRING\"";
+const TRAIN_NUMBER: &str = "13141516";
+
+struct ErrorRecorderHandle {
+ handle: ErrorRecorderT,
+}
+impl ErrorRecorderHandle {}
+impl Drop for ErrorRecorderHandle {
+ fn drop(&mut self) {
+ let mut recorded = self.handle.lock().unwrap();
+ assert!(recorded.is_some(), "exclusive");
+ *recorded = None;
+ }
+}
+
+struct ErrorCollector {
+ collected: Arc<Mutex<Vec<String>>>,
+ delim: String,
+ got_delim: Option<oneshot::Sender<()>>,
+}
+impl ErrorRecorder for ErrorCollector {
+ fn push_message(&mut self, msg: &str) -> bool {
+ if msg == self.delim {
+ let _ = self.got_delim
+ .take()
+ .expect("error delim is only expected once")
+ .send(());
+ return true;
+ }
+ let Some(msg) = msg.strip_prefix("@nix ") else {
+ return false;
+ };
+ #[derive(Deserialize)]
+ struct ErrorAction {
+ action: String,
+ level: u32,
+ msg: String,
+ }
+ let Ok(act) = serde_json::from_str::<ErrorAction>(msg) else {
+ return false;
+ };
+ if act.action != "msg" || act.level != 0 {
+ return false;
+ }
+ self.collected.lock().unwrap().push(act.msg);
+ true
+ }
+}
+
+impl NixSessionInner {
+ async fn new(flake: &OsStr, extra_args: impl IntoIterator<Item = &OsStr>) -> Result<Self> {
+ let mut cmd = Command::new("nix");
+ cmd.arg("repl")
+ .arg(flake)
+ .arg("--log-format")
+ .arg("internal-json");
+ for arg in extra_args {
+ cmd.arg(arg);
+ }
+ cmd.stdin(Stdio::piped());
+ cmd.stdout(Stdio::piped());
+ cmd.stderr(Stdio::piped());
+ let cmd = cmd.spawn()?;
+ let stdout = cmd.stdout.unwrap();
+ let stderr = cmd.stderr.unwrap();
+ let mut stdin = cmd.stdin.unwrap();
+ let error_recorder = ErrorRecorderT::default();
+ let err_recorder = error_recorder.clone();
+ let stderr_handler = abort_on_drop::ChildTask::from(tokio::spawn(async move {
+ let mut handler = NixHandler::default();
+ process_child_stderr(stderr, &mut handler, err_recorder).await
+ }));
+ // Standard repl hello doesn't work with internal-json logger
+ stdin.write_all(REPL_DELIMITER.as_bytes()).await?;
+ stdin.write_all(b"\n").await?;
+ stdin.flush().await?;
+ let mut read = FramedRead::new(stdout, LinesCodec::new());
+ let mut full_delimiter = None;
+ while let Some(line) = read.next().await {
+ let line = line?;
+ if line.contains(REPL_DELIMITER) {
+ debug!("discovered repl delimiter with added colors: {line}");
+ full_delimiter = Some(line.to_owned());
+ break;
+ }
+ }
+ let Some(full_delimiter) = full_delimiter else {
+ bail!("failed to discover delimiter");
+ };
+ let mut res = Self {
+ full_delimiter,
+ error_delimiter: "[[filled after training]]".to_owned(),
+ stderr_handler,
+ error_recorder,
+ read,
+ stdin,
+ string_wrapping: Default::default(),
+ number_wrapping: Default::default(),
+
+ next_id: 0,
+ free_list: vec![],
+ };
+ res.train().await?;
+ Ok(res)
+ }
+ async fn train(&mut self) -> Result<()> {
+ {
+ let full_string = self.execute_expression_raw(TRAIN_STRING).await?;
+ let string_offset = full_string.find(TRAIN_STRING).expect("contained");
+ let string_prefix = &full_string[..string_offset];
+ let string_suffix = &full_string[string_offset + TRAIN_STRING.len()..];
+ self.string_wrapping = (string_prefix.to_owned(), string_suffix.to_owned());
+ }
+ {
+ let full_number = self.execute_expression_raw(TRAIN_NUMBER).await?;
+ let number_offset = full_number.find(TRAIN_NUMBER).expect("contained");
+ let number_prefix = &full_number[..number_offset];
+ let number_suffix = &full_number[number_offset + TRAIN_NUMBER.len()..];
+ self.number_wrapping = (number_prefix.to_owned(), number_suffix.to_owned());
+ }
+ {
+ struct TrainingErrorCollector(Option<oneshot::Sender<String>>);
+ impl ErrorRecorder for TrainingErrorCollector {
+ fn push_message(&mut self, msg: &str) -> bool {
+ if msg.contains(ERROR_DELIMITER) {
+ let _ = self
+ .0
+ .take()
+ .expect("error delimiter is sent once")
+ .send(msg.to_owned());
+ }
+ true
+ }
+ }
+ let (tx, rx) = oneshot::channel();
+ let _handle = self.record_error(TrainingErrorCollector(Some(tx)));
+ self.send_command(ERROR_DELIMITER).await?;
+ self.send_command(REPL_DELIMITER).await?;
+ self.read_until_delimiter().await?;
+ let msg = rx.await?;
+ self.error_delimiter = msg;
+ }
+ Ok(())
+ }
+ fn record_error(&mut self, v: impl ErrorRecorder + 'static) -> ErrorRecorderHandle {
+ {
+ let mut recorder = self.error_recorder.lock().unwrap();
+ assert!(recorder.is_none(), "recorder is already started");
+ *recorder = Some(Box::new(v));
+ }
+ ErrorRecorderHandle {
+ handle: self.error_recorder.clone(),
+ }
+ }
+ async fn send_command(&mut self, cmd: impl AsRef<[u8]>) -> Result<()> {
+ self.stdin.write_all(cmd.as_ref()).await?;
+ self.stdin.write_all(b"\n").await?;
+ Ok(())
+ }
+ async fn read_until_delimiter(&mut self) -> Result<String> {
+ let mut out = String::new();
+ while let Some(line) = self.read.next().await {
+ let line = line?;
+ if line == self.full_delimiter {
+ return Ok(out);
+ }
+ if !out.is_empty() {
+ out.push('\n');
+ }
+ out.push_str(&line);
+ }
+ bail!("didn't reached delimiter");
+ }
+ async fn execute_expression_number(&mut self, expr: impl AsRef<[u8]>) -> Result<u64> {
+ let num = self.number_wrapping.clone();
+ let n = self.execute_expression_wrapping(expr, &num).await?;
+ Ok(n.parse::<u64>()?)
+ }
+ async fn execute_expression_string(&mut self, expr: impl AsRef<[u8]>) -> Result<String> {
+ let num = self.string_wrapping.clone();
+ let n = self.execute_expression_wrapping(expr, &num).await?;
+ let str: String = serde_json::from_str(&n)?;
+ Ok(str)
+ }
+ async fn execute_expression_to_json<V: DeserializeOwned>(
+ &mut self,
+ expr: impl AsRef<[u8]>,
+ ) -> Result<V> {
+ let mut fexpr = b"builtins.toJSON (".to_vec();
+ fexpr.extend_from_slice(expr.as_ref());
+ fexpr.push(b')');
+ let v = self.execute_expression_string(fexpr).await?;
+ Ok(serde_json::from_str(&v)?)
+ }
+ async fn execute_expression_wrapping(
+ &mut self,
+ expr: impl AsRef<[u8]>,
+ wrapping: &(String, String),
+ ) -> Result<String> {
+ let collected = Arc::new(Mutex::new(vec![]));
+ let (etx, erx) = oneshot::channel();
+ let _collector = self.record_error(ErrorCollector{collected:collected.clone(), delim: self.error_delimiter.clone(), got_delim: Some(etx)});
+ let res = self.execute_expression_raw(expr).await?;
+ let _ = self.execute_expression_raw(ERROR_DELIMITER).await?;
+ let _ = erx.await;
+ if res.is_empty() {
+ let c = collected.lock().unwrap();
+ if c.is_empty() {
+ bail!("expected expression, got nothing")
+ }
+ bail!("{}", c.join("\n"));
+ }
+ drop(_collector);
+ let Some(res) = res.strip_prefix(&wrapping.0) else {
+ bail!("invalid type")
+ };
+ let Some(res) = res.strip_suffix(&wrapping.1) else {
+ bail!("invalid type")
+ };
+ Ok(res.to_owned())
+ }
+ async fn execute_expression_empty(&mut self, expr: impl AsRef<[u8]>) -> Result<()> {
+ let collected = Arc::new(Mutex::new(vec![]));
+ let (etx, erx) = oneshot::channel();
+ let _collector = self.record_error(ErrorCollector{collected:collected.clone(), delim: self.error_delimiter.clone(), got_delim: Some(etx)});
+ let v = self.execute_expression_raw(expr).await?;
+ let _ = self.execute_expression_raw(ERROR_DELIMITER).await;
+ let _ = erx.await;
+
+ let c = collected.lock().unwrap();
+ if !c.is_empty() {
+ bail!("{}", c.join("\n"));
+ }
+ ensure!(v.is_empty(), "unexpected expression result");
+ Ok(())
+ }
+ async fn execute_expression_raw(&mut self, expr: impl AsRef<[u8]>) -> Result<String> {
+ self.send_command(expr).await?;
+ // It will be echoed
+ self.send_command(REPL_DELIMITER).await?;
+ self.read_until_delimiter().await
+ }
+ async fn execute_assign(&mut self, expr: impl AsRef<str>) -> Result<u32> {
+ let id = self.allocate_id();
+ self.execute_expression_empty(format!("sess_field_{id} = {}", expr.as_ref()))
+ .await?;
+ Ok(id)
+ }
+
+ /// Id should be immediately used
+ fn allocate_id(&mut self) -> u32 {
+ if let Some(free) = self.free_list.pop() {
+ free
+ } else {
+ let v = self.next_id;
+ self.next_id += 1;
+ v
+ }
+ }
+ // Nix has no way to deallocate variable, yet GC will correct everything not reachable.
+ // async fn free_id(&mut self, id: u32) -> Result<()> {
+ // self.execute_expression_empty(format!("sess_field_{id} = null"))
+ // .await?;
+ // self.free_list.push(id);
+ // Ok(())
+ // }
+}
+
+#[derive(Clone)]
+pub struct NixSession(Arc<tokio::sync::Mutex<PooledConnection<NixSessionPoolInner>>>);
+
+#[derive(Clone, Debug)]
+enum Index {
+ String(String),
+ // Idx(u32),
+}
+pub struct Field {
+ full_path: Vec<Index>,
+ session: NixSession,
+ value: Option<u32>,
+}
+impl Field {
+ fn root(session: NixSession) -> Self {
+ Self {
+ full_path: vec![],
+ session,
+ value: None,
+ }
+ }
+ pub async fn field(session: NixSession, field: &str) -> Result<Self> {
+ Self::root(session).get_field_deep([field]).await
+ }
+ pub async fn get_field(&self, name: &str) -> Result<Self> {
+ self.get_field_deep([name]).await
+ }
+ pub async fn get_field_deep<'a>(
+ &self,
+ name: impl IntoIterator<Item = &'a str>,
+ ) -> Result<Self> {
+ let mut iter = name.into_iter();
+
+ let mut full_path = self.full_path.clone();
+ let mut query = if let Some(id) = self.value {
+ format!("sess_field_{id}")
+ } else {
+ let first = iter.next().expect("name not empty");
+ ensure!(
+ !(first.contains('.') | first.contains(' ')),
+ "bad name for root query: {first}"
+ );
+ full_path.push(Index::String(first.to_string()));
+ first.to_string()
+ };
+ for v in iter {
+ full_path.push(Index::String(v.to_string()));
+ // Escape
+ let escaped = nixlike::serialize(v)?;
+ let escaped = escaped.trim();
+ query.push('.');
+ query.push_str(escaped);
+ }
+
+ let vid = self
+ .session
+ .0
+ .lock()
+ .await
+ .execute_assign(&query)
+ .await
+ .with_context(|| format!("full path: {:?}", full_path))?;
+ Ok(Self {
+ full_path,
+ session: self.session.clone(),
+ value: Some(vid),
+ })
+ }
+ pub async fn as_json<V: DeserializeOwned>(&self) -> Result<V> {
+ let id = self.value.expect("can't serialize root field");
+ self.session
+ .0
+ .lock()
+ .await
+ .execute_expression_to_json(&format!("sess_field_{id}"))
+ .await
+ .with_context(|| format!("full path: {:?}", self.full_path))
+ }
+ pub async fn list_fields(&self) -> Result<Vec<String>> {
+ let id = self.value.expect("can't list root fields");
+ self.session
+ .0
+ .lock()
+ .await
+ .execute_expression_to_json(&format!("builtins.attrNames sess_field_{id}"))
+ .await
+ .with_context(|| format!("full path: {:?}", self.full_path))
+ }
+}
+impl Drop for Field {
+ fn drop(&mut self) {
+ if let Some(id) = self.value {
+ if let Ok(mut lock) = self.session.0.try_lock() {
+ lock.free_list.push(id)
+ }
+ // Leaked
+ }
+ }
+}
+struct NixSessionPoolInner {
+ flake: OsString,
+ nix_args: Vec<OsString>,
+}
+
+#[derive(Debug)]
+pub struct NixPoolError(anyhow::Error);
+impl From<anyhow::Error> for NixPoolError {
+ fn from(value: anyhow::Error) -> Self {
+ Self(value)
+ }
+}
+impl std::error::Error for NixPoolError {}
+impl std::fmt::Display for NixPoolError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+impl r2d2::ManageConnection for NixSessionPoolInner {
+ type Connection = NixSessionInner;
+ type Error = NixPoolError;
+ fn connect(&self) -> std::result::Result<Self::Connection, Self::Error> {
+ let _v = TOKIO_RUNTIME
+ .get()
+ .expect("missed tokio runtime init!")
+ .enter();
+ Ok(futures::executor::block_on(NixSessionInner::new(
+ self.flake.as_os_str(),
+ self.nix_args.iter().map(OsString::as_os_str),
+ ))?)
+ }
+
+ fn is_valid(&self, conn: &mut Self::Connection) -> std::result::Result<(), Self::Error> {
+ let _v = TOKIO_RUNTIME
+ .get()
+ .expect("missed tokio runtime init!")
+ .enter();
+ let res = futures::executor::block_on(conn.execute_expression_number("2 + 2"))?;
+ if res != 4 {
+ return Err(anyhow!("sanity check failed").into());
+ };
+ Ok(())
+ }
+
+ fn has_broken(&self, _conn: &mut Self::Connection) -> bool {
+ false
+ }
+}
+pub struct NixSessionPool(Pool<NixSessionPoolInner>);
+impl NixSessionPool {
+ pub async fn new(flake: OsString, nix_args: Vec<OsString>) -> Result<Self> {
+ let inner = tokio::task::block_in_place(|| {
+ r2d2::Builder::<NixSessionPoolInner>::new()
+ .min_idle(Some(0))
+ .build(NixSessionPoolInner { flake, nix_args })
+ })?;
+ Ok(Self(inner))
+ }
+ pub async fn get(&self) -> Result<NixSession> {
+ let v = tokio::task::block_in_place(|| self.0.get())?;
+ Ok(NixSession(Arc::new(tokio::sync::Mutex::new(v))))
+ }
+}
+
+pub static TOKIO_RUNTIME: OnceLock<tokio::runtime::Handle> = OnceLock::new();
cmds/fleet/src/nix_eval.rsdiffbeforeafterboth1//! Calling nix eval for everything is slow, it is not easy to link nix evaluator itself,2//! and tvix-nix doesn't have proper flake support. Fleets solution: automating nix repl calls.3//!4//! Api is synchronous, yet it is good enough with pooling, and in environment without IFDs for using5//! those blocking calls from async code.67use std::borrow::Cow;8use std::sync::{Arc, Mutex};9use std::time::Instant;1011use anyhow::{anyhow, bail, ensure, Context, Result};12use itertools::Itertools;13use r2d2::PooledConnection;14use rexpect::session::{PtyReplSession, PtySession};15use serde::de::DeserializeOwned;16use std::ffi::OsString;17use tracing::info_span;1819fn parse_error(res: &str) -> Option<String> {20 let res = if let Some(v) = res.strip_prefix("error: ") {21 if let Some((first_line, next)) = v.split_once('\n') {22 format!("{first_line}\n{}", unindent::unindent(next))23 } else {24 v.trim_start().to_owned()25 }26 } else if let Some(v) = res.strip_prefix("error:\n") {27 let mut v = v.to_owned();28 v.insert(0, '\n');29 unindent::unindent(&v).trim_start().to_owned()30 } else {31 return None;32 };33 let res = res.trim_end();34 Some(35 res.replace('Â', "")36 .split('\n')37 .map(|l| l.strip_prefix("â\u{80}¦ ").unwrap_or(l))38 .join("\n"),39 )40}41pub struct NixSessionPool {42 pub flake: OsString,43 pub nix_args: Vec<OsString>,44}4546#[derive(Debug)]47pub struct NixPoolError(anyhow::Error);48impl From<anyhow::Error> for NixPoolError {49 fn from(value: anyhow::Error) -> Self {50 Self(value)51 }52}53impl std::error::Error for NixPoolError {}54impl std::fmt::Display for NixPoolError {55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {56 self.0.fmt(f)57 }58}5960impl r2d2::ManageConnection for NixSessionPool {61 type Connection = NixSession;62 type Error = NixPoolError;6364 fn connect(&self) -> std::result::Result<Self::Connection, Self::Error> {65 Ok(NixSession::new(&self.flake, &self.nix_args, None)?)66 }6768 fn is_valid(&self, conn: &mut Self::Connection) -> std::result::Result<(), Self::Error> {69 let res = conn.expression_result("2 + 2")?;70 if res != "4" {71 return Err(anyhow!("basic expression failed").into());72 }73 Ok(())74 }7576 fn has_broken(&self, conn: &mut Self::Connection) -> bool {77 conn.finished78 }79}8081pub struct NixSession {82 session: PtyReplSession,83 next_id: u32,84 free_list: Vec<u32>,85 finished: bool,86}87impl NixSession {88 fn new(flake: &OsString, args: &[OsString], timeout: Option<u64>) -> Result<Self> {89 let mut cmd = std::process::Command::new("nix");90 cmd.arg("repl");91 cmd.arg(flake);92 for arg in args {93 cmd.arg(arg);94 }95 cmd.env("TERM", "dumb");96 cmd.env("NO_COLOR", "1");97 let pty_session = rexpect::session::spawn_command(cmd, timeout)?;98 let mut repl = PtyReplSession {99 prompt: "nix-repl> ".to_string(),100 pty_session,101 quit_command: Some(":q".to_string()),102 echo_on: true,103 };104 repl.wait_for_prompt()?;105 Ok(Self {106 session: repl,107 next_id: 0,108 free_list: vec![],109 finished: false,110 })111 }112 fn expression_result(&mut self, cmd: &str) -> Result<String> {113 dbg!(cmd);114 self.session.send_line(cmd)?;115 dbg!("waiting");116 let result = self.session.wait_for_prompt()?;117 let result = strip_ansi_escapes::strip_str(&result);118 let result = result.trim();119 dbg!(result);120 Ok(result.to_owned())121 }122 fn json_result<V: DeserializeOwned>(&mut self, cmd: &str) -> Result<V> {123 let v = match self.expression_result(&format!("builtins.toJSON ({cmd})")) {124 Ok(v) => {125 if let Some(e) = parse_error(&v) {126 bail!("{e}")127 }128 v129 }130 Err(e) => {131 self.finished = true;132 bail!("{e}")133 }134 };135 // Remove outer quoting136 let v: String = serde_json::from_str(&v)?;137 Ok(serde_json::from_str(&v)?)138 }139 /// Id should be immediately used140 fn allocate_id(&mut self) -> u32 {141 if let Some(free) = self.free_list.pop() {142 free143 } else {144 let v = self.next_id;145 self.next_id += 1;146 v147 }148 }149 fn allocate_result(&mut self, cmd: &str) -> Result<u32> {150 let id = self.allocate_id();151 match self.expression_result(&format!("sess_field_{id} = ({cmd})")) {152 Ok(v) => {153 if let Some(e) = parse_error(&v) {154 self.free_list.push(id);155 bail!("{e}")156 }157 }158 Err(e) => {159 self.finished = true;160 }161 }162163 Ok(id)164 }165 /// Nix has no way to deallocate variable, yet GC will correct everything not reachable.166 fn free_id(&mut self, id: u32) {167 if let Err(e) = self.expression_result(&format!("sess_field_{id} = null")) {168 self.finished = true;169 } else {170 self.free_list.push(id)171 }172 }173}174175#[derive(Clone, Debug)]176enum Index {177 String(String),178 Idx(u32),179}180181pub struct Field {182 full_path: Vec<Index>,183 session: Arc<Mutex<PooledConnection<NixSessionPool>>>,184 value: Option<u32>,185}186impl Field {187 pub fn root(conn: PooledConnection<NixSessionPool>) -> Self {188 Self {189 full_path: vec![],190 session: Arc::new(Mutex::new(conn)),191 value: None,192 }193 }194 pub fn get_field_deep<'a>(&self, name: impl IntoIterator<Item = &'a str>) -> Result<Self> {195 let mut iter = name.into_iter();196197 let mut full_path = self.full_path.clone();198 let mut query = if let Some(id) = self.value {199 format!("sess_field_{id}")200 } else {201 let first = iter.next().expect("name not empty");202 ensure!(203 !(first.contains('.') | first.contains(' ')),204 "bad name for root query: {first}"205 );206 full_path.push(Index::String(first.to_string()));207 first.to_string()208 };209 for v in iter {210 full_path.push(Index::String(v.to_string()));211 // Escape212 let escaped = nixlike::serialize(v)?;213 let escaped = escaped.trim();214 query.push('.');215 query.push_str(escaped);216 }217218 let vid = self219 .session220 .lock()221 .unwrap()222 .allocate_result(&query)223 .with_context(|| format!("full path: {:?}", full_path))?;224 Ok(Self {225 full_path,226 session: self.session.clone(),227 value: Some(vid),228 })229 }230 pub fn get_field<'a>(&self, name: &str) -> Result<Self> {231 self.get_field_deep([name])232 }233 pub fn as_json<V: DeserializeOwned>(&self) -> Result<V> {234 let id = self.value.expect("can't serialize root field");235 self.session236 .lock()237 .unwrap()238 .json_result(&format!("sess_field_{id}"))239 .with_context(|| format!("full path: {:?}", self.full_path))240 }241 pub fn list_fields(&self) -> Result<Vec<String>> {242 let id = self.value.expect("can't list root fields");243 self.session244 .lock()245 .unwrap()246 .json_result(&format!("builtins.attrNames sess_field_{id}"))247 .with_context(|| format!("full path: {:?}", self.full_path))248 }249}250impl Drop for Field {251 fn drop(&mut self) {252 if let Some(id) = self.value {253 self.session.lock().unwrap().free_id(id)254 }255 }256}