Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stdlib array extension #1195

Merged
merged 12 commits into from
Mar 23, 2023
113 changes: 112 additions & 1 deletion src/eval/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2305,7 +2305,6 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
MergeMode::Standard,
&mut self.call_stack,
),

BinaryOp::Hash() => {
let mk_err_fst = |t1| {
Err(EvalError::TypeError(
Expand Down Expand Up @@ -3280,6 +3279,118 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
pos2.into_inherited(),
)))
}
NAryOp::ArraySlice() => {
let mut args = args.into_iter();

let (
Closure {
body:
RichTerm {
term: t1,
pos: pos1,
},
..
},
fst_pos,
) = args.next().unwrap();

let (
Closure {
body:
RichTerm {
term: t2,
pos: pos2,
},
..
},
snd_pos,
) = args.next().unwrap();

let (
Closure {
body:
RichTerm {
term: t3,
pos: pos3,
},
env: env3,
},
third_pos,
) = args.next().unwrap();
debug_assert!(args.next().is_none());

let Term::Num(ref start) = &*t1 else {
return Err(EvalError::TypeError(
String::from("Number"),
String::from("%array_slice%, 1st argument"),
fst_pos,
RichTerm {
term: t1,
pos: pos1,
},
));
};

let Term::Num(ref end) = &*t2 else {
return Err(EvalError::TypeError(
String::from("Number"),
String::from("%array_slice%, 2nd argument"),
snd_pos,
RichTerm {
term: t2,
pos: pos2,
},
));
};

let t3_owned = t3.into_owned();

let Term::Array(mut array, attrs) = t3_owned else {
return Err(EvalError::TypeError(
String::from("Array"),
String::from("%array_slice%, 3rd argument"),
third_pos,
RichTerm::new(t3_owned, pos3),
));
};

let Ok(start_as_usize) = usize::try_from(start) else {
return Err(EvalError::Other(
format!(
"%array_slice%: expected the 1st argument (start) to be a positive integer smaller than {}, got {start}",
usize::MAX
),
pos_op
));
};

let Ok(end_as_usize) = usize::try_from(end) else {
return Err(EvalError::Other(
format!(
"%array_slice%: expected the 2nd argument (end) to be a positive integer smaller than {}, got {end}",
usize::MAX
),
pos_op
));
};

let result = array.slice(start_as_usize, end_as_usize);

if let Err(crate::term::array::OutOfBoundError) = result {
return Err(EvalError::Other(
format!(
"%array_slice%: index out of bounds. Expected `start <= end <= {}`, but got `start={start}` and `end={end}`.",
array.len()
),
pos_op
));
};

Ok(Closure {
body: RichTerm::new(Term::Array(array, attrs), pos_op_inh),
env: env3,
})
}
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/parser/grammar.lalrpop
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,8 @@ NOpPre<ArgRule>: UniTerm = {
UniTerm::from(mk_opn!(NAryOp::RecordUnsealTail(), t1, t2, t3)),
"insert_type_variable" <key: ArgRule> <pol: ArgRule> <label: ArgRule> =>
UniTerm::from(mk_opn!(NAryOp::InsertTypeVar(), key, pol, label)),
"array_slice" <t1: ArgRule> <t2: ArgRule> <t3: ArgRule> =>
UniTerm::from(mk_opn!(NAryOp::ArraySlice(), t1, t2, t3)),
}

