diff --git a/core/src/eval/mod.rs b/core/src/eval/mod.rs index b8f7bd6ba5..8bad8a4eae 100644 --- a/core/src/eval/mod.rs +++ b/core/src/eval/mod.rs @@ -1157,6 +1157,7 @@ pub fn subst( // loop. Although avoidable, this requires some care and is not currently needed. | v @ Term::Fun(..) | v @ Term::Lbl(_) + | v @ Term::ForeignId(_) | v @ Term::SealingKey(_) | v @ Term::Enum(_) | v @ Term::Import(_) diff --git a/core/src/eval/operation.rs b/core/src/eval/operation.rs index fa88a69ed2..ee435cab3e 100644 --- a/core/src/eval/operation.rs +++ b/core/src/eval/operation.rs @@ -235,6 +235,7 @@ impl VirtualMachine { Term::Array(..) => "Array", Term::Record(..) | Term::RecRecord(..) => "Record", Term::Lbl(..) => "Label", + Term::ForeignId(_) => "ForeignId", _ => "Other", }; Ok(Closure::atomic_closure(RichTerm::new( @@ -3439,6 +3440,14 @@ fn eq( eq_pos: pos_op, term: RichTerm::new(Term::Fun(i, rt), pos2), }), + (Term::ForeignId(v), _) => Err(EvalError::EqError { + eq_pos: pos_op, + term: RichTerm::new(Term::ForeignId(v), pos1), + }), + (_, Term::ForeignId(v)) => Err(EvalError::EqError { + eq_pos: pos_op, + term: RichTerm::new(Term::ForeignId(v), pos2), + }), (_, _) => Ok(EqResult::Bool(false)), } } diff --git a/core/src/eval/tests.rs b/core/src/eval/tests.rs index 09cf5614ff..d749feb6d9 100644 --- a/core/src/eval/tests.rs +++ b/core/src/eval/tests.rs @@ -8,7 +8,8 @@ use crate::term::make as mk_term; use crate::term::Number; use crate::term::{BinaryOp, StrChunk, UnaryOp}; use crate::transform::import_resolution::strict::resolve_imports; -use crate::{mk_app, mk_fun}; +use crate::{mk_app, mk_fun, mk_record}; +use assert_matches::assert_matches; use codespan::Files; /// Evaluate a term without import support. @@ -18,6 +19,13 @@ fn eval_no_import(t: RichTerm) -> Result { .map(Term::from) } +/// Fully evaluate a term without import support. +fn eval_full_no_import(t: RichTerm) -> Result { + VirtualMachine::<_, CacheImpl>::new(DummyResolver {}, std::io::sink()) + .eval_full(t) + .map(Term::from) +} + fn parse(s: &str) -> Option { let id = Files::new().add("", String::from(s)); @@ -347,3 +355,45 @@ fn substitution() { .to_string() ); } + +#[test] +fn foreign_id() { + let t = mk_term::op2( + BinaryOp::Merge(Label::default().into()), + mk_record!(("a", RichTerm::from(Term::Num(Number::from(1))))), + mk_record!(("b", RichTerm::from(Term::ForeignId(42)))), + ); + + // Terms that include foreign ids can be manipulated like normal, and the ids + // are passed through. + let Term::Record(data) = eval_no_import(t.clone()).unwrap() else { + panic!(); + }; + let b = LocIdent::from(Ident::new("b")); + let field = data.fields.get(&b).unwrap(); + assert_matches!(field.value.as_ref().unwrap().as_ref(), Term::ForeignId(42)); + + // Foreign ids cannot be compared for equality. + let t_eq = mk_term::op2( + BinaryOp::Eq(), + RichTerm::from(Term::ForeignId(43)), + RichTerm::from(Term::ForeignId(42)), + ); + assert_matches!(eval_no_import(t_eq), Err(EvalError::EqError { .. })); + + // Opaque values cannot be merged (even if they're equal, since they can't get compared for equality). + let t_merge = mk_term::op2( + BinaryOp::Merge(Label::default().into()), + t.clone(), + t.clone(), + ); + assert_matches!( + eval_full_no_import(t_merge), + Err(EvalError::MergeIncompatibleArgs { .. }) + ); + + let t_typeof = mk_term::op1(UnaryOp::Typeof(), Term::ForeignId(42)); + let ty = eval_no_import(t_typeof).unwrap(); + let fid = LocIdent::from(Ident::new("ForeignId")); + assert_matches!(ty, Term::Enum(f) if f == fid); +} diff --git a/core/src/parser/uniterm.rs b/core/src/parser/uniterm.rs index afa4d0c0ac..07b1bb8c38 100644 --- a/core/src/parser/uniterm.rs +++ b/core/src/parser/uniterm.rs @@ -692,6 +692,7 @@ impl FixTypeVars for Type { | TypeF::Number | TypeF::Bool | TypeF::String + | TypeF::ForeignId | TypeF::Symbol | TypeF::Flat(_) // We don't fix type variables inside a dictionary contract. A dictionary contract diff --git a/core/src/pretty.rs b/core/src/pretty.rs index 3bd179007e..f7d9e1fa2d 100644 --- a/core/src/pretty.rs +++ b/core/src/pretty.rs @@ -975,6 +975,7 @@ where .nest(2) ] .group(), + ForeignId(_) => allocator.text("%"), SealingKey(sym) => allocator.text(format!("%")), Sealed(_i, _rt, _lbl) => allocator.text("%"), Annotated(annot, rt) => allocator.atom(rt).append(annot.pretty(allocator)), @@ -1110,6 +1111,7 @@ where ] } .group(), + ForeignId => allocator.text("ForeignId"), Symbol => allocator.text("Symbol"), Flat(t) => t.pretty(allocator), Var(var) => allocator.as_string(var), diff --git a/core/src/stdlib.rs b/core/src/stdlib.rs index bcec753450..c2c1a54053 100644 --- a/core/src/stdlib.rs +++ b/core/src/stdlib.rs @@ -71,6 +71,7 @@ pub mod internals { generate_accessor!(num); generate_accessor!(bool); + generate_accessor!(foreign_id); generate_accessor!(string); generate_accessor!(fail); diff --git a/core/src/term/mod.rs b/core/src/term/mod.rs index d63496d4d1..6be4408ffb 100644 --- a/core/src/term/mod.rs +++ b/core/src/term/mod.rs @@ -58,6 +58,9 @@ use std::{ rc::Rc, }; +/// The payload of a `Term::ForeignId`. +pub type ForeignIdPayload = u64; + /// The AST of a Nickel expression. /// /// Parsed terms also need to store their position in the source for error reporting. This is why @@ -261,6 +264,13 @@ pub enum Term { /// /// This is a temporary solution, and will be removed in the future. Closure(CacheIndex), + + #[serde(skip)] + /// An opaque value that cannot be constructed within Nickel code. + /// + /// This can be used by programs that embed Nickel, as they can inject these opaque + /// values into the AST. + ForeignId(ForeignIdPayload), } // PartialEq is mostly used for tests, when it's handy to compare something to an expected result. @@ -872,6 +882,7 @@ impl Term { Term::SealingKey(_) => Some("SealingKey".to_owned()), Term::Sealed(..) => Some("Sealed".to_owned()), Term::Annotated(..) => Some("Annotated".to_owned()), + Term::ForeignId(_) => Some("ForeignId".to_owned()), Term::Let(..) | Term::LetPattern(..) | Term::App(_, _) @@ -918,6 +929,7 @@ impl Term { | Term::EnumVariant {..} | Term::Record(..) | Term::Array(..) + | Term::ForeignId(_) | Term::SealingKey(_) => true, Term::Let(..) | Term::LetPattern(..) @@ -975,6 +987,7 @@ impl Term { | Term::Str(_) | Term::Lbl(_) | Term::Enum(_) + | Term::ForeignId(_) | Term::SealingKey(_) => true, Term::Let(..) | Term::LetPattern(..) @@ -1017,6 +1030,7 @@ impl Term { | Term::Array(..) | Term::Var(..) | Term::SealingKey(..) + | Term::ForeignId(..) | Term::Op1(UnaryOp::StaticAccess(_), _) | Term::Op2(BinaryOp::DynAccess(), _, _) // Those special cases aren't really atoms, but mustn't be parenthesized because they @@ -2169,6 +2183,7 @@ impl Traverse for RichTerm { | Term::Import(_) | Term::ResolvedImport(_) | Term::SealingKey(_) + | Term::ForeignId(_) | Term::ParseError(_) | Term::RuntimeError(_) => None, Term::StrChunks(chunks) => chunks.iter().find_map(|ch| { diff --git a/core/src/transform/free_vars.rs b/core/src/transform/free_vars.rs index e8f86c7d92..4339a3f590 100644 --- a/core/src/transform/free_vars.rs +++ b/core/src/transform/free_vars.rs @@ -39,6 +39,7 @@ impl CollectFreeVars for RichTerm { | Term::Num(_) | Term::Str(_) | Term::Lbl(_) + | Term::ForeignId(_) | Term::SealingKey(_) | Term::Enum(_) | Term::Import(_) @@ -186,6 +187,7 @@ impl CollectFreeVars for Type { | TypeF::Number | TypeF::Bool | TypeF::String + | TypeF::ForeignId | TypeF::Symbol | TypeF::Var(_) | TypeF::Wildcard(_) => (), diff --git a/core/src/typ.rs b/core/src/typ.rs index 10f9caade6..a0ab19eddc 100644 --- a/core/src/typ.rs +++ b/core/src/typ.rs @@ -269,6 +269,8 @@ pub enum TypeF { /// /// See [`crate::term::Term::Sealed`]. Symbol, + /// The type of `Term::ForeignId`. + ForeignId, /// A type created from a user-defined contract. Flat(RichTerm), /// A function. @@ -543,6 +545,7 @@ impl TypeF { TypeF::Number => Ok(TypeF::Number), TypeF::Bool => Ok(TypeF::Bool), TypeF::String => Ok(TypeF::String), + TypeF::ForeignId => Ok(TypeF::ForeignId), TypeF::Symbol => Ok(TypeF::Symbol), TypeF::Flat(t) => Ok(TypeF::Flat(t)), TypeF::Arrow(dom, codom) => Ok(TypeF::Arrow(f(dom, state)?, f(codom, state)?)), @@ -818,6 +821,7 @@ impl Subcontract for Type { TypeF::Number => internals::num(), TypeF::Bool => internals::bool(), TypeF::String => internals::string(), + TypeF::ForeignId => internals::foreign_id(), // Array Dyn is specialized to array_dyn, which is constant time TypeF::Array(ref ty) if matches!(ty.typ, TypeF::Dyn) => internals::array_dyn(), TypeF::Array(ref ty) => mk_app!(internals::array(), ty.subcontract(vars, pol, sy)?), @@ -1402,6 +1406,7 @@ impl Traverse for Type { | TypeF::Number | TypeF::Bool | TypeF::String + | TypeF::ForeignId | TypeF::Symbol | TypeF::Var(_) | TypeF::Enum(_) diff --git a/core/src/typecheck/mk_uniftype.rs b/core/src/typecheck/mk_uniftype.rs index 8d55d7627e..5208be0d4d 100644 --- a/core/src/typecheck/mk_uniftype.rs +++ b/core/src/typecheck/mk_uniftype.rs @@ -151,3 +151,4 @@ generate_builder!(str, String); generate_builder!(num, Number); generate_builder!(bool, Bool); generate_builder!(sym, Symbol); +generate_builder!(foreign_id, ForeignId); diff --git a/core/src/typecheck/mod.rs b/core/src/typecheck/mod.rs index 3fa71b905c..cd5809a646 100644 --- a/core/src/typecheck/mod.rs +++ b/core/src/typecheck/mod.rs @@ -224,9 +224,12 @@ impl VarLevelUpperBound for GenericUnifType { impl VarLevelUpperBound for GenericUnifTypeUnrolling { fn var_level_upper_bound(&self) -> VarLevel { match self { - TypeF::Dyn | TypeF::Bool | TypeF::Number | TypeF::String | TypeF::Symbol => { - VarLevel::NO_VAR - } + TypeF::Dyn + | TypeF::Bool + | TypeF::Number + | TypeF::String + | TypeF::ForeignId + | TypeF::Symbol => VarLevel::NO_VAR, TypeF::Arrow(domain, codomain) => max( domain.var_level_upper_bound(), codomain.var_level_upper_bound(), @@ -1440,6 +1443,7 @@ fn walk( | Term::Str(_) | Term::Lbl(_) | Term::Enum(_) + | Term::ForeignId(_) | Term::SealingKey(_) // This function doesn't recursively typecheck imports: this is the responsibility of the // caller. @@ -1628,6 +1632,7 @@ fn walk_type( | TypeF::Number | TypeF::Bool | TypeF::String + | TypeF::ForeignId | TypeF::Symbol // Currently, the parser can't generate unbound type variables by construction. Thus we // don't check here for unbound type variables again. @@ -2276,6 +2281,9 @@ fn check( } } + Term::ForeignId(_) => ty + .unify(mk_uniftype::foreign_id(), state, &ctxt) + .map_err(|err| err.into_typecheck_err(state, rt.pos)), Term::SealingKey(_) => ty .unify(mk_uniftype::sym(), state, &ctxt) .map_err(|err| err.into_typecheck_err(state, rt.pos)), diff --git a/core/src/typecheck/operation.rs b/core/src/typecheck/operation.rs index 6b3f23f08d..46f068ef76 100644 --- a/core/src/typecheck/operation.rs +++ b/core/src/typecheck/operation.rs @@ -28,7 +28,16 @@ pub fn get_uop_type( UnaryOp::Typeof() => ( mk_uniftype::dynamic(), mk_uty_enum!( - "Number", "Bool", "String", "Enum", "Function", "Array", "Record", "Label", "Other" + "Number", + "Bool", + "String", + "Enum", + "Function", + "Array", + "Record", + "Label", + "ForeignId", + "Other" ), ), // Bool -> Bool -> Bool diff --git a/core/stdlib/internals.ncl b/core/stdlib/internals.ncl index 22539fba33..0a633b828b 100644 --- a/core/stdlib/internals.ncl +++ b/core/stdlib/internals.ncl @@ -11,6 +11,8 @@ "$string" = fun label value => if %typeof% value == 'String then value else %blame% label, + "$foreign_id" = fun label value => if %typeof% value == 'ForeignId then value else %blame% label, + "$fail" = fun label _value => %blame% label, "$array" = fun element_contract label value => diff --git a/core/stdlib/std.ncl b/core/stdlib/std.ncl index 4d74bc2e72..48fdfe9fc1 100644 --- a/core/stdlib/std.ncl +++ b/core/stdlib/std.ncl @@ -3187,6 +3187,7 @@ 'Function, 'Array, 'Record, + 'ForeignId, 'Other |] | doc m%"