From 6bcec165e6029be4074d1350f783a2a6bf7fb852 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Tue, 19 Apr 2022 17:44:50 -0400 Subject: [PATCH] feat(dap): implement DAP debugger This allows debugging from within VSCode (or other IDEs). --- Cargo.lock | 15 +- Cargo.toml | 6 - src/dap/codec.rs | 399 ---------------------------- src/dap/mod.rs | 310 +++------------------ src/frontend/cli.rs | 8 +- src/lsp/clarity_language_backend.rs | 2 +- src/runnner/deno.rs | 4 +- 7 files changed, 55 insertions(+), 689 deletions(-) delete mode 100644 src/dap/codec.rs diff --git a/Cargo.lock b/Cargo.lock index 8d1d39c22..46007d537 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -679,14 +679,12 @@ dependencies = [ "bitcoincore-rpc-json", "block-modes", "bollard", - "bytes", "chrono", "clap 3.1.6", "clap_generate", "clarity-repl", "crossterm 0.22.1", "ctrlc", - "dap-types", "deno", "deno_core", "deno_runtime", @@ -696,14 +694,11 @@ dependencies = [ "fwdansi", "hex", "hmac 0.12.1", - "httparse", "indexmap", "lazy_static", "libc", "libsecp256k1 0.7.0", - "log", "mac_address", - "memchr", "pbkdf2", "percent-encoding", "pin-project", @@ -724,7 +719,6 @@ dependencies = [ "text-size", "tiny-hderive", "tokio", - "tokio-util 0.7.1", "toml", "tower-lsp", "tracing", @@ -739,15 +733,19 @@ dependencies = [ [[package]] name = "clarity-repl" version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fe12cd50f77ccfbbbfccef7a295f93f1ba8aa8d83a18ca325d9b638eea281c" dependencies = [ "ansi_term", "atty", + "bytes", + "dap-types", + "futures", "getrandom 0.2.6", + "httparse", "integer-sqrt", "lazy_static", "libsecp256k1 0.5.0", + "log", + "memchr", "pico-args 0.4.2", "prettytable-rs", "rand 0.7.3", @@ -764,6 +762,7 @@ dependencies = [ "sha2 0.9.9", "sha3", "tokio", + "tokio-util 0.7.1", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1fa69458c..35ae4edb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,12 +81,6 @@ mac_address = { version = "1.1.2", optional = true } tower-lsp = { version = "0.14.0", optional = true } hex = "0.4.3" dirs = "4.0.0" -tokio-util = { version = "0.7.1", features = ["codec"] } -httparse = "1.6.0" -bytes = "1.1.0" -log = "0.4.16" -memchr = "2.4.1" -dap_types = { package = "dap-types", path = "../dap-types" } [dependencies.tui] version = "0.16.0" diff --git a/src/dap/codec.rs b/src/dap/codec.rs deleted file mode 100644 index 5f80def58..000000000 --- a/src/dap/codec.rs +++ /dev/null @@ -1,399 +0,0 @@ -//! Encoder and decoder for Debug Adapter Protocol messages. - -use std::error::Error; -use std::fmt::{self, Display, Formatter}; -use std::fs::{File, OpenOptions}; -use std::io::{Error as IoError, Write}; -use std::marker::PhantomData; -use std::num::ParseIntError; -use std::str::Utf8Error; - -use bytes::buf::BufMut; -use bytes::{Buf, BytesMut}; -use log::{trace, warn}; -use memchr::memmem; -use serde::{de::DeserializeOwned, Serialize}; -use tokio_util::codec::{Decoder, Encoder}; - -/// Errors that can occur when processing a DAP message. -#[derive(Debug)] -pub enum ParseError { - /// Failed to parse the JSON body. - Body(serde_json::Error), - /// Failed to encode the response. - Encode(IoError), - /// Failed to parse headers. - Headers(httparse::Error), - /// The media type in the `Content-Type` header is invalid. - InvalidContentType, - /// The length value in the `Content-Length` header is invalid. - InvalidContentLength(ParseIntError), - /// Request lacks the required `Content-Length` header. - MissingContentLength, - /// Request contains invalid UTF8. - Utf8(Utf8Error), -} - -impl Display for ParseError { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match *self { - ParseError::Body(ref e) => write!(f, "unable to parse JSON body: {}", e), - ParseError::Encode(ref e) => write!(f, "failed to encode response: {}", e), - ParseError::Headers(ref e) => write!(f, "failed to parse headers: {}", e), - ParseError::InvalidContentType => write!(f, "unable to parse content type"), - ParseError::InvalidContentLength(ref e) => { - write!(f, "unable to parse content length: {}", e) - } - ParseError::MissingContentLength => { - write!(f, "missing required `Content-Length` header") - } - ParseError::Utf8(ref e) => write!(f, "request contains invalid UTF8: {}", e), - } - } -} - -impl Error for ParseError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - match *self { - ParseError::Body(ref e) => Some(e), - ParseError::Encode(ref e) => Some(e), - ParseError::InvalidContentLength(ref e) => Some(e), - ParseError::Utf8(ref e) => Some(e), - _ => None, - } - } -} - -impl From for ParseError { - fn from(error: serde_json::Error) -> Self { - ParseError::Body(error) - } -} - -impl From for ParseError { - fn from(error: IoError) -> Self { - ParseError::Encode(error) - } -} - -impl From for ParseError { - fn from(error: httparse::Error) -> Self { - ParseError::Headers(error) - } -} - -impl From for ParseError { - fn from(error: ParseIntError) -> Self { - ParseError::InvalidContentLength(error) - } -} - -impl From for ParseError { - fn from(error: Utf8Error) -> Self { - ParseError::Utf8(error) - } -} - -/// Encodes and decodes Language Server Protocol messages. -pub struct DebugAdapterCodec { - content_len: Option, - _marker: PhantomData, -} - -impl Default for DebugAdapterCodec { - fn default() -> Self { - DebugAdapterCodec { - content_len: None, - _marker: PhantomData, - } - } -} - -impl Encoder for DebugAdapterCodec { - type Error = ParseError; - - fn encode(&mut self, item: T, dst: &mut BytesMut) -> Result<(), Self::Error> { - let msg = serde_json::to_string(&item)?; - trace!("-> {}", msg); - - // Reserve just enough space to hold the `Content-Length: ` and `\r\n\r\n` constants, - // the length of the message, and the message body. - dst.reserve(msg.len() + number_of_digits(msg.len()) + 20); - let mut writer = dst.writer(); - write!(writer, "Content-Length: {}\r\n\r\n{}", msg.len(), msg)?; - writer.flush()?; - - Ok(()) - } -} - -#[inline] -fn number_of_digits(mut n: usize) -> usize { - let mut num_digits = 0; - - while n > 0 { - n /= 10; - num_digits += 1; - } - - num_digits -} - -impl Decoder for DebugAdapterCodec { - type Item = T; - type Error = ParseError; - - fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { - if let Some(content_len) = self.content_len { - if src.len() < content_len { - return Ok(None); - } - - let bytes = &src[..content_len]; - let message = std::str::from_utf8(bytes)?; - - let result = if message.is_empty() { - Ok(None) - } else { - let mut file = OpenOptions::new() - .write(true) - .append(true) - .open("/Users/brice/work/debugger-demo/debugger.txt") - .unwrap(); - writeln!(file, "---> {}", message); - trace!("<- {}", message); - match serde_json::from_str(message) { - Ok(parsed) => Ok(Some(parsed)), - Err(err) => Err(err.into()), - } - }; - - src.advance(content_len); - self.content_len = None; // Reset state in preparation for parsing next message. - - result - } else { - let mut dst = [httparse::EMPTY_HEADER; 2]; - - let (headers_len, headers) = match httparse::parse_headers(src, &mut dst)? { - httparse::Status::Complete(output) => output, - httparse::Status::Partial => return Ok(None), - }; - - match decode_headers(headers) { - Ok(content_len) => { - src.advance(headers_len); - self.content_len = Some(content_len); - self.decode(src) // Recurse right back in, now that `Content-Length` is known. - } - Err(err) => { - match err { - ParseError::MissingContentLength => {} - _ => src.advance(headers_len), - } - - // Skip any garbage bytes by scanning ahead for another potential message. - src.advance(memmem::find(src, b"Content-Length").unwrap_or_default()); - Err(err) - } - } - } - } -} - -fn decode_headers(headers: &[httparse::Header<'_>]) -> Result { - let mut content_len = None; - - for header in headers { - match header.name { - "Content-Length" => { - let string = std::str::from_utf8(header.value)?; - let parsed_len = string.parse()?; - content_len = Some(parsed_len); - } - "Content-Type" => { - let string = std::str::from_utf8(header.value)?; - let charset = string - .split(';') - .skip(1) - .map(|param| param.trim()) - .find_map(|param| param.strip_prefix("charset=")); - - match charset { - Some("utf-8") | Some("utf8") => {} - _ => return Err(ParseError::InvalidContentType), - } - } - other => warn!("encountered unsupported header: {:?}", other), - } - } - - if let Some(content_len) = content_len { - Ok(content_len) - } else { - Err(ParseError::MissingContentLength) - } -} - -#[cfg(test)] -mod tests { - use bytes::BytesMut; - use serde_json::Value; - - use super::*; - - macro_rules! assert_err { - ($expression:expr, $($pattern:tt)+) => { - match $expression { - $($pattern)+ => (), - ref e => panic!("expected `{}` but got `{:?}`", stringify!($($pattern)+), e), - } - } - } - - fn encode_message(content_type: Option<&str>, message: &str) -> String { - let content_type = content_type - .map(|ty| format!("\r\nContent-Type: {}", ty)) - .unwrap_or_default(); - - format!( - "Content-Length: {}{}\r\n\r\n{}", - message.len(), - content_type, - message - ) - } - - #[test] - fn encode_and_decode() { - let decoded = r#"{"jsonrpc":"2.0","method":"exit"}"#; - let encoded = encode_message(None, decoded); - - let mut codec = DebugAdapterCodec::default(); - let mut buffer = BytesMut::new(); - let item: Value = serde_json::from_str(decoded).unwrap(); - codec.encode(item, &mut buffer).unwrap(); - assert_eq!(buffer, BytesMut::from(encoded.as_str())); - - let mut buffer = BytesMut::from(encoded.as_str()); - let message = codec.decode(&mut buffer).unwrap(); - let decoded = serde_json::from_str(decoded).unwrap(); - assert_eq!(message, Some(decoded)); - } - - #[test] - fn decodes_optional_content_type() { - let decoded = r#"{"jsonrpc":"2.0","method":"exit"}"#; - let content_type = "application/vscode-jsonrpc; charset=utf-8"; - let encoded = encode_message(Some(content_type), decoded); - - let mut codec = DebugAdapterCodec::default(); - let mut buffer = BytesMut::from(encoded.as_str()); - let message = codec.decode(&mut buffer).unwrap(); - let decoded_: Value = serde_json::from_str(decoded).unwrap(); - assert_eq!(message, Some(decoded_)); - - let content_type = "application/vscode-jsonrpc; charset=utf8"; - let encoded = encode_message(Some(content_type), decoded); - - let mut buffer = BytesMut::from(encoded.as_str()); - let message = codec.decode(&mut buffer).unwrap(); - let decoded_: Value = serde_json::from_str(decoded).unwrap(); - assert_eq!(message, Some(decoded_)); - - let content_type = "application/vscode-jsonrpc; charset=invalid"; - let encoded = encode_message(Some(content_type), decoded); - - let mut buffer = BytesMut::from(encoded.as_str()); - assert_err!( - codec.decode(&mut buffer), - Err(ParseError::InvalidContentType) - ); - - let content_type = "application/vscode-jsonrpc"; - let encoded = encode_message(Some(content_type), decoded); - - let mut buffer = BytesMut::from(encoded.as_str()); - assert_err!( - codec.decode(&mut buffer), - Err(ParseError::InvalidContentType) - ); - - let content_type = "this-mime-should-be-ignored; charset=utf8"; - let encoded = encode_message(Some(content_type), decoded); - - let mut buffer = BytesMut::from(encoded.as_str()); - let message = codec.decode(&mut buffer).unwrap(); - let decoded_: Value = serde_json::from_str(decoded).unwrap(); - assert_eq!(message, Some(decoded_)); - } - - #[test] - fn decodes_zero_length_message() { - let content_type = "application/vscode-jsonrpc; charset=utf-8"; - let encoded = encode_message(Some(content_type), ""); - - let mut codec = DebugAdapterCodec::default(); - let mut buffer = BytesMut::from(encoded.as_str()); - let message: Option = codec.decode(&mut buffer).unwrap(); - assert_eq!(message, None); - } - - #[test] - fn recovers_from_parse_error() { - let decoded = r#"{"jsonrpc":"2.0","method":"exit"}"#; - let encoded = encode_message(None, decoded); - let mixed = format!("foobar{}Content-Length: foobar\r\n\r\n{}", encoded, encoded); - - let mut codec = DebugAdapterCodec::default(); - let mut buffer = BytesMut::from(mixed.as_str()); - assert_err!( - codec.decode(&mut buffer), - Err(ParseError::MissingContentLength) - ); - - let message: Option = codec.decode(&mut buffer).unwrap(); - let first_valid = serde_json::from_str(decoded).unwrap(); - assert_eq!(message, Some(first_valid)); - assert_err!( - codec.decode(&mut buffer), - Err(ParseError::InvalidContentLength(_)) - ); - - let message = codec.decode(&mut buffer).unwrap(); - let second_valid = serde_json::from_str(decoded).unwrap(); - assert_eq!(message, Some(second_valid)); - - let message = codec.decode(&mut buffer).unwrap(); - assert_eq!(message, None); - } - - #[test] - fn decodes_small_chunks() { - let decoded = r#"{"jsonrpc":"2.0","method":"exit"}"#; - let content_type = "application/vscode-jsonrpc; charset=utf-8"; - let encoded = encode_message(Some(content_type), decoded); - - let mut codec = DebugAdapterCodec::default(); - let mut buffer = BytesMut::from(encoded.as_str()); - - let rest = buffer.split_off(40); - let message = codec.decode(&mut buffer).unwrap(); - assert_eq!(message, None); - buffer.unsplit(rest); - - let rest = buffer.split_off(80); - let message = codec.decode(&mut buffer).unwrap(); - assert_eq!(message, None); - buffer.unsplit(rest); - - let rest = buffer.split_off(16); - let message = codec.decode(&mut buffer).unwrap(); - assert_eq!(message, None); - buffer.unsplit(rest); - - let decoded: Value = serde_json::from_str(decoded).unwrap(); - let message = codec.decode(&mut buffer).unwrap(); - assert_eq!(message, Some(decoded)); - } -} diff --git a/src/dap/mod.rs b/src/dap/mod.rs index 0439207e9..0f6372d54 100644 --- a/src/dap/mod.rs +++ b/src/dap/mod.rs @@ -1,27 +1,46 @@ -use self::codec::{DebugAdapterCodec, ParseError}; use crate::poke::load_session; use crate::types::Network; -use clarity_repl::repl::Session; -use dap_types::events::*; -use dap_types::requests::*; -use dap_types::responses::*; -use dap_types::types::*; -use dap_types::*; -use futures::{SinkExt, StreamExt}; -use std::fs::File; -use std::io::prelude::*; +use clarity_repl::clarity::debug::dap::DAPDebugger; use std::path::PathBuf; -use tokio; -use tokio::io::{Stdin, Stdout}; -use tokio_util::codec::{FramedRead, FramedWrite}; -mod codec; +pub fn run_dap() -> Result<(), String> { + let mut dap = DAPDebugger::new(); + match block_on(dap.init()) { + Ok((manifest, expression)) => { + let manifest_path = PathBuf::from(manifest); + let mut session = match load_session(&manifest_path, false, &Network::Devnet) { + Ok((session, _, _, _)) => session, + Err((_, e)) => { + println!("{}: unable to load session: {}", red!("error"), e); + std::process::exit(1); + } + }; + + for contract in &session.settings.initial_contracts { + dap.path_to_contract_id.insert( + contract.path.clone(), + contract.get_contract_identifier(false).unwrap(), + ); + dap.contract_id_to_path.insert( + contract.get_contract_identifier(false).unwrap(), + contract.path.clone(), + ); + } -pub fn run_dap() { - match block_on(do_run_dap()) { - Err(_) => std::process::exit(1), - _ => (), - }; + // Begin execution of the expression in debug mode + match session.interpret( + expression.clone(), + None, + Some(vec![Box::new(dap)]), + false, + None, + ) { + Ok(result) => Ok(()), + Err(diagnostics) => Err("unable to interpret expression".to_string()), + } + } + Err(e) => Err(format!("dap_init: {}", e)), + } } pub fn block_on(future: F) -> R @@ -31,256 +50,3 @@ where let rt = crate::utils::create_basic_runtime(); rt.block_on(future) } - -async fn do_run_dap() -> Result<(), String> { - let stdin = tokio::io::stdin(); - let stdout = tokio::io::stdout(); - - let mut reader = FramedRead::new(stdin, DebugAdapterCodec::::default()); - let mut writer = FramedWrite::new(stdout, DebugAdapterCodec::::default()); - let mut dap_session = DapSession::new(reader, writer); - // let mut reader = FramedRead::new(stdin, LinesCodec::new()); - - match dap_session.start().await { - Ok(_) => Ok(()), - Err(e) => { - println!("error: {}", e); - Err(format!("error: {}", e)) - } - } -} - -struct DapSession { - log_file: File, - send_seq: i64, - reader: FramedRead>, - writer: FramedWrite>, - session: Option, -} - -impl DapSession { - pub fn new( - reader: FramedRead>, - writer: FramedWrite>, - ) -> Self { - Self { - log_file: File::create("/Users/brice/work/debugger-demo/dap.txt").unwrap(), - send_seq: 0, - reader, - writer, - session: None, - } - } - - pub async fn start(&mut self) -> Result<(), ParseError> { - writeln!(self.log_file, "STARTING"); - - while let Some(msg) = self.reader.next().await { - writeln!(self.log_file, "LOOPING"); - match msg { - Ok(msg) => { - println!("got message: {:?}", msg); - writeln!(self.log_file, "message: {:?}", msg); - - use dap_types::MessageKind::*; - match msg.message { - Request(command) => self.handle_request(msg.seq, command).await, - Response(response) => self.handle_response(msg.seq, response).await, - Event(event) => self.handle_event(msg.seq, event).await, - } - } - Err(e) => { - println!("got error: {}", e); - writeln!(self.log_file, "error: {}", e); - return Err(e); - } - } - } - writeln!(self.log_file, "clean exit."); - Ok(()) - } - - async fn send_response(&mut self, response: Response) { - let response_json = serde_json::to_string(&response).unwrap(); - writeln!(self.log_file, "::::response: {}", response_json); - println!("::::response: {}", response_json); - - let message = ProtocolMessage { - seq: self.send_seq, - message: MessageKind::Response(response), - }; - - match self.writer.send(message).await { - Ok(_) => (), - Err(e) => { - writeln!(self.log_file, "ERROR: sending response: {}", e); - } - }; - - self.send_seq += 1; - } - - async fn send_event(&mut self, body: EventBody) { - let event_json = serde_json::to_string(&body).unwrap(); - writeln!(self.log_file, "::::event: {}", event_json); - println!("::::event: {}", event_json); - - let message = ProtocolMessage { - seq: self.send_seq, - message: MessageKind::Event(Event { body: Some(body) }), - }; - - match self.writer.send(message).await { - Ok(_) => (), - Err(e) => { - writeln!(self.log_file, "ERROR: sending response: {}", e); - } - }; - - self.send_seq += 1; - } - - pub async fn handle_request(&mut self, seq: i64, command: RequestCommand) { - use dap_types::requests::RequestCommand::*; - let result = match command { - Initialize(arguments) => self.initialize(arguments), - Launch(arguments) => self.launch(arguments).await, - // SetBreakpoints(arguments) => self.setBreakpoints(arguments), - _ => Err("unsupported request".to_string()), - }; - - let response = match result { - Ok(body) => Response { - request_seq: seq, - success: true, - message: None, - body, - }, - Err(err_msg) => Response { - request_seq: seq, - success: false, - message: Some(err_msg), - body: None, - }, - }; - - println!("SENDING RESPONSE"); - self.send_response(response).await; - } - - pub async fn handle_event(&mut self, seq: i64, event: Event) { - let response = Response { - request_seq: seq, - success: true, - message: None, - body: None, - }; - self.send_response(response).await; - } - - pub async fn handle_response(&mut self, seq: i64, response: Response) { - let response = Response { - request_seq: seq, - success: true, - message: None, - body: None, - }; - self.send_response(response).await; - } - - // Request handlers - - fn initialize( - &mut self, - arguments: InitializeRequestArguments, - ) -> Result, String> { - println!("INITIALIZE"); - let capabilities = Capabilities { - supports_function_breakpoints: Some(true), - supports_step_in_targets_request: Some(true), - support_terminate_debuggee: Some(true), - supports_loaded_sources_request: Some(true), - supports_data_breakpoints: Some(true), - supports_breakpoint_locations_request: Some(true), - supports_configuration_done_request: None, - supports_conditional_breakpoints: None, - supports_hit_conditional_breakpoints: None, - supports_evaluate_for_hovers: None, - exception_breakpoint_filters: None, - supports_step_back: None, - supports_set_variable: None, - supports_restart_frame: None, - supports_goto_targets_request: None, - supports_completions_request: None, - completion_trigger_characters: None, - supports_modules_request: None, - additional_module_columns: None, - supported_checksum_algorithms: None, - supports_restart_request: None, - supports_exception_options: None, - supports_value_formatting_options: None, - supports_exception_info_request: None, - support_suspend_debuggee: None, - supports_delayed_stack_trace_loading: None, - supports_log_points: None, - supports_terminate_threads_request: None, - supports_set_expression: None, - supports_terminate_request: None, - supports_read_memory_request: None, - supports_write_memory_request: None, - supports_disassemble_request: None, - supports_cancel_request: None, - supports_clipboard_context: None, - supports_stepping_granularity: None, - supports_instruction_breakpoints: None, - supports_exception_filter_options: None, - supports_single_thread_execution_requests: None, - }; - Ok(Some(ResponseBody::Initialize(InitializeResponse { - capabilities, - }))) - } - - async fn launch( - &mut self, - arguments: LaunchRequestArguments, - ) -> Result, String> { - println!("LAUNCH"); - // Verify that the manifest and expression were specified - let manifest = match arguments.manifest { - Some(manifest) => manifest, - None => return Err("manifest must be specified".to_string()), - }; - let expression = match arguments.expression { - Some(expression) => expression, - None => return Err("expression to debug must be specified".to_string()), - }; - - // Initiate the session - let manifest_path = PathBuf::from(manifest); - let session = match load_session(&manifest_path, false, &Network::Devnet) { - Ok((session, _, _, _)) => session, - Err((_, e)) => return Err(e), - }; - self.session = Some(session); - - // Begin execution of the expression in debug mode - // if self - // .session - // .as_mut() - // .unwrap() - // .interpret(expression, None, false, true, None) - // .is_err() - // { - // return Err("unable to start session".to_string()); - // } - - self.send_event(EventBody::Initialized).await; - - Ok(Some(ResponseBody::Launch)) - } - - // fn setBreakpoints(arguments: SetBreakpointsArguments) -> Result, String> { - // println!("SET BREAKPOINTS"); - // } -} diff --git a/src/frontend/cli.rs b/src/frontend/cli.rs index dea31c3df..d42f96e7d 100644 --- a/src/frontend/cli.rs +++ b/src/frontend/cli.rs @@ -668,7 +668,13 @@ pub fn main() { } } Command::LSP => run_lsp(), - Command::DAP => run_dap(), + Command::DAP => match run_dap() { + Ok(_) => (), + Err(e) => { + println!("{}: {}", red!("error"), e); + process::exit(1); + } + }, Command::Completions(cmd) => { let mut app = Opts::command(); let file_name = cmd.shell.file_name("clarinet"); diff --git a/src/lsp/clarity_language_backend.rs b/src/lsp/clarity_language_backend.rs index ea732bb2f..a14ca4909 100644 --- a/src/lsp/clarity_language_backend.rs +++ b/src/lsp/clarity_language_backend.rs @@ -196,7 +196,7 @@ impl ClarityLanguageBackend { contract.code.clone(), analysis.clone(), false, - false, + None, None, ); diff --git a/src/runnner/deno.rs b/src/runnner/deno.rs index cc5b110be..34a793a82 100644 --- a/src/runnner/deno.rs +++ b/src/runnner/deno.rs @@ -1431,8 +1431,8 @@ fn mine_block(state: &mut OpState, args: Value, _: ()) -> Result Result format!("{}", output),