forked from extern/nushell
merge w/ upstream
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
use std::ops::{Index, IndexMut};
|
||||
|
||||
use crate::Signature;
|
||||
use crate::{DeclId, Signature};
|
||||
|
||||
use super::Statement;
|
||||
|
||||
@ -8,6 +8,7 @@ use super::Statement;
|
||||
pub struct Block {
|
||||
pub signature: Box<Signature>,
|
||||
pub stmts: Vec<Statement>,
|
||||
pub exports: Vec<(Vec<u8>, DeclId)>, // Assuming just defs for now
|
||||
}
|
||||
|
||||
impl Block {
|
||||
@ -45,6 +46,28 @@ impl Block {
|
||||
Self {
|
||||
signature: Box::new(Signature::new("")),
|
||||
stmts: vec![],
|
||||
exports: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_exports(self, exports: Vec<(Vec<u8>, DeclId)>) -> Self {
|
||||
Self {
|
||||
signature: self.signature,
|
||||
stmts: self.stmts,
|
||||
exports,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Block
|
||||
where
|
||||
T: Iterator<Item = Statement>,
|
||||
{
|
||||
fn from(stmts: T) -> Self {
|
||||
Self {
|
||||
signature: Box::new(Signature::new("")),
|
||||
stmts: stmts.collect(),
|
||||
exports: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,4 +25,24 @@ impl Call {
|
||||
named: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_flag(&self, flag_name: &str) -> bool {
|
||||
for name in &self.named {
|
||||
if flag_name == name.0 {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn get_flag_expr(&self, flag_name: &str) -> Option<Expression> {
|
||||
for name in &self.named {
|
||||
if flag_name == name.0 {
|
||||
return name.1.clone();
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
@ -7,14 +7,16 @@ pub enum Expr {
|
||||
Int(i64),
|
||||
Float(f64),
|
||||
Range(
|
||||
Option<Box<Expression>>,
|
||||
Option<Box<Expression>>,
|
||||
Option<Box<Expression>>, // from
|
||||
Option<Box<Expression>>, // next value after "from"
|
||||
Option<Box<Expression>>, // to
|
||||
RangeOperator,
|
||||
),
|
||||
Var(VarId),
|
||||
Call(Box<Call>),
|
||||
ExternalCall(Vec<u8>, Vec<Vec<u8>>),
|
||||
ExternalCall(Span, Vec<Span>),
|
||||
Operator(Operator),
|
||||
RowCondition(VarId, Box<Expression>),
|
||||
BinaryOp(Box<Expression>, Box<Expression>, Box<Expression>), //lhs, op, rhs
|
||||
Subexpression(BlockId),
|
||||
Block(BlockId),
|
||||
|
@ -6,6 +6,7 @@ pub struct Expression {
|
||||
pub expr: Expr,
|
||||
pub span: Span,
|
||||
pub ty: Type,
|
||||
pub custom_completion: Option<String>,
|
||||
}
|
||||
|
||||
impl Expression {
|
||||
@ -14,6 +15,7 @@ impl Expression {
|
||||
expr: Expr::Garbage,
|
||||
span,
|
||||
ty: Type::Unknown,
|
||||
custom_completion: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
14
crates/nu-protocol/src/ast/import_pattern.rs
Normal file
14
crates/nu-protocol/src/ast/import_pattern.rs
Normal file
@ -0,0 +1,14 @@
|
||||
use crate::Span;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ImportPatternMember {
|
||||
Glob { span: Span },
|
||||
Name { name: Vec<u8>, span: Span },
|
||||
List { names: Vec<(Vec<u8>, Span)> },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ImportPattern {
|
||||
pub head: Vec<u8>,
|
||||
pub members: Vec<ImportPatternMember>,
|
||||
}
|
@ -3,6 +3,7 @@ mod call;
|
||||
mod cell_path;
|
||||
mod expr;
|
||||
mod expression;
|
||||
mod import_pattern;
|
||||
mod operator;
|
||||
mod pipeline;
|
||||
mod statement;
|
||||
@ -12,6 +13,7 @@ pub use call::*;
|
||||
pub use cell_path::*;
|
||||
pub use expr::*;
|
||||
pub use expression::*;
|
||||
pub use import_pattern::*;
|
||||
pub use operator::*;
|
||||
pub use pipeline::*;
|
||||
pub use statement::*;
|
||||
|
@ -1,8 +1,9 @@
|
||||
use crate::Span;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Display;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Operator {
|
||||
Equal,
|
||||
NotEqual,
|
||||
@ -49,7 +50,7 @@ impl Display for Operator {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum RangeInclusion {
|
||||
Inclusive,
|
||||
RightExclusive,
|
||||
@ -59,6 +60,7 @@ pub enum RangeInclusion {
|
||||
pub struct RangeOperator {
|
||||
pub inclusion: RangeInclusion,
|
||||
pub span: Span,
|
||||
pub next_op_span: Span,
|
||||
}
|
||||
|
||||
impl Display for RangeOperator {
|
||||
|
@ -2,7 +2,7 @@ use crate::{ast::Call, value::Value, BlockId, Example, ShellError, Signature};
|
||||
|
||||
use super::EvaluationContext;
|
||||
|
||||
pub trait Command {
|
||||
pub trait Command: Send + Sync {
|
||||
fn name(&self) -> &str;
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
|
@ -1,7 +1,7 @@
|
||||
use super::Command;
|
||||
use crate::{ast::Block, BlockId, DeclId, Span, Type, VarId};
|
||||
use crate::{ast::Block, BlockId, DeclId, Signature, Span, Type, VarId};
|
||||
use core::panic;
|
||||
use std::{collections::HashMap, ops::Range, slice::Iter};
|
||||
use std::{collections::HashMap, slice::Iter};
|
||||
|
||||
pub struct EngineState {
|
||||
files: Vec<(String, usize, usize)>,
|
||||
@ -17,6 +17,7 @@ pub struct ScopeFrame {
|
||||
vars: HashMap<Vec<u8>, VarId>,
|
||||
decls: HashMap<Vec<u8>, DeclId>,
|
||||
aliases: HashMap<Vec<u8>, Vec<Span>>,
|
||||
modules: HashMap<Vec<u8>, BlockId>,
|
||||
}
|
||||
|
||||
impl ScopeFrame {
|
||||
@ -25,6 +26,7 @@ impl ScopeFrame {
|
||||
vars: HashMap::new(),
|
||||
decls: HashMap::new(),
|
||||
aliases: HashMap::new(),
|
||||
modules: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,6 +78,9 @@ impl EngineState {
|
||||
for item in first.aliases.into_iter() {
|
||||
last.aliases.insert(item.0, item.1);
|
||||
}
|
||||
for item in first.modules.into_iter() {
|
||||
last.modules.insert(item.0, item.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,6 +118,11 @@ impl EngineState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_contents(&self) {
|
||||
let string = String::from_utf8_lossy(&self.file_contents);
|
||||
println!("{}", string);
|
||||
}
|
||||
|
||||
pub fn find_decl(&self, name: &[u8]) -> Option<DeclId> {
|
||||
for scope in self.scope.iter().rev() {
|
||||
if let Some(decl_id) = scope.decls.get(name) {
|
||||
@ -123,6 +133,24 @@ impl EngineState {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn find_commands_by_prefix(&self, name: &[u8]) -> Vec<Vec<u8>> {
|
||||
let mut output = vec![];
|
||||
|
||||
for scope in self.scope.iter().rev() {
|
||||
for decl in &scope.decls {
|
||||
if decl.0.starts_with(name) {
|
||||
output.push(decl.0.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
pub fn get_span_contents(&self, span: &Span) -> &[u8] {
|
||||
&self.file_contents[span.start..span.end]
|
||||
}
|
||||
|
||||
pub fn get_var(&self, var_id: VarId) -> &Type {
|
||||
self.vars
|
||||
.get(var_id)
|
||||
@ -136,6 +164,21 @@ impl EngineState {
|
||||
.expect("internal error: missing declaration")
|
||||
}
|
||||
|
||||
pub fn get_decls(&self) -> Vec<Signature> {
|
||||
let mut output = vec![];
|
||||
for decl in self.decls.iter() {
|
||||
if decl.get_block_id().is_none() {
|
||||
let mut signature = (*decl).signature();
|
||||
signature.usage = decl.usage().to_string();
|
||||
signature.extra_usage = decl.extra_usage().to_string();
|
||||
|
||||
output.push(signature);
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
pub fn get_block(&self, block_id: BlockId) -> &Block {
|
||||
self.blocks
|
||||
.get(block_id)
|
||||
@ -272,6 +315,37 @@ impl<'a> StateWorkingSet<'a> {
|
||||
self.num_blocks() - 1
|
||||
}
|
||||
|
||||
pub fn add_module(&mut self, name: &str, block: Block) -> BlockId {
|
||||
let name = name.as_bytes().to_vec();
|
||||
|
||||
self.delta.blocks.push(block);
|
||||
let block_id = self.num_blocks() - 1;
|
||||
|
||||
let scope_frame = self
|
||||
.delta
|
||||
.scope
|
||||
.last_mut()
|
||||
.expect("internal error: missing required scope frame");
|
||||
|
||||
scope_frame.modules.insert(name, block_id);
|
||||
|
||||
block_id
|
||||
}
|
||||
|
||||
pub fn activate_overlay(&mut self, overlay: Vec<(Vec<u8>, DeclId)>) {
|
||||
// TODO: This will overwrite all existing definitions in a scope. When we add deactivate,
|
||||
// we need to re-think how make it recoverable.
|
||||
let scope_frame = self
|
||||
.delta
|
||||
.scope
|
||||
.last_mut()
|
||||
.expect("internal error: missing required scope frame");
|
||||
|
||||
for (name, decl_id) in overlay {
|
||||
scope_frame.decls.insert(name, decl_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_span_start(&self) -> usize {
|
||||
self.permanent_state.next_span_start() + self.delta.file_contents.len()
|
||||
}
|
||||
@ -357,6 +431,22 @@ impl<'a> StateWorkingSet<'a> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn find_module(&self, name: &[u8]) -> Option<BlockId> {
|
||||
for scope in self.delta.scope.iter().rev() {
|
||||
if let Some(block_id) = scope.modules.get(name) {
|
||||
return Some(*block_id);
|
||||
}
|
||||
}
|
||||
|
||||
for scope in self.permanent_state.scope.iter().rev() {
|
||||
if let Some(block_id) = scope.modules.get(name) {
|
||||
return Some(*block_id);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
// pub fn update_decl(&mut self, decl_id: usize, block: Option<BlockId>) {
|
||||
// let decl = self.get_decl_mut(decl_id);
|
||||
// decl.body = block;
|
||||
@ -496,6 +586,24 @@ impl<'a> StateWorkingSet<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_commands_by_prefix(&self, name: &[u8]) -> Vec<Vec<u8>> {
|
||||
let mut output = vec![];
|
||||
|
||||
for scope in self.delta.scope.iter().rev() {
|
||||
for decl in &scope.decls {
|
||||
if decl.0.starts_with(name) {
|
||||
output.push(decl.0.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut permanent = self.permanent_state.find_commands_by_prefix(name);
|
||||
|
||||
output.append(&mut permanent);
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
pub fn get_block(&self, block_id: BlockId) -> &Block {
|
||||
let num_permanent_blocks = self.permanent_state.num_blocks();
|
||||
if block_id < num_permanent_blocks {
|
||||
@ -513,95 +621,83 @@ impl<'a> StateWorkingSet<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> codespan_reporting::files::Files<'a> for StateWorkingSet<'a> {
|
||||
type FileId = usize;
|
||||
|
||||
type Name = String;
|
||||
|
||||
type Source = String;
|
||||
|
||||
fn name(&'a self, id: Self::FileId) -> Result<Self::Name, codespan_reporting::files::Error> {
|
||||
Ok(self.get_filename(id))
|
||||
}
|
||||
|
||||
fn source(
|
||||
&'a self,
|
||||
id: Self::FileId,
|
||||
) -> Result<Self::Source, codespan_reporting::files::Error> {
|
||||
Ok(self.get_file_source(id))
|
||||
}
|
||||
|
||||
fn line_index(
|
||||
&'a self,
|
||||
id: Self::FileId,
|
||||
byte_index: usize,
|
||||
) -> Result<usize, codespan_reporting::files::Error> {
|
||||
let source = self.get_file_source(id);
|
||||
|
||||
let mut count = 0;
|
||||
|
||||
for byte in source.bytes().enumerate() {
|
||||
if byte.0 == byte_index {
|
||||
// println!("count: {} for file: {} index: {}", count, id, byte_index);
|
||||
return Ok(count);
|
||||
}
|
||||
if byte.1 == b'\n' {
|
||||
count += 1;
|
||||
}
|
||||
impl<'a> miette::SourceCode for &StateWorkingSet<'a> {
|
||||
fn read_span<'b>(
|
||||
&'b self,
|
||||
span: &miette::SourceSpan,
|
||||
context_lines_before: usize,
|
||||
context_lines_after: usize,
|
||||
) -> Result<Box<dyn miette::SpanContents + 'b>, miette::MietteError> {
|
||||
let debugging = std::env::var("MIETTE_DEBUG").is_ok();
|
||||
if debugging {
|
||||
let finding_span = "Finding span in StateWorkingSet";
|
||||
dbg!(finding_span, span);
|
||||
}
|
||||
|
||||
// println!("count: {} for file: {} index: {}", count, id, byte_index);
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
fn line_range(
|
||||
&'a self,
|
||||
id: Self::FileId,
|
||||
line_index: usize,
|
||||
) -> Result<Range<usize>, codespan_reporting::files::Error> {
|
||||
let source = self.get_file_source(id);
|
||||
|
||||
let mut count = 0;
|
||||
|
||||
let mut start = Some(0);
|
||||
let mut end = None;
|
||||
|
||||
for byte in source.bytes().enumerate() {
|
||||
#[allow(clippy::comparison_chain)]
|
||||
if count > line_index {
|
||||
let start = start.expect("internal error: couldn't find line");
|
||||
let end = end.expect("internal error: couldn't find line");
|
||||
|
||||
// println!(
|
||||
// "Span: {}..{} for fileid: {} index: {}",
|
||||
// start, end, id, line_index
|
||||
// );
|
||||
return Ok(start..end);
|
||||
} else if count == line_index {
|
||||
end = Some(byte.0 + 1);
|
||||
for (filename, start, end) in self.files() {
|
||||
if debugging {
|
||||
dbg!(&filename, start, end);
|
||||
}
|
||||
if span.offset() >= *start && span.offset() + span.len() <= *end {
|
||||
if debugging {
|
||||
let found_file = "Found matching file";
|
||||
dbg!(found_file);
|
||||
}
|
||||
let our_span = Span {
|
||||
start: *start,
|
||||
end: *end,
|
||||
};
|
||||
// We need to move to a local span because we're only reading
|
||||
// the specific file contents via self.get_span_contents.
|
||||
let local_span = (span.offset() - *start, span.len()).into();
|
||||
if debugging {
|
||||
dbg!(&local_span);
|
||||
}
|
||||
let span_contents = self.get_span_contents(our_span);
|
||||
if debugging {
|
||||
dbg!(String::from_utf8_lossy(span_contents));
|
||||
}
|
||||
let span_contents = span_contents.read_span(
|
||||
&local_span,
|
||||
context_lines_before,
|
||||
context_lines_after,
|
||||
)?;
|
||||
let content_span = span_contents.span();
|
||||
// Back to "global" indexing
|
||||
let retranslated = (content_span.offset() + start, content_span.len()).into();
|
||||
if debugging {
|
||||
dbg!(&retranslated);
|
||||
}
|
||||
|
||||
#[allow(clippy::comparison_chain)]
|
||||
if byte.1 == b'\n' {
|
||||
count += 1;
|
||||
if count > line_index {
|
||||
break;
|
||||
} else if count == line_index {
|
||||
start = Some(byte.0 + 1);
|
||||
let data = span_contents.data();
|
||||
if filename == "<cli>" {
|
||||
if debugging {
|
||||
let success_cli = "Successfully read CLI span";
|
||||
dbg!(success_cli, String::from_utf8_lossy(data));
|
||||
}
|
||||
return Ok(Box::new(miette::MietteSpanContents::new(
|
||||
data,
|
||||
retranslated,
|
||||
span_contents.line(),
|
||||
span_contents.column(),
|
||||
span_contents.line_count(),
|
||||
)));
|
||||
} else {
|
||||
if debugging {
|
||||
let success_file = "Successfully read file span";
|
||||
dbg!(success_file);
|
||||
}
|
||||
return Ok(Box::new(miette::MietteSpanContents::new_named(
|
||||
filename.clone(),
|
||||
data,
|
||||
retranslated,
|
||||
span_contents.line(),
|
||||
span_contents.column(),
|
||||
span_contents.line_count(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match (start, end) {
|
||||
(Some(start), Some(end)) => {
|
||||
// println!(
|
||||
// "Span: {}..{} for fileid: {} index: {}",
|
||||
// start, end, id, line_index
|
||||
// );
|
||||
Ok(start..end)
|
||||
}
|
||||
_ => Err(codespan_reporting::files::Error::FileMissing),
|
||||
}
|
||||
Err(miette::MietteError::OutOfBounds)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
use super::EngineState;
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||
|
||||
use crate::{ShellError, Value, VarId};
|
||||
use crate::{ShellError, Signature, Value, VarId};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EvaluationContext {
|
||||
@ -46,6 +46,10 @@ impl EvaluationContext {
|
||||
pub fn print_stack(&self) {
|
||||
self.stack.print_stack();
|
||||
}
|
||||
|
||||
pub fn get_commands_info(&self) -> Vec<Signature> {
|
||||
self.engine_state.borrow().get_decls()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -104,6 +108,10 @@ impl Stack {
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn get_env_vars(&self) -> HashMap<String, String> {
|
||||
self.0.borrow().env_vars.clone()
|
||||
}
|
||||
|
||||
pub fn print_stack(&self) {
|
||||
println!("===frame===");
|
||||
println!("vars:");
|
||||
|
@ -1,24 +1,81 @@
|
||||
use miette::Diagnostic;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{ast::Operator, Span, Type};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Error, Diagnostic, Serialize, Deserialize)]
|
||||
pub enum ShellError {
|
||||
#[error("Type mismatch during operation.")]
|
||||
#[diagnostic(code(nu::shell::type_mismatch), url(docsrs))]
|
||||
OperatorMismatch {
|
||||
#[label = "type mismatch for operator"]
|
||||
op_span: Span,
|
||||
lhs_ty: Type,
|
||||
#[label("{lhs_ty}")]
|
||||
lhs_span: Span,
|
||||
rhs_ty: Type,
|
||||
#[label("{rhs_ty}")]
|
||||
rhs_span: Span,
|
||||
},
|
||||
UnsupportedOperator(Operator, Span),
|
||||
UnknownOperator(String, Span),
|
||||
ExternalNotSupported(Span),
|
||||
|
||||
#[error("Unsupported operator: {0}.")]
|
||||
#[diagnostic(code(nu::shell::unsupported_operator), url(docsrs))]
|
||||
UnsupportedOperator(Operator, #[label = "unsupported operator"] Span),
|
||||
|
||||
#[error("Unsupported operator: {0}.")]
|
||||
#[diagnostic(code(nu::shell::unknown_operator), url(docsrs))]
|
||||
UnknownOperator(String, #[label = "unsupported operator"] Span),
|
||||
|
||||
#[error("External commands not yet supported")]
|
||||
#[diagnostic(code(nu::shell::external_commands), url(docsrs))]
|
||||
ExternalNotSupported(#[label = "external not supported"] Span),
|
||||
|
||||
#[error("Internal error: {0}.")]
|
||||
#[diagnostic(code(nu::shell::internal_error), url(docsrs))]
|
||||
InternalError(String),
|
||||
VariableNotFoundAtRuntime(Span),
|
||||
CantConvert(String, Span),
|
||||
DivisionByZero(Span),
|
||||
CannotCreateRange(Span),
|
||||
AccessBeyondEnd(usize, Span),
|
||||
AccessBeyondEndOfStream(Span),
|
||||
IncompatiblePathAccess(String, Span),
|
||||
CantFindColumn(Span),
|
||||
|
||||
#[error("Variable not found")]
|
||||
#[diagnostic(code(nu::shell::variable_not_found), url(docsrs))]
|
||||
VariableNotFoundAtRuntime(#[label = "variable not found"] Span),
|
||||
|
||||
#[error("Can't convert to {0}.")]
|
||||
#[diagnostic(code(nu::shell::cant_convert), url(docsrs))]
|
||||
CantConvert(String, #[label("can't convert to {0}")] Span),
|
||||
|
||||
#[error("Division by zero.")]
|
||||
#[diagnostic(code(nu::shell::division_by_zero), url(docsrs))]
|
||||
DivisionByZero(#[label("division by zero")] Span),
|
||||
|
||||
#[error("Can't convert range to countable values")]
|
||||
#[diagnostic(code(nu::shell::range_to_countable), url(docsrs))]
|
||||
CannotCreateRange(#[label = "can't convert to countable values"] Span),
|
||||
|
||||
#[error("Row number too large (max: {0}).")]
|
||||
#[diagnostic(code(nu::shell::access_beyond_end), url(docsrs))]
|
||||
AccessBeyondEnd(usize, #[label = "too large"] Span),
|
||||
|
||||
#[error("Row number too large.")]
|
||||
#[diagnostic(code(nu::shell::access_beyond_end_of_stream), url(docsrs))]
|
||||
AccessBeyondEndOfStream(#[label = "too large"] Span),
|
||||
|
||||
#[error("Data cannot be accessed with a cell path")]
|
||||
#[diagnostic(code(nu::shell::incompatible_path_access), url(docsrs))]
|
||||
IncompatiblePathAccess(String, #[label("{0} doesn't support cell paths")] Span),
|
||||
|
||||
#[error("Cannot find column")]
|
||||
#[diagnostic(code(nu::shell::column_not_found), url(docsrs))]
|
||||
CantFindColumn(#[label = "cannot find column"] Span),
|
||||
|
||||
#[error("External command")]
|
||||
#[diagnostic(code(nu::shell::external_command), url(docsrs))]
|
||||
ExternalCommand(String, #[label("{0}")] Span),
|
||||
|
||||
#[error("Unsupported input")]
|
||||
#[diagnostic(code(nu::shell::unsupported_input), url(docsrs))]
|
||||
UnsupportedInput(String, #[label("{0}")] Span),
|
||||
|
||||
#[error("Flag not found")]
|
||||
#[diagnostic(code(nu::shell::flag_not_found), url(docsrs))]
|
||||
FlagNotFound(String, #[label("{0} not found")] Span),
|
||||
}
|
||||
|
@ -1,9 +1,23 @@
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
use miette::SourceSpan;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub struct Spanned<T> {
|
||||
pub item: T,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Span {
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
}
|
||||
|
||||
impl From<Span> for SourceSpan {
|
||||
fn from(s: Span) -> Self {
|
||||
Self::new(s.start.into(), (s.end - s.start).into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Span {
|
||||
pub fn new(start: usize, end: usize) -> Span {
|
||||
Span { start, end }
|
||||
|
@ -33,8 +33,11 @@ pub enum SyntaxShape {
|
||||
/// A glob pattern is allowed, eg `foo*`
|
||||
GlobPattern,
|
||||
|
||||
/// A module path pattern used for imports
|
||||
ImportPattern,
|
||||
|
||||
/// A block is allowed, eg `{start this thing}`
|
||||
Block,
|
||||
Block(Option<Vec<SyntaxShape>>),
|
||||
|
||||
/// A table is allowed, eg `[[first, second]; [1, 2]]`
|
||||
Table,
|
||||
@ -69,20 +72,25 @@ pub enum SyntaxShape {
|
||||
|
||||
/// A general expression, eg `1 + 2` or `foo --bar`
|
||||
Expression,
|
||||
|
||||
/// A custom shape with custom completion logic
|
||||
Custom(Box<SyntaxShape>, String),
|
||||
}
|
||||
|
||||
impl SyntaxShape {
|
||||
pub fn to_type(&self) -> Type {
|
||||
match self {
|
||||
SyntaxShape::Any => Type::Unknown,
|
||||
SyntaxShape::Block => Type::Block,
|
||||
SyntaxShape::Block(_) => Type::Block,
|
||||
SyntaxShape::CellPath => Type::Unknown,
|
||||
SyntaxShape::Custom(custom, _) => custom.to_type(),
|
||||
SyntaxShape::Duration => Type::Duration,
|
||||
SyntaxShape::Expression => Type::Unknown,
|
||||
SyntaxShape::FilePath => Type::FilePath,
|
||||
SyntaxShape::Filesize => Type::Filesize,
|
||||
SyntaxShape::FullCellPath => Type::Unknown,
|
||||
SyntaxShape::GlobPattern => Type::String,
|
||||
SyntaxShape::ImportPattern => Type::Unknown,
|
||||
SyntaxShape::Int => Type::Int,
|
||||
SyntaxShape::List(x) => {
|
||||
let contents = x.to_type();
|
||||
|
@ -1,6 +1,8 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Type {
|
||||
Int,
|
||||
Float,
|
||||
@ -16,9 +18,11 @@ pub enum Type {
|
||||
Number,
|
||||
Nothing,
|
||||
Record(Vec<String>, Vec<Type>),
|
||||
Table,
|
||||
ValueStream,
|
||||
Unknown,
|
||||
Error,
|
||||
Binary,
|
||||
}
|
||||
|
||||
impl Display for Type {
|
||||
@ -34,6 +38,7 @@ impl Display for Type {
|
||||
Type::Int => write!(f, "int"),
|
||||
Type::Range => write!(f, "range"),
|
||||
Type::Record(cols, vals) => write!(f, "record<{}, {:?}>", cols.join(", "), vals),
|
||||
Type::Table => write!(f, "table"),
|
||||
Type::List(l) => write!(f, "list<{}>", l),
|
||||
Type::Nothing => write!(f, "nothing"),
|
||||
Type::Number => write!(f, "number"),
|
||||
@ -41,6 +46,7 @@ impl Display for Type {
|
||||
Type::ValueStream => write!(f, "value stream"),
|
||||
Type::Unknown => write!(f, "unknown"),
|
||||
Type::Error => write!(f, "error"),
|
||||
Type::Binary => write!(f, "binary"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,17 +4,18 @@ mod stream;
|
||||
|
||||
pub use range::*;
|
||||
pub use row::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use stream::*;
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::ast::{PathMember, RangeInclusion};
|
||||
use crate::ast::PathMember;
|
||||
use crate::{span, BlockId, Span, Type};
|
||||
|
||||
use crate::ShellError;
|
||||
|
||||
/// Core structured values that pass through the pipeline in engine-q
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum Value {
|
||||
Bool {
|
||||
val: bool,
|
||||
@ -24,6 +25,14 @@ pub enum Value {
|
||||
val: i64,
|
||||
span: Span,
|
||||
},
|
||||
Filesize {
|
||||
val: u64,
|
||||
span: Span,
|
||||
},
|
||||
Duration {
|
||||
val: u64,
|
||||
span: Span,
|
||||
},
|
||||
Range {
|
||||
val: Box<Range>,
|
||||
span: Span,
|
||||
@ -59,6 +68,10 @@ pub enum Value {
|
||||
Error {
|
||||
error: ShellError,
|
||||
},
|
||||
Binary {
|
||||
val: Vec<u8>,
|
||||
span: Span,
|
||||
},
|
||||
}
|
||||
|
||||
impl Value {
|
||||
@ -76,6 +89,8 @@ impl Value {
|
||||
Value::Bool { span, .. } => *span,
|
||||
Value::Int { span, .. } => *span,
|
||||
Value::Float { span, .. } => *span,
|
||||
Value::Filesize { span, .. } => *span,
|
||||
Value::Duration { span, .. } => *span,
|
||||
Value::Range { span, .. } => *span,
|
||||
Value::String { span, .. } => *span,
|
||||
Value::Record { span, .. } => *span,
|
||||
@ -83,6 +98,7 @@ impl Value {
|
||||
Value::Block { span, .. } => *span,
|
||||
Value::Stream { span, .. } => *span,
|
||||
Value::Nothing { span, .. } => *span,
|
||||
Value::Binary { span, .. } => *span,
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,6 +108,8 @@ impl Value {
|
||||
Value::Bool { span, .. } => *span = new_span,
|
||||
Value::Int { span, .. } => *span = new_span,
|
||||
Value::Float { span, .. } => *span = new_span,
|
||||
Value::Filesize { span, .. } => *span = new_span,
|
||||
Value::Duration { span, .. } => *span = new_span,
|
||||
Value::Range { span, .. } => *span = new_span,
|
||||
Value::String { span, .. } => *span = new_span,
|
||||
Value::Record { span, .. } => *span = new_span,
|
||||
@ -100,6 +118,7 @@ impl Value {
|
||||
Value::Block { span, .. } => *span = new_span,
|
||||
Value::Nothing { span, .. } => *span = new_span,
|
||||
Value::Error { .. } => {}
|
||||
Value::Binary { span, .. } => *span = new_span,
|
||||
}
|
||||
|
||||
self
|
||||
@ -111,6 +130,8 @@ impl Value {
|
||||
Value::Bool { .. } => Type::Bool,
|
||||
Value::Int { .. } => Type::Int,
|
||||
Value::Float { .. } => Type::Float,
|
||||
Value::Filesize { .. } => Type::Filesize,
|
||||
Value::Duration { .. } => Type::Duration,
|
||||
Value::Range { .. } => Type::Range,
|
||||
Value::String { .. } => Type::String,
|
||||
Value::Record { cols, vals, .. } => {
|
||||
@ -121,6 +142,7 @@ impl Value {
|
||||
Value::Block { .. } => Type::Block,
|
||||
Value::Stream { .. } => Type::ValueStream,
|
||||
Value::Error { .. } => Type::Error,
|
||||
Value::Binary { .. } => Type::Binary,
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,21 +152,13 @@ impl Value {
|
||||
Value::Bool { val, .. } => val.to_string(),
|
||||
Value::Int { val, .. } => val.to_string(),
|
||||
Value::Float { val, .. } => val.to_string(),
|
||||
Value::Filesize { val, .. } => format!("{} bytes", val),
|
||||
Value::Duration { val, .. } => format!("{} ns", val),
|
||||
Value::Range { val, .. } => {
|
||||
let vals: Vec<i64> = match (&val.from, &val.to) {
|
||||
(Value::Int { val: from, .. }, Value::Int { val: to, .. }) => {
|
||||
match val.inclusion {
|
||||
RangeInclusion::Inclusive => (*from..=*to).collect(),
|
||||
RangeInclusion::RightExclusive => (*from..*to).collect(),
|
||||
}
|
||||
}
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
format!(
|
||||
"range: [{}]",
|
||||
vals.iter()
|
||||
.map(|x| x.to_string())
|
||||
val.into_iter()
|
||||
.map(|x| x.into_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
)
|
||||
@ -169,6 +183,38 @@ impl Value {
|
||||
Value::Block { val, .. } => format!("<Block {}>", val),
|
||||
Value::Nothing { .. } => String::new(),
|
||||
Value::Error { error } => format!("{:?}", error),
|
||||
Value::Binary { val, .. } => format!("{:?}", val),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collect_string(self) -> String {
|
||||
match self {
|
||||
Value::Bool { val, .. } => val.to_string(),
|
||||
Value::Int { val, .. } => val.to_string(),
|
||||
Value::Float { val, .. } => val.to_string(),
|
||||
Value::Filesize { val, .. } => format!("{} bytes", val),
|
||||
Value::Duration { val, .. } => format!("{} ns", val),
|
||||
Value::Range { val, .. } => val
|
||||
.into_iter()
|
||||
.map(|x| x.into_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(", "),
|
||||
Value::String { val, .. } => val,
|
||||
Value::Stream { stream, .. } => stream.collect_string(),
|
||||
Value::List { vals: val, .. } => val
|
||||
.into_iter()
|
||||
.map(|x| x.collect_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
Value::Record { vals, .. } => vals
|
||||
.into_iter()
|
||||
.map(|y| y.collect_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
Value::Block { val, .. } => format!("<Block {}>", val),
|
||||
Value::Nothing { .. } => String::new(),
|
||||
Value::Error { error } => format!("{:?}", error),
|
||||
Value::Binary { val, .. } => format!("{:?}", val),
|
||||
}
|
||||
}
|
||||
|
||||
@ -180,9 +226,9 @@ impl Value {
|
||||
}
|
||||
|
||||
/// Follow a given column path into the value: for example accessing nth elements in a stream or list
|
||||
pub fn follow_cell_path(self, column_path: &[PathMember]) -> Result<Value, ShellError> {
|
||||
pub fn follow_cell_path(self, cell_path: &[PathMember]) -> Result<Value, ShellError> {
|
||||
let mut current = self;
|
||||
for member in column_path {
|
||||
for member in cell_path {
|
||||
// FIXME: this uses a few extra clones for simplicity, but there may be a way
|
||||
// to traverse the path without them
|
||||
match member {
|
||||
@ -278,6 +324,24 @@ impl Value {
|
||||
|
||||
Ok(current)
|
||||
}
|
||||
|
||||
pub fn string(s: &str, span: Span) -> Value {
|
||||
Value::String {
|
||||
val: s.into(),
|
||||
span,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_true(&self) -> bool {
|
||||
matches!(self, Value::Bool { val: true, .. })
|
||||
}
|
||||
|
||||
pub fn columns(&self) -> Vec<String> {
|
||||
match self {
|
||||
Value::Record { cols, .. } => cols.clone(),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Value {
|
||||
|
@ -1,13 +1,110 @@
|
||||
use crate::{ast::RangeInclusion, *};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cmp::Ordering;
|
||||
|
||||
/// A Range is an iterator over integers.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
use crate::{
|
||||
ast::{RangeInclusion, RangeOperator},
|
||||
*,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Range {
|
||||
pub from: Value,
|
||||
pub incr: Value,
|
||||
pub to: Value,
|
||||
pub inclusion: RangeInclusion,
|
||||
}
|
||||
|
||||
impl Range {
|
||||
pub fn new(
|
||||
expr_span: Span,
|
||||
from: Value,
|
||||
next: Value,
|
||||
to: Value,
|
||||
operator: &RangeOperator,
|
||||
) -> Result<Range, ShellError> {
|
||||
// Select from & to values if they're not specified
|
||||
// TODO: Replace the placeholder values with proper min/max based on data type
|
||||
let from = if let Value::Nothing { .. } = from {
|
||||
Value::Int {
|
||||
val: 0i64,
|
||||
span: Span::unknown(),
|
||||
}
|
||||
} else {
|
||||
from
|
||||
};
|
||||
|
||||
let to = if let Value::Nothing { .. } = to {
|
||||
if let Ok(Value::Bool { val: true, .. }) = next.lt(expr_span, &from) {
|
||||
Value::Int {
|
||||
val: -100i64,
|
||||
span: Span::unknown(),
|
||||
}
|
||||
} else {
|
||||
Value::Int {
|
||||
val: 100i64,
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
to
|
||||
};
|
||||
|
||||
// Check if the range counts up or down
|
||||
let moves_up = matches!(from.lte(expr_span, &to), Ok(Value::Bool { val: true, .. }));
|
||||
|
||||
// Convert the next value into the inctement
|
||||
let incr = if let Value::Nothing { .. } = next {
|
||||
if moves_up {
|
||||
Value::Int {
|
||||
val: 1i64,
|
||||
span: Span::unknown(),
|
||||
}
|
||||
} else {
|
||||
Value::Int {
|
||||
val: -1i64,
|
||||
span: Span::unknown(),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
next.sub(operator.next_op_span, &from)?
|
||||
};
|
||||
|
||||
let zero = Value::Int {
|
||||
val: 0i64,
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
// Increment must be non-zero, otherwise we iterate forever
|
||||
if matches!(incr.eq(expr_span, &zero), Ok(Value::Bool { val: true, .. })) {
|
||||
return Err(ShellError::CannotCreateRange(expr_span));
|
||||
}
|
||||
|
||||
// If to > from, then incr > 0, otherwise we iterate forever
|
||||
if let (Value::Bool { val: true, .. }, Value::Bool { val: false, .. }) = (
|
||||
to.gt(operator.span, &from)?,
|
||||
incr.gt(operator.next_op_span, &zero)?,
|
||||
) {
|
||||
return Err(ShellError::CannotCreateRange(expr_span));
|
||||
}
|
||||
|
||||
// If to < from, then incr < 0, otherwise we iterate forever
|
||||
if let (Value::Bool { val: true, .. }, Value::Bool { val: false, .. }) = (
|
||||
to.lt(operator.span, &from)?,
|
||||
incr.lt(operator.next_op_span, &zero)?,
|
||||
) {
|
||||
return Err(ShellError::CannotCreateRange(expr_span));
|
||||
}
|
||||
|
||||
Ok(Range {
|
||||
from,
|
||||
incr,
|
||||
to,
|
||||
inclusion: operator.inclusion,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for Range {
|
||||
type Item = Value;
|
||||
|
||||
@ -26,8 +123,7 @@ pub struct RangeIterator {
|
||||
span: Span,
|
||||
is_end_inclusive: bool,
|
||||
moves_up: bool,
|
||||
one: Value,
|
||||
negative_one: Value,
|
||||
incr: Value,
|
||||
done: bool,
|
||||
}
|
||||
|
||||
@ -53,41 +149,66 @@ impl RangeIterator {
|
||||
span,
|
||||
is_end_inclusive: matches!(range.inclusion, RangeInclusion::Inclusive),
|
||||
done: false,
|
||||
one: Value::Int { val: 1, span },
|
||||
negative_one: Value::Int { val: -1, span },
|
||||
incr: range.incr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compare two floating point numbers. The decision interval for equality is dynamically scaled
|
||||
// as the value being compared increases in magnitude.
|
||||
fn compare_floats(val: f64, other: f64) -> Option<Ordering> {
|
||||
let prec = f64::EPSILON.max(val.abs() * f64::EPSILON);
|
||||
|
||||
if (other - val).abs() < prec {
|
||||
return Some(Ordering::Equal);
|
||||
}
|
||||
|
||||
val.partial_cmp(&other)
|
||||
}
|
||||
|
||||
impl Iterator for RangeIterator {
|
||||
type Item = Value;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
use std::cmp::Ordering;
|
||||
if self.done {
|
||||
return None;
|
||||
}
|
||||
|
||||
let ordering = if matches!(self.end, Value::Nothing { .. }) {
|
||||
Ordering::Less
|
||||
Some(Ordering::Less)
|
||||
} else {
|
||||
match (&self.curr, &self.end) {
|
||||
(Value::Int { val: x, .. }, Value::Int { val: y, .. }) => x.cmp(y),
|
||||
// (Value::Float { val: x, .. }, Value::Float { val: y, .. }) => x.cmp(y),
|
||||
// (Value::Float { val: x, .. }, Value::Int { val: y, .. }) => x.cmp(y),
|
||||
// (Value::Int { val: x, .. }, Value::Float { val: y, .. }) => x.cmp(y),
|
||||
_ => {
|
||||
self.done = true;
|
||||
return Some(Value::Error {
|
||||
error: ShellError::CannotCreateRange(self.span),
|
||||
});
|
||||
(Value::Int { val: curr, .. }, Value::Int { val: end, .. }) => Some(curr.cmp(end)),
|
||||
(Value::Float { val: curr, .. }, Value::Float { val: end, .. }) => {
|
||||
compare_floats(*curr, *end)
|
||||
}
|
||||
(Value::Float { val: curr, .. }, Value::Int { val: end, .. }) => {
|
||||
compare_floats(*curr, *end as f64)
|
||||
}
|
||||
(Value::Int { val: curr, .. }, Value::Float { val: end, .. }) => {
|
||||
compare_floats(*curr as f64, *end)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
|
||||
if self.moves_up
|
||||
&& (ordering == Ordering::Less || self.is_end_inclusive && ordering == Ordering::Equal)
|
||||
let ordering = if let Some(ord) = ordering {
|
||||
ord
|
||||
} else {
|
||||
self.done = true;
|
||||
return Some(Value::Error {
|
||||
error: ShellError::CannotCreateRange(self.span),
|
||||
});
|
||||
};
|
||||
|
||||
let desired_ordering = if self.moves_up {
|
||||
Ordering::Less
|
||||
} else {
|
||||
Ordering::Greater
|
||||
};
|
||||
|
||||
if (ordering == desired_ordering) || (self.is_end_inclusive && ordering == Ordering::Equal)
|
||||
{
|
||||
let next_value = self.curr.add(self.span, &self.one);
|
||||
let next_value = self.curr.add(self.span, &self.incr);
|
||||
|
||||
let mut next = match next_value {
|
||||
Ok(result) => result,
|
||||
@ -99,22 +220,6 @@ impl Iterator for RangeIterator {
|
||||
};
|
||||
std::mem::swap(&mut self.curr, &mut next);
|
||||
|
||||
Some(next)
|
||||
} else if !self.moves_up
|
||||
&& (ordering == Ordering::Greater
|
||||
|| self.is_end_inclusive && ordering == Ordering::Equal)
|
||||
{
|
||||
let next_value = self.curr.add(self.span, &self.negative_one);
|
||||
|
||||
let mut next = match next_value {
|
||||
Ok(result) => result,
|
||||
Err(error) => {
|
||||
self.done = true;
|
||||
return Some(Value::Error { error });
|
||||
}
|
||||
};
|
||||
std::mem::swap(&mut self.curr, &mut next);
|
||||
|
||||
Some(next)
|
||||
} else {
|
||||
None
|
||||
|
@ -1,6 +1,8 @@
|
||||
use crate::*;
|
||||
use std::{cell::RefCell, fmt::Debug, rc::Rc};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ValueStream(pub Rc<RefCell<dyn Iterator<Item = Value>>>);
|
||||
|
||||
@ -14,6 +16,12 @@ impl ValueStream {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn collect_string(self) -> String {
|
||||
self.map(|x: Value| x.collect_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
pub fn from_stream(input: impl Iterator<Item = Value> + 'static) -> ValueStream {
|
||||
ValueStream(Rc::new(RefCell::new(input)))
|
||||
}
|
||||
@ -30,12 +38,31 @@ impl Iterator for ValueStream {
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
{
|
||||
let mut iter = self.0.borrow_mut();
|
||||
iter.next()
|
||||
self.0.borrow_mut().next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ValueStream {
|
||||
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
// FIXME: implement these
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ValueStream {
|
||||
fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
// FIXME: implement these
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoValueStream {
|
||||
fn into_value_stream(self) -> ValueStream;
|
||||
}
|
||||
|
Reference in New Issue
Block a user