1
1
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:
Yann Hamdaoui 2021-02-11 18:39:52 +01:00
parent 9592d7935b
commit 761127879f
9 changed files with 106 additions and 62 deletions

View File

@ -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)])],

View File

@ -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(_)

View File

@ -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),

View File

@ -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)?;

View File

@ -69,6 +69,8 @@ pub enum NormalToken<'input> {
#[token("switch")]
Switch,
#[token("null")]
Null,
#[token("true")]
True,
#[token("false")]

View File

@ -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]

View File

@ -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(_)

View File

@ -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(_)

View File

@ -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())