TypeBuiltin: Types = {
Expand Down Expand Up @@ -1072,6 +1074,7 @@ extern {
"label_with_notes" => Token::Normal(NormalToken::LabelWithNotes),
"label_append_note" => Token::Normal(NormalToken::LabelAppendNote),
"label_push_diag" => Token::Normal(NormalToken::LabelPushDiag),
"array_slice" => Token::Normal(NormalToken::ArraySlice),

"{" => Token::Normal(NormalToken::LBrace),
"}" => Token::Normal(NormalToken::RBrace),
Expand Down
2 changes: 2 additions & 0 deletions src/parser/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,8 @@ pub enum NormalToken<'input> {
LabelAppendNote,
#[token("%label_push_diag%")]
LabelPushDiag,
#[token("%array_slice%")]
ArraySlice,

#[token("{")]
LBrace,
Expand Down
8 changes: 7 additions & 1 deletion src/stdlib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::term::make as mk_term;
use crate::term::RichTerm;

/// This is an array containing all the Nickel standard library modules.
pub fn modules() -> [StdlibModule; 8] {
pub fn modules() -> [StdlibModule; 9] {
[
StdlibModule::Builtin,
StdlibModule::Contract,
Expand All @@ -14,6 +14,7 @@ pub fn modules() -> [StdlibModule; 8] {
StdlibModule::String,
StdlibModule::Number,
StdlibModule::Function,
StdlibModule::Enum,
StdlibModule::Internals,
]
}
Expand All @@ -28,6 +29,7 @@ pub enum StdlibModule {
String,
Number,
Function,
Enum,
Internals,
}

Expand All @@ -41,6 +43,7 @@ impl StdlibModule {
StdlibModule::String => "<stdlib/string.ncl>",
StdlibModule::Number => "<stdlib/number.ncl>",
StdlibModule::Function => "<stdlib/function.ncl>",
StdlibModule::Enum => "<stdlib/enum.ncl>",
StdlibModule::Internals => "<stdlib/internals.ncl>",
}
}
Expand All @@ -54,6 +57,7 @@ impl StdlibModule {
StdlibModule::String => include_str!("../stdlib/string.ncl"),
StdlibModule::Number => include_str!("../stdlib/number.ncl"),
StdlibModule::Function => include_str!("../stdlib/function.ncl"),
StdlibModule::Enum => include_str!("../stdlib/enum.ncl"),
StdlibModule::Internals => include_str!("../stdlib/internals.ncl"),
}
}
Expand All @@ -73,6 +77,7 @@ impl TryFrom<Ident> for StdlibModule {
"string" => StdlibModule::String,
"number" => StdlibModule::Number,
"function" => StdlibModule::Function,
"enum" => StdlibModule::Enum,
"internals" => StdlibModule::Internals,
_ => return Err(UnknownStdlibModule),
};
Expand All @@ -90,6 +95,7 @@ impl From<StdlibModule> for Ident {
StdlibModule::String => "string",
StdlibModule::Number => "number",
StdlibModule::Function => "function",
StdlibModule::Enum => "enum",
StdlibModule::Internals => "internals",
};
Ident::from(name)
Expand Down
23 changes: 23 additions & 0 deletions src/term/array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,19 @@ impl ArrayAttrs {
}
}

/// A Nickel array, represented as a view (slice) into a shared backing array. The view is
/// delimited by `start` (included) and `end` (excluded). This allows to take the tail of an array,
/// or an arbitrary slice, in constant time, providing actual linear time iteration when
/// imlementing recursive functions, such as folds, for example.
#[derive(Debug, Clone, PartialEq)]
pub struct Array {
inner: Rc<[RichTerm]>,
start: usize,
end: usize,
}

pub struct OutOfBoundError;

impl Array {
/// Creates a Nickel array from reference-counted slice.
pub fn new(inner: Rc<[RichTerm]>) -> Self {
Expand All @@ -69,6 +75,23 @@ impl Array {
Self { inner, start, end }
}

/// Resize the view to be a a sub-view of the current one, by considering a slice `start`
/// (included) to `end` (excluded).
///
/// The parameters must satisfy `0 <= start <= end <= self.end - self.start`. Otherwise,
/// `Err(..)` is returned.
pub fn slice(&mut self, start: usize, end: usize) -> Result<(), OutOfBoundError> {
if start > end || end > self.len() {
return Err(OutOfBoundError);
}

let prev_start = self.start;
self.start = prev_start + start;
self.end = prev_start + end;

Ok(())
}

/// Returns the effective length of the array.
pub fn len(&self) -> usize {
self.end - self.start
Expand Down
20 changes: 13 additions & 7 deletions src/term/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1375,6 +1375,10 @@ pub enum NAryOp {
/// - the [kind](types::VarKind) of the type variable
/// - a [label](Term::Label) on which to operate
InsertTypeVar(),
/// Return a sub-array corresponding to a range. Given that Nickel uses array slices under the
/// hood, as long as the array isn't modified later, this operation is constant in time and
/// memory.
ArraySlice(),
}

impl NAryOp {
Expand All @@ -1385,7 +1389,8 @@ impl NAryOp {
| NAryOp::StrSubstr()
| NAryOp::MergeContract()
| NAryOp::RecordUnsealTail()
| NAryOp::InsertTypeVar() => 3,
| NAryOp::InsertTypeVar()
| NAryOp::ArraySlice() => 3,
NAryOp::RecordSealTail() => 4,
}
}
Expand All @@ -1394,13 +1399,14 @@ impl NAryOp {
impl fmt::Display for NAryOp {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
NAryOp::StrReplace() => write!(f, "strReplace"),
NAryOp::StrReplaceRegex() => write!(f, "strReplaceRegex"),
NAryOp::StrReplace() => write!(f, "str_replace"),
NAryOp::StrReplaceRegex() => write!(f, "str_replace_regex"),
NAryOp::StrSubstr() => write!(f, "substring"),
NAryOp::MergeContract() => write!(f, "mergeContract"),
NAryOp::RecordSealTail() => write!(f, "%record_seal_tail%"),
NAryOp::RecordUnsealTail() => write!(f, "%record_unseal_tail%"),
NAryOp::InsertTypeVar() => write!(f, "%insert_type_variable%"),
NAryOp::MergeContract() => write!(f, "merge_contract"),
NAryOp::RecordSealTail() => write!(f, "record_seal_tail"),
NAryOp::RecordUnsealTail() => write!(f, "record_unseal_tail"),
NAryOp::InsertTypeVar() => write!(f, "insert_type_variable"),
NAryOp::ArraySlice() => write!(f, "array_slice"),
}
}
}
Expand Down
15 changes: 14 additions & 1 deletion src/typecheck/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ pub fn get_bop_type(
}

pub fn get_nop_type(
_state: &mut State,
state: &mut State,
op: &NAryOp,
) -> Result<(Vec<UnifType>, UnifType), TypecheckError> {
Ok(match op {
Expand Down Expand Up @@ -470,6 +470,19 @@ pub fn get_nop_type(
],
mk_uniftype::dynamic(),
),
// Num -> Num -> Array a -> Array a
NAryOp::ArraySlice() => {
let element_type = state.table.fresh_type_uvar();

(
vec![
mk_uniftype::num(),
mk_uniftype::num(),
mk_uniftype::array(element_type.clone()),
],
mk_uniftype::array(element_type),
)
}
// This should not happen, as MergeContract() is only produced during evaluation.
NAryOp::MergeContract() => panic!("cannot typecheck MergeContract()"),
// Morally: Sym -> Polarity -> Lbl -> Lbl
Expand Down