mirror of
https://github.com/tweag/nickel.git
synced 2024-11-10 10:46:49 +03:00
Add the null value
This commit is contained in:
parent
9592d7935b
commit
761127879f
16
src/error.rs
16
src/error.rs
@ -8,6 +8,7 @@ use crate::label::ty_path;
|
||||
use crate::parser::lexer::LexicalError;
|
||||
use crate::parser::utils::mk_span;
|
||||
use crate::position::RawSpan;
|
||||
use crate::serialize::ExportFormat;
|
||||
use crate::term::RichTerm;
|
||||
use crate::types::Types;
|
||||
use crate::{label, repl};
|
||||
@ -224,6 +225,10 @@ pub enum ImportError {
|
||||
/// An error occurred during serialization.
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum SerializationError {
|
||||
/// Encountered a null value for a format that doesn't support them.
|
||||
UnsupportedNull(ExportFormat, RichTerm),
|
||||
/// Tried exporting something else than a `Str` to raw format.
|
||||
NotAString(RichTerm),
|
||||
/// A term contains constructs that cannot be serialized.
|
||||
NonSerializable(RichTerm),
|
||||
Other(String),
|
||||
@ -1308,6 +1313,17 @@ impl ToDiagnostic<FileId> for SerializationError {
|
||||
_contract_id: Option<FileId>,
|
||||
) -> Vec<Diagnostic<FileId>> {
|
||||
match self {
|
||||
SerializationError::NotAString(rt) => vec![Diagnostic::error()
|
||||
.with_message(format!(
|
||||
"raw export only supports `Str`, got {}",
|
||||
rt.as_ref()
|
||||
.type_of()
|
||||
.unwrap_or(String::from("<unevaluated>"))
|
||||
))
|
||||
.with_labels(vec![primary_term(&rt, files)])],
|
||||
SerializationError::UnsupportedNull(format, rt) => vec![Diagnostic::error()
|
||||
.with_message(format!("{} doesn't support null values", format))
|
||||
.with_labels(vec![primary_term(&rt, files)])],
|
||||
SerializationError::NonSerializable(rt) => vec![Diagnostic::error()
|
||||
.with_message("non serializable term")
|
||||
.with_labels(vec![primary_term(&rt, files)])],
|
||||
|
@ -785,7 +785,8 @@ pub fn subst(rt: RichTerm, global_env: &Environment, env: &Environment) -> RichT
|
||||
subst_(closure.body, global_env, &closure.env, bound)
|
||||
})
|
||||
.unwrap_or_else(|| RichTerm::new(Term::Var(id), pos)),
|
||||
v @ Term::Bool(_)
|
||||
v @ Term::Null
|
||||
| v @ Term::Bool(_)
|
||||
| v @ Term::Num(_)
|
||||
| v @ Term::Str(_)
|
||||
| v @ Term::Lbl(_)
|
||||
|
@ -237,6 +237,7 @@ Atom: RichTerm = {
|
||||
)
|
||||
),
|
||||
"num literal" => RichTerm::from(Term::Num(<>)),
|
||||
"null" => RichTerm::from(Term::Null),
|
||||
Bool => RichTerm::from(Term::Bool(<>)),
|
||||
<StrChunks>,
|
||||
Ident => RichTerm::from(Term::Var(<>)),
|
||||
@ -616,6 +617,7 @@ extern {
|
||||
"let" => Token::Normal(NormalToken::Let),
|
||||
"switch" => Token::Normal(NormalToken::Switch),
|
||||
|
||||
"null" => Token::Normal(NormalToken::Null),
|
||||
"true" => Token::Normal(NormalToken::True),
|
||||
"false" => Token::Normal(NormalToken::False),
|
||||
|
||||
|
14
src/main.rs
14
src/main.rs
@ -21,6 +21,7 @@ mod types;
|
||||
use crate::error::{Error, IOError, SerializationError};
|
||||
use crate::program::Program;
|
||||
use crate::repl::rustyline_frontend;
|
||||
use crate::serialize::ExportFormat;
|
||||
use crate::term::{RichTerm, Term};
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
@ -43,15 +44,6 @@ struct Opt {
|
||||
command: Option<Command>,
|
||||
}
|
||||
|
||||
/// Available export formats.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||||
enum ExportFormat {
|
||||
Raw,
|
||||
Json,
|
||||
Yaml,
|
||||
Toml,
|
||||
}
|
||||
|
||||
impl std::default::Default for ExportFormat {
|
||||
fn default() -> Self {
|
||||
ExportFormat::Json
|
||||
@ -187,10 +179,10 @@ fn export(
|
||||
output: Option<PathBuf>,
|
||||
) -> Result<(), Error> {
|
||||
let rt = program.eval_full().map(RichTerm::from)?;
|
||||
serialize::validate(&rt)?;
|
||||
|
||||
let format = format.unwrap_or_default();
|
||||
|
||||
serialize::validate(&rt, format)?;
|
||||
|
||||
if let Some(file) = output {
|
||||
let mut file = fs::File::create(&file).map_err(IOError::from)?;
|
||||
|
||||
|
@ -69,6 +69,8 @@ pub enum NormalToken<'input> {
|
||||
#[token("switch")]
|
||||
Switch,
|
||||
|
||||
#[token("null")]
|
||||
Null,
|
||||
#[token("true")]
|
||||
True,
|
||||
#[token("false")]
|
||||
|
@ -6,6 +6,15 @@ use serde::de::{Deserialize, Deserializer};
|
||||
use serde::ser::{Error, Serialize, SerializeMap, Serializer};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Available export formats.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||||
pub enum ExportFormat {
|
||||
Raw,
|
||||
Json,
|
||||
Yaml,
|
||||
Toml,
|
||||
}
|
||||
|
||||
/// Serializer for metavalues.
|
||||
pub fn serialize_meta_value<S>(meta: &MetaValue, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
@ -58,25 +67,36 @@ impl<'de> Deserialize<'de> for RichTerm {
|
||||
|
||||
/// Check that a term is serializable. Serializable terms are booleans, numbers, strings, enum,
|
||||
/// lists of serializable terms or records of serializable terms.
|
||||
pub fn validate(t: &RichTerm) -> Result<(), SerializationError> {
|
||||
pub fn validate(t: &RichTerm, format: ExportFormat) -> Result<(), SerializationError> {
|
||||
use crate::term;
|
||||
use Term::*;
|
||||
|
||||
match t.term.as_ref() {
|
||||
Bool(_) | Num(_) | Str(_) | Enum(_) => Ok(()),
|
||||
Record(map) | RecRecord(map) => {
|
||||
map.iter().try_for_each(|(_, t)| validate(t))?;
|
||||
if format == ExportFormat::Raw {
|
||||
if let Term::Str(_) = t.term.as_ref() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SerializationError::NotAString(t.clone()))
|
||||
}
|
||||
List(vec) => {
|
||||
vec.iter().try_for_each(validate)?;
|
||||
Ok(())
|
||||
} else {
|
||||
match t.term.as_ref() {
|
||||
// TOML doesn't support null values
|
||||
Null if format == ExportFormat::Json || format == ExportFormat::Yaml => Ok(()),
|
||||
Null => Err(SerializationError::UnsupportedNull(format, t.clone())),
|
||||
Bool(_) | Num(_) | Str(_) | Enum(_) => Ok(()),
|
||||
Record(map) | RecRecord(map) => {
|
||||
map.iter().try_for_each(|(_, t)| validate(t, format))?;
|
||||
Ok(())
|
||||
}
|
||||
List(vec) => {
|
||||
vec.iter().try_for_each(|t| validate(t, format))?;
|
||||
Ok(())
|
||||
}
|
||||
//TODO: have a specific error for such missing value.
|
||||
MetaValue(term::MetaValue {
|
||||
value: Some(ref t), ..
|
||||
}) => validate(t, format),
|
||||
_ => Err(SerializationError::NonSerializable(t.clone())),
|
||||
}
|
||||
//TODO: have a specific error for such missing value.
|
||||
MetaValue(term::MetaValue {
|
||||
value: Some(ref t), ..
|
||||
}) => validate(t),
|
||||
_ => Err(SerializationError::NonSerializable(t.clone())),
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,13 +127,24 @@ mod tests {
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! assert_non_serializable {
|
||||
( $term:expr ) => {
|
||||
macro_rules! assert_pass_validation {
|
||||
( $term:expr, $format:expr, true) => {
|
||||
validate(
|
||||
&mk_program($term)
|
||||
.and_then(|mut p| p.eval_full())
|
||||
.unwrap()
|
||||
.into(),
|
||||
$format,
|
||||
)
|
||||
.unwrap();
|
||||
};
|
||||
( $term:expr, $format:expr, false) => {
|
||||
validate(
|
||||
&mk_program($term)
|
||||
.and_then(|mut p| p.eval_full())
|
||||
.unwrap()
|
||||
.into(),
|
||||
$format,
|
||||
)
|
||||
.unwrap_err();
|
||||
};
|
||||
@ -160,6 +191,10 @@ mod tests {
|
||||
#[test]
|
||||
fn basic() {
|
||||
assert_json_eq!("1 + 1", 2.0);
|
||||
|
||||
let null: Option<()> = None;
|
||||
assert_json_eq!("null", null);
|
||||
|
||||
assert_json_eq!("if true then false else true", false);
|
||||
assert_json_eq!(r##""Hello, #{"world"}!""##, "Hello, world!");
|
||||
assert_json_eq!("`foo", "foo");
|
||||
@ -168,7 +203,7 @@ mod tests {
|
||||
#[test]
|
||||
fn lists() {
|
||||
assert_json_eq!("[]", json!([]));
|
||||
assert_json_eq!("[(1+1), (2+2), (3+3)]", json!([2.0, 4.0, 6.0]));
|
||||
assert_json_eq!("[null, (1+1), (2+2), (3+3)]", json!([null, 2.0, 4.0, 6.0]));
|
||||
assert_json_eq!(
|
||||
r##"[`a, ("b" ++ "c"), "d#{"e"}f", "g"]"##,
|
||||
json!(["a", "bc", "def", "g"])
|
||||
@ -183,8 +218,8 @@ mod tests {
|
||||
#[test]
|
||||
fn records() {
|
||||
assert_json_eq!(
|
||||
"{a = 1; b = 2+2; c = 3}",
|
||||
json!({"a": 1.0, "b": 4.0, "c": 3.0})
|
||||
"{a = 1; b = 2+2; c = 3; d = null}",
|
||||
json!({"a": 1.0, "b": 4.0, "c": 3.0, "d": null})
|
||||
);
|
||||
|
||||
assert_json_eq!(
|
||||
@ -218,8 +253,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn prevalidation() {
|
||||
assert_non_serializable!("{a = 1; b = { c = fun x => x }}");
|
||||
assert_non_serializable!("{foo = { bar = let y = \"a\" in y}; b = [[fun x => x]] }");
|
||||
assert_pass_validation!("{a = 1; b = { c = fun x => x }}", ExportFormat::Json, false);
|
||||
assert_pass_validation!(
|
||||
"{foo = { bar = let y = \"a\" in y}; b = [[fun x => x]] }",
|
||||
ExportFormat::Json,
|
||||
false
|
||||
);
|
||||
assert_pass_validation!("{foo = null}", ExportFormat::Json, true);
|
||||
assert_pass_validation!("{foo = null}", ExportFormat::Toml, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
44
src/term.rs
44
src/term.rs
@ -33,6 +33,10 @@ use std::ffi::OsString;
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum Term {
|
||||
/// The null value.
|
||||
// #[serde(serialize_with = "crate::serialize::serialize_null")]
|
||||
// #[serde(deserialize_with = "crate::serialize::deserialize_null")]
|
||||
Null,
|
||||
/// A boolean value.
|
||||
Bool(bool),
|
||||
/// A floating-point value.
|
||||
@ -217,6 +221,7 @@ impl Term {
|
||||
{
|
||||
use self::Term::*;
|
||||
match self {
|
||||
Null => (),
|
||||
Switch(ref mut t, ref mut cases, ref mut def) => {
|
||||
cases.iter_mut().for_each(|c| {
|
||||
let (_, t) = c;
|
||||
@ -276,6 +281,7 @@ impl Term {
|
||||
/// for records, `"Fun`" for functions, etc. If the term is not a WHNF, `None` is returned.
|
||||
pub fn type_of(&self) -> Option<String> {
|
||||
match self {
|
||||
Term::Null => Some("Null"),
|
||||
Term::Bool(_) => Some("Bool"),
|
||||
Term::Num(_) => Some("Num"),
|
||||
Term::Str(_) => Some("Str"),
|
||||
@ -305,6 +311,7 @@ impl Term {
|
||||
/// Return a shallow string representation of a term, used for error reporting.
|
||||
pub fn shallow_repr(&self) -> String {
|
||||
match self {
|
||||
Term::Null => String::from("null"),
|
||||
Term::Bool(true) => String::from("true"),
|
||||
Term::Bool(false) => String::from("false"),
|
||||
Term::Num(n) => format!("{}", n),
|
||||
@ -367,7 +374,8 @@ impl Term {
|
||||
/// Determine if a term is in evaluated from, called weak head normal form (WHNF).
|
||||
pub fn is_whnf(&self) -> bool {
|
||||
match self {
|
||||
Term::Bool(_)
|
||||
Term::Null
|
||||
| Term::Bool(_)
|
||||
| Term::Num(_)
|
||||
| Term::Str(_)
|
||||
| Term::Fun(_, _)
|
||||
@ -395,40 +403,17 @@ impl Term {
|
||||
|
||||
/// Determine if a term is an enriched value.
|
||||
pub fn is_enriched(&self) -> bool {
|
||||
match self {
|
||||
Term::MetaValue(_) => true,
|
||||
Term::Bool(_)
|
||||
| Term::Num(_)
|
||||
| Term::Str(_)
|
||||
| Term::StrChunks(_)
|
||||
| Term::Fun(_, _)
|
||||
| Term::Lbl(_)
|
||||
| Term::Enum(_)
|
||||
| Term::Record(_)
|
||||
| Term::RecRecord(_)
|
||||
| Term::List(_)
|
||||
| Term::Sym(_)
|
||||
| Term::Wrapped(_, _)
|
||||
| Term::Let(_, _, _)
|
||||
| Term::App(_, _)
|
||||
| Term::Switch(..)
|
||||
| Term::Var(_)
|
||||
| Term::Op1(_, _)
|
||||
| Term::Op2(_, _, _)
|
||||
| Term::Promise(_, _, _)
|
||||
| Term::Assume(_, _, _)
|
||||
| Term::Import(_)
|
||||
| Term::ResolvedImport(_) => false,
|
||||
}
|
||||
matches!(self, Term::MetaValue(..))
|
||||
}
|
||||
|
||||
/// Determine if a term is a constant.
|
||||
///
|
||||
/// In this context, a constant is an atomic literal of the language: a boolean, a number, a
|
||||
/// In this context, a constant is an atomic literal of the language: null, a boolean, a number, a
|
||||
/// string, a label, an enum tag or a symbol.
|
||||
pub fn is_constant(&self) -> bool {
|
||||
match self {
|
||||
Term::Bool(_)
|
||||
Term::Null
|
||||
| Term::Bool(_)
|
||||
| Term::Num(_)
|
||||
| Term::Str(_)
|
||||
| Term::Lbl(_)
|
||||
@ -677,7 +662,8 @@ impl RichTerm {
|
||||
{
|
||||
let RichTerm { term, pos } = self;
|
||||
match *term {
|
||||
v @ Term::Bool(_)
|
||||
v @ Term::Null
|
||||
| v @ Term::Bool(_)
|
||||
| v @ Term::Num(_)
|
||||
| v @ Term::Str(_)
|
||||
| v @ Term::Lbl(_)
|
||||
|
@ -143,7 +143,8 @@ pub mod share_normal_form {
|
||||
/// subexpressions, such as a record, should be shared.
|
||||
fn should_share(t: &Term) -> bool {
|
||||
match t {
|
||||
Term::Bool(_)
|
||||
Term::Null
|
||||
| Term::Bool(_)
|
||||
| Term::Num(_)
|
||||
| Term::Str(_)
|
||||
| Term::Lbl(_)
|
||||
|
@ -496,6 +496,9 @@ fn type_check_(
|
||||
let RichTerm { term: t, pos } = rt;
|
||||
|
||||
match t.as_ref() {
|
||||
// null is inferred to be of type Dyn
|
||||
Term::Null => unify(state, strict, ty, mk_typewrapper::dynamic())
|
||||
.map_err(|err| err.into_typecheck_err(state, &rt.pos)),
|
||||
Term::Bool(_) => unify(state, strict, ty, mk_typewrapper::bool())
|
||||
.map_err(|err| err.into_typecheck_err(state, &rt.pos)),
|
||||
Term::Num(_) => unify(state, strict, ty, mk_typewrapper::num())
|
||||
|
Loading…
Reference in New Issue
Block a user