Skip to main content

datumesh_core/
descriptor.rs

1//! Function descriptors (7), units (7.1, `units.md`) and unit resolution (`units.md` 3).
2
3use std::collections::BTreeMap;
4
5use crate::error::{err, index, member, ErrorCode, Result};
6use crate::json::{self, jcs_sorted, Value};
7use crate::lexical::{self, limits};
8use crate::model::{Arg, Cell, Scalar, Type};
9use crate::schema;
10
11/// One side of a rate: a currency code or an argument reference.
12#[derive(Clone, Debug, PartialEq, Eq, Hash)]
13pub enum RateSide {
14    /// A currency code from `units.md` 2.1.
15    Currency(String),
16    /// `{"arg": name}`: the value of a string argument.
17    Arg(String),
18}
19
20/// A unit in a signature (7.1).
21#[derive(Clone, Debug, PartialEq, Eq, Hash)]
22pub enum Unit {
23    /// A unit listed in `units.md`, or an index unit.
24    Literal(String),
25    /// `{"arg": name}`: the value of the named string argument.
26    Arg(String),
27    /// `{"of": X, "per": Y}`: units of X per one unit of Y.
28    Rate {
29        /// The numerator.
30        of: RateSide,
31        /// The denominator.
32        per: RateSide,
33    },
34}
35
36impl Unit {
37    /// The JSON form.
38    pub fn to_value(&self) -> Value {
39        fn arg(n: &str) -> Value {
40            Value::Object(vec![("arg".into(), Value::String(n.into()))])
41        }
42        fn side(s: &RateSide) -> Value {
43            match s {
44                RateSide::Currency(c) => Value::String(c.clone()),
45                RateSide::Arg(n) => arg(n),
46            }
47        }
48        match self {
49            Unit::Literal(s) => Value::String(s.clone()),
50            Unit::Arg(n) => arg(n),
51            Unit::Rate { of, per } => Value::Object(vec![("of".into(), side(of)), ("per".into(), side(per))]),
52        }
53    }
54}
55
56/// An argument declaration: `{"type", "valid"}`, plus `unit` for numbers.
57#[derive(Clone, Debug, PartialEq, Eq, Hash)]
58pub struct ArgDecl {
59    /// The argument's type.
60    pub ty: Type,
61    /// The unit; present exactly when the type is `number`.
62    pub unit: Option<Unit>,
63    /// Whether this is a valid-time argument.
64    pub valid: bool,
65}
66
67/// The declared return: a type, with a unit exactly when it is `number`.
68#[derive(Clone, Debug, PartialEq, Eq, Hash)]
69pub struct Returns {
70    /// The value type.
71    pub ty: Type,
72    /// The unit of a number return.
73    pub unit: Option<Unit>,
74}
75
76/// A function signature.
77#[derive(Clone, Debug, PartialEq, Eq, Hash)]
78pub struct Signature {
79    /// Argument declarations by name.
80    pub args: BTreeMap<String, ArgDecl>,
81    /// The return type.
82    pub returns: Returns,
83}
84
85impl Signature {
86    /// The JSON form, as it appears in a descriptor.
87    pub fn to_value(&self) -> Value {
88        let args = self
89            .args
90            .iter()
91            .map(|(k, d)| {
92                let mut m = vec![("type".to_string(), Value::String(d.ty.as_str().into()))];
93                if let Some(u) = &d.unit {
94                    m.push(("unit".into(), u.to_value()));
95                }
96                m.push(("valid".into(), Value::Bool(d.valid)));
97                (k.clone(), Value::Object(m))
98            })
99            .collect();
100        let returns = match &self.returns.unit {
101            Some(u) => Value::Object(vec![
102                ("type".into(), Value::String(self.returns.ty.as_str().into())),
103                ("unit".into(), u.to_value()),
104            ]),
105            None => Value::String(self.returns.ty.as_str().into()),
106        };
107        Value::Object(vec![("args".into(), Value::Object(args)), ("returns".into(), returns)])
108    }
109}
110
111/// A function descriptor (7): signature, code and declared inputs.
112#[derive(Clone, Debug, PartialEq, Eq, Hash)]
113pub struct Descriptor {
114    /// `code.lang`, e.g. `stored`.
115    pub lang: String,
116    /// `code.body`, opaque to this crate.
117    pub body: String,
118    /// Fully qualified names of the functions this one may read, ascending.
119    pub inputs: Vec<String>,
120    /// The signature.
121    pub signature: Signature,
122}
123
124impl Descriptor {
125    /// Parses a descriptor from JSON text: strict JSON, structure, canonical form (7).
126    pub fn from_json(bytes: &[u8]) -> Result<Descriptor> {
127        Descriptor::from_value(&json::parse(bytes)?)
128    }
129
130    /// Checks a parsed JSON value as a descriptor.
131    pub fn from_value(v: &Value) -> Result<Descriptor> {
132        json::check_value_depth(v)?;
133        descriptor_structure(v)?;
134        descriptor_canonical(v)
135    }
136
137    /// The JSON form.
138    pub fn to_value(&self) -> Value {
139        Value::Object(vec![
140            (
141                "code".into(),
142                Value::Object(vec![
143                    ("body".into(), Value::String(self.body.clone())),
144                    ("lang".into(), Value::String(self.lang.clone())),
145                ]),
146            ),
147            ("inputs".into(), Value::Array(self.inputs.iter().map(|s| Value::String(s.clone())).collect())),
148            ("signature".into(), self.signature.to_value()),
149        ])
150    }
151
152    /// The canonical bytes (6.2).
153    pub fn canonical(&self) -> Vec<u8> {
154        self.to_value().to_jcs()
155    }
156
157    /// The function hash: SHA-256 of the canonical bytes, lowercase hex.
158    pub fn hash(&self) -> String {
159        crate::crypto::sha256_hex(&self.canonical())
160    }
161}
162
163fn unit_shape(v: &Value, path: &str) -> Result<()> {
164    let bad = || err(ErrorCode::Schema, path, "a unit is a string, {\"arg\": name} or {\"of\", \"per\"}");
165    match v {
166        Value::String(_) => Ok(()),
167        Value::Object(m) if m.len() == 1 && m[0].0 == "arg" => match m[0].1 {
168            Value::String(_) => Ok(()),
169            _ => bad(),
170        },
171        Value::Object(m) if m.len() == 2 && schema::opt(m, "of").is_some() && schema::opt(m, "per").is_some() => {
172            for side in ["of", "per"] {
173                let ok = match schema::get(m, side) {
174                    Value::String(_) => true,
175                    Value::Object(a) => a.len() == 1 && a[0].0 == "arg" && matches!(a[0].1, Value::String(_)),
176                    _ => false,
177                };
178                if !ok {
179                    return err(
180                        ErrorCode::Schema,
181                        &member(path, side),
182                        "a rate side is a currency code or {\"arg\": name}",
183                    );
184                }
185            }
186            Ok(())
187        }
188        _ => bad(),
189    }
190}
191
192pub(crate) fn descriptor_structure(v: &Value) -> Result<()> {
193    let m = schema::object(v, "$")?;
194    schema::exact(m, &["code", "inputs", "signature"], &[], "$", ErrorCode::Schema)?;
195    let code = schema::object(schema::get(m, "code"), "$.code")?;
196    schema::exact(code, &["body", "lang"], &[], "$.code", ErrorCode::Schema)?;
197    schema::string(code, "body", "$.code")?;
198    schema::string(code, "lang", "$.code")?;
199    match schema::get(m, "inputs") {
200        Value::Array(items) if items.iter().all(|i| matches!(i, Value::String(_))) => {}
201        _ => return err(ErrorCode::Schema, "$.inputs", "inputs must be an array of strings"),
202    }
203    let sig = schema::object(schema::get(m, "signature"), "$.signature")?;
204    schema::exact(sig, &["args", "returns"], &[], "$.signature", ErrorCode::Schema)?;
205    let args = schema::object(schema::get(sig, "args"), "$.signature.args")?;
206    for (k, d) in jcs_sorted(args) {
207        let p = member("$.signature.args", k);
208        let dm = schema::object(d, &p)?;
209        schema::exact(dm, &["type", "valid"], &["unit"], &p, ErrorCode::Schema)?;
210        schema::string(dm, "type", &p)?;
211        if !matches!(schema::get(dm, "valid"), Value::Bool(_)) {
212            return err(ErrorCode::Schema, &member(&p, "valid"), "valid must be a boolean");
213        }
214        if let Some(u) = schema::opt(dm, "unit") {
215            unit_shape(u, &member(&p, "unit"))?;
216        }
217    }
218    let rp = "$.signature.returns";
219    match schema::get(sig, "returns") {
220        Value::String(_) => {}
221        Value::Object(rm) => {
222            schema::exact(rm, &["type"], &["unit"], rp, ErrorCode::Schema)?;
223            schema::string(rm, "type", rp)?;
224            if let Some(u) = schema::opt(rm, "unit") {
225                unit_shape(u, &member(rp, "unit"))?;
226            }
227        }
228        _ => return err(ErrorCode::Schema, rp, "returns must be a type name or an object"),
229    }
230    Ok(())
231}
232
233fn unit_arg_ref(name: &str, args: &[(String, Value)], path: &str) -> Result<()> {
234    let is_string = schema::opt(args, name)
235        .and_then(Value::as_object)
236        .map(|d| schema::get(d, "type").as_str() == Some("string"))
237        .unwrap_or(false);
238    if !is_string {
239        return err(ErrorCode::CanonUnit, path, format!("a unit names a string argument of the signature: {name:?}"));
240    }
241    Ok(())
242}
243
244fn side_value(v: &Value) -> RateSide {
245    match v {
246        Value::String(s) => RateSide::Currency(s.clone()),
247        other => RateSide::Arg(other.get("arg").and_then(Value::as_str).unwrap_or("").into()),
248    }
249}
250
251/// Checks a structurally valid unit against 7.1 and returns it.
252fn unit_canonical(v: &Value, args: &[(String, Value)], path: &str) -> Result<Unit> {
253    match v {
254        Value::String(s) => {
255            if !lexical::is_literal_unit(s) {
256                return err(ErrorCode::CanonUnit, path, format!("not a unit listed in units.md: {s:?}"));
257            }
258            Ok(Unit::Literal(s.clone()))
259        }
260        Value::Object(m) if m.len() == 1 => {
261            let name = schema::get(m, "arg").as_str().unwrap_or("");
262            unit_arg_ref(name, args, path)?;
263            Ok(Unit::Arg(name.into()))
264        }
265        Value::Object(m) => {
266            for side in ["of", "per"] {
267                let sp = member(path, side);
268                match schema::get(m, side) {
269                    Value::String(s) => {
270                        if !lexical::is_currency(s) {
271                            return err(ErrorCode::CanonUnit, &sp, format!("a rate side is a currency code: {s:?}"));
272                        }
273                    }
274                    other => unit_arg_ref(other.get("arg").and_then(Value::as_str).unwrap_or(""), args, &sp)?,
275                }
276            }
277            Ok(Unit::Rate { of: side_value(schema::get(m, "of")), per: side_value(schema::get(m, "per")) })
278        }
279        _ => err(ErrorCode::Schema, path, "not a unit"),
280    }
281}
282
283pub(crate) fn descriptor_canonical(v: &Value) -> Result<Descriptor> {
284    let m = schema::object(v, "$")?;
285    let code = schema::object(schema::get(m, "code"), "$.code")?;
286    let body = schema::string(code, "body", "$.code")?;
287    lexical::check_text(body, limits::BODY, true, "$.code.body")?;
288    let lang = schema::string(code, "lang", "$.code")?;
289    lexical::check_name(lang, "$.code.lang")?;
290
291    let items: Vec<&str> = match schema::get(m, "inputs") {
292        Value::Array(items) => items.iter().filter_map(Value::as_str).collect(),
293        _ => Vec::new(),
294    };
295    for (i, s) in items.iter().enumerate() {
296        lexical::check_function_name(s, &index("$.inputs", i))?;
297    }
298    if items.windows(2).any(|w| w[0] >= w[1]) {
299        return err(ErrorCode::CanonSet, "$.inputs", "entries must be strictly ascending");
300    }
301    if items.len() > limits::INPUTS {
302        return err(ErrorCode::CanonLimit, "$.inputs", "more than 4096 inputs");
303    }
304
305    let sig = schema::object(schema::get(m, "signature"), "$.signature")?;
306    let args = schema::object(schema::get(sig, "args"), "$.signature.args")?;
307    if args.len() > limits::ARGS {
308        return err(ErrorCode::CanonLimit, "$.signature.args", "more than 32 arguments");
309    }
310    let mut decls = BTreeMap::new();
311    for (k, d) in jcs_sorted(args) {
312        let p = member("$.signature.args", k);
313        lexical::check_arg_name(k, &p)?;
314        let dm = schema::object(d, &p)?;
315        let ty = Type::parse(schema::string(dm, "type", &p)?, &member(&p, "type"))?;
316        let unit = match (ty, schema::opt(dm, "unit")) {
317            (Type::Number, None) => return err(ErrorCode::CanonUnit, &p, "a number argument declares its unit"),
318            (Type::Number, Some(u)) => Some(unit_canonical(u, args, &member(&p, "unit"))?),
319            (_, Some(_)) => return err(ErrorCode::CanonUnit, &member(&p, "unit"), "only number arguments have a unit"),
320            (_, None) => None,
321        };
322        let valid = matches!(schema::get(dm, "valid"), Value::Bool(true));
323        if valid && !ty.may_be_valid_time() {
324            return err(ErrorCode::CanonType, &p, "a valid-time argument must be a date, instant or string");
325        }
326        decls.insert(k.clone(), ArgDecl { ty, unit, valid });
327    }
328
329    let rp = "$.signature.returns";
330    let returns = match schema::get(sig, "returns") {
331        Value::String(name) => {
332            let ty = Type::parse(name, rp)?;
333            if ty == Type::Number {
334                return err(
335                    ErrorCode::CanonUnit,
336                    rp,
337                    "a number return is written {\"type\": \"number\", \"unit\": ...}",
338                );
339            }
340            Returns { ty, unit: None }
341        }
342        Value::Object(rm) => {
343            let ty = Type::parse(schema::string(rm, "type", rp)?, &member(rp, "type"))?;
344            if ty != Type::Number {
345                return err(ErrorCode::CanonUnit, rp, "only a number return is written as an object");
346            }
347            let Some(u) = schema::opt(rm, "unit") else {
348                return err(ErrorCode::CanonUnit, rp, "a number return declares its unit");
349            };
350            Returns { ty, unit: Some(unit_canonical(u, args, &member(rp, "unit"))?) }
351        }
352        _ => return err(ErrorCode::Schema, rp, "returns must be a type name or an object"),
353    };
354    Ok(Descriptor {
355        lang: lang.into(),
356        body: body.into(),
357        inputs: items.iter().map(|s| s.to_string()).collect(),
358        signature: Signature { args: decls, returns },
359    })
360}
361
362/// The unit of a cell's value (7.1, `units.md` 3), from the descriptor and cell JSON texts.
363///
364/// Checks, in order: the cell (structure, canonical form; slices allowed), that the
365/// descriptor hashes to `cell.fn_hash` (`E_FN_HASH_MISMATCH`), the descriptor, then
366/// substitution. Returns `None` when the function does not return a number.
367pub fn resolved_unit(descriptor: &[u8], cell: &[u8]) -> Result<Option<String>> {
368    let cell_v = json::parse(cell)?;
369    let desc_v = json::parse(descriptor)?;
370    resolved_unit_value(&desc_v, &cell_v)
371}
372
373/// [`resolved_unit`] over parsed JSON values.
374pub fn resolved_unit_value(descriptor: &Value, cell: &Value) -> Result<Option<String>> {
375    json::check_value_depth(descriptor)?;
376    json::check_value_depth(cell)?;
377    let cell = Cell::from_value(cell)?;
378    let hash = crate::crypto::sha256_hex(&descriptor.to_jcs());
379    if hash != cell.fn_hash {
380        return err(ErrorCode::FnHashMismatch, "$.descriptor", "the descriptor does not hash to cell.fn_hash");
381    }
382    let d = Descriptor::from_value(descriptor)?;
383    resolve_with(&d, &cell)
384}
385
386/// Substitutes a checked cell's arguments into a checked descriptor's return unit.
387pub fn resolve_with(d: &Descriptor, cell: &Cell) -> Result<Option<String>> {
388    let Some(unit) = &d.signature.returns.unit else { return Ok(None) };
389    let arg_value = |name: &str| -> Result<String> {
390        let p = member("$.cell.args", name);
391        match cell.args.get(name) {
392            None => err(
393                ErrorCode::ArgMissing,
394                &p,
395                format!("the unit refers to argument {name:?}, which the cell does not bind"),
396            ),
397            Some(Arg::Slice) => err(ErrorCode::Slice, &p, "a slice has no single unit"),
398            Some(Arg::Value(Scalar::String(s))) => Ok(s.clone()),
399            Some(Arg::Value(other)) => {
400                err(ErrorCode::ArgType, &p, format!("bound as {}, not string", other.ty().as_str()))
401            }
402        }
403    };
404    let side = |s: &RateSide| -> Result<String> {
405        match s {
406            RateSide::Currency(c) => Ok(c.clone()),
407            RateSide::Arg(n) => {
408                let v = arg_value(n)?;
409                if !lexical::is_currency(&v) {
410                    return err(
411                        ErrorCode::CanonUnit,
412                        &member("$.cell.args", n),
413                        format!("a rate side must be a currency code: {v:?}"),
414                    );
415                }
416                Ok(v)
417            }
418        }
419    };
420    Ok(Some(match unit {
421        Unit::Literal(s) => s.clone(),
422        Unit::Arg(n) => {
423            let v = arg_value(n)?;
424            if !lexical::is_literal_unit(&v) {
425                return err(ErrorCode::CanonUnit, &member("$.cell.args", n), format!("not a unit: {v:?}"));
426            }
427            v
428        }
429        Unit::Rate { of, per } => {
430            let o = side(of)?;
431            let p = side(per)?;
432            format!("{o}/{p}")
433        }
434    }))
435}