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::{self, 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/// The cells a slice over an argument enumerates (7, `spec-0.4.0`): the argument
112/// declaration's optional `extent`.
113#[derive(Clone, Debug, PartialEq, Eq, Hash)]
114#[non_exhaustive]
115pub enum Extent {
116    /// Lexical values of the argument's type, strictly ascending in the type's order
117    /// (numbers numerically, dates and instants chronologically, strings by code point); at
118    /// most 4096.
119    Values(Vec<String>),
120    /// For a `date` or `number` argument, every date (or every integer) from `lo` to `hi`,
121    /// inclusive; number ends are integers, and `lo <= hi`.
122    Range {
123        /// The first value.
124        lo: String,
125        /// The last value.
126        hi: String,
127    },
128}
129
130impl Extent {
131    /// The JSON form: an array of strings, or `{"hi", "lo"}`.
132    pub fn to_value(&self) -> Value {
133        match self {
134            Extent::Values(v) => Value::Array(v.iter().map(|s| Value::String(s.clone())).collect()),
135            Extent::Range { lo, hi } => {
136                Value::Object(vec![("hi".into(), Value::String(hi.clone())), ("lo".into(), Value::String(lo.clone()))])
137            }
138        }
139    }
140}
141
142/// A function descriptor (7) in its `spec-0.3.0` form: signature, code and declared inputs.
143///
144/// It has no place for the members `spec-0.4.0` adds, `signature.order` and argument
145/// `extent`, so [`from_json`](Self::from_json) and [`from_value`](Self::from_value) refuse a
146/// descriptor that has them with `E_SCHEMA`, as in 0.3.3, rather than return a value whose
147/// [`canonical`](Self::canonical) bytes and [`hash`](Self::hash) would drop them. Read any
148/// descriptor with [`FunctionDescriptor`], which has both; [`FunctionDescriptor::to_descriptor`]
149/// gives this form when neither is present.
150#[derive(Clone, Debug, PartialEq, Eq, Hash)]
151pub struct Descriptor {
152    /// `code.lang`, e.g. `stored`.
153    pub lang: String,
154    /// `code.body`, opaque to this crate.
155    pub body: String,
156    /// Fully qualified names of the functions this one may read, ascending.
157    pub inputs: Vec<String>,
158    /// The signature.
159    pub signature: Signature,
160}
161
162impl Descriptor {
163    /// Parses a descriptor from JSON text: strict JSON, structure, canonical form (7). A
164    /// descriptor with `signature.order` or an argument `extent` is `E_SCHEMA` here (see
165    /// [`Descriptor`]); use [`FunctionDescriptor::from_json`].
166    pub fn from_json(bytes: &[u8]) -> Result<Descriptor> {
167        Descriptor::from_value(&json::parse(bytes)?)
168    }
169
170    /// Checks a parsed JSON value as a descriptor; `E_SCHEMA` for `order` or `extent`, as
171    /// [`from_json`](Self::from_json).
172    pub fn from_value(v: &Value) -> Result<Descriptor> {
173        json::check_value_depth(v)?;
174        descriptor_structure(v, false)?;
175        let f = descriptor_canonical(v)?;
176        Ok(Descriptor { lang: f.lang, body: f.body, inputs: f.inputs, signature: f.signature })
177    }
178
179    /// The JSON form.
180    pub fn to_value(&self) -> Value {
181        descriptor_value(&self.lang, &self.body, &self.inputs, &self.signature, None, &BTreeMap::new())
182    }
183
184    /// The canonical bytes (6.2).
185    pub fn canonical(&self) -> Vec<u8> {
186        self.to_value().to_jcs()
187    }
188
189    /// The function hash: SHA-256 of the canonical bytes, lowercase hex.
190    pub fn hash(&self) -> String {
191        crate::crypto::sha256_hex(&self.canonical())
192    }
193}
194
195/// A function descriptor (7) with the optional members of `spec-0.4.0`: `signature.order`
196/// and argument `extent`. Absent, it is the [`Descriptor`] of `spec-0.3.0`, byte for byte and
197/// hash for hash ([`From<Descriptor>`], [`to_descriptor`](Self::to_descriptor)).
198///
199/// The fields are public to read; a value changed by hand is not checked again until it is
200/// parsed: `FunctionDescriptor::from_value(&d.to_value())` checks it.
201#[derive(Clone, Debug, PartialEq, Eq, Hash)]
202#[non_exhaustive]
203pub struct FunctionDescriptor {
204    /// `code.lang`, e.g. `stored` or `formula`.
205    pub lang: String,
206    /// `code.body`, opaque to this crate.
207    pub body: String,
208    /// Fully qualified names of the functions this one may read, ascending.
209    pub inputs: Vec<String>,
210    /// The signature's arguments and return; its `order` and the arguments' extents are
211    /// [`order`](Self::order) and [`extents`](Self::extents).
212    pub signature: Signature,
213    /// `signature.order`: every argument name exactly once, the order positional arguments
214    /// bind in (15.5); `None` when absent.
215    pub order: Option<Vec<String>>,
216    /// The arguments' `extent` members, by argument name; empty when none has one.
217    pub extents: BTreeMap<String, Extent>,
218}
219
220impl FunctionDescriptor {
221    /// Parses a descriptor from JSON text: strict JSON, structure, canonical form (7), with
222    /// `order` and `extent` and their checks (`E_SCHEMA` for their shapes, `E_CANON_SET`,
223    /// `E_CANON_TYPE`, `E_CANON_NUMBER`, `E_CANON_LIMIT` and the lexical codes otherwise).
224    pub fn from_json(bytes: &[u8]) -> Result<FunctionDescriptor> {
225        FunctionDescriptor::from_value(&json::parse(bytes)?)
226    }
227
228    /// Checks a parsed JSON value as a descriptor.
229    pub fn from_value(v: &Value) -> Result<FunctionDescriptor> {
230        json::check_value_depth(v)?;
231        descriptor_structure(v, true)?;
232        descriptor_canonical(v)
233    }
234
235    /// The JSON form.
236    pub fn to_value(&self) -> Value {
237        descriptor_value(&self.lang, &self.body, &self.inputs, &self.signature, self.order.as_deref(), &self.extents)
238    }
239
240    /// The canonical bytes (6.2).
241    pub fn canonical(&self) -> Vec<u8> {
242        self.to_value().to_jcs()
243    }
244
245    /// The function hash: SHA-256 of the canonical bytes, lowercase hex.
246    pub fn hash(&self) -> String {
247        crate::crypto::sha256_hex(&self.canonical())
248    }
249
250    /// The `spec-0.3.0` form, when there is no `order` and no `extent`.
251    pub fn to_descriptor(&self) -> Option<Descriptor> {
252        (self.order.is_none() && self.extents.is_empty()).then(|| Descriptor {
253            lang: self.lang.clone(),
254            body: self.body.clone(),
255            inputs: self.inputs.clone(),
256            signature: self.signature.clone(),
257        })
258    }
259
260    /// The unit of `cell`'s value (7.1, `units.md` 3), as [`resolve_with`] does for a
261    /// [`Descriptor`]; `None` when the function does not return a number.
262    pub fn resolve_unit(&self, cell: &Cell) -> Result<Option<String>> {
263        resolve_returns(&self.signature.returns, cell)
264    }
265}
266
267impl From<Descriptor> for FunctionDescriptor {
268    fn from(d: Descriptor) -> FunctionDescriptor {
269        FunctionDescriptor {
270            lang: d.lang,
271            body: d.body,
272            inputs: d.inputs,
273            signature: d.signature,
274            order: None,
275            extents: BTreeMap::new(),
276        }
277    }
278}
279
280fn descriptor_value(
281    lang: &str,
282    body: &str,
283    inputs: &[String],
284    sig: &Signature,
285    order: Option<&[String]>,
286    extents: &BTreeMap<String, Extent>,
287) -> Value {
288    let mut sig_v = sig.to_value();
289    if let Value::Object(m) = &mut sig_v {
290        if let Some((_, Value::Object(args))) = m.iter_mut().find(|(k, _)| k == "args") {
291            for (name, decl) in args.iter_mut() {
292                if let (Some(e), Value::Object(dm)) = (extents.get(name), decl) {
293                    dm.push(("extent".into(), e.to_value()));
294                }
295            }
296        }
297        if let Some(o) = order {
298            m.push(("order".into(), Value::Array(o.iter().map(|s| Value::String(s.clone())).collect())));
299        }
300    }
301    Value::Object(vec![
302        (
303            "code".into(),
304            Value::Object(vec![
305                ("body".into(), Value::String(body.into())),
306                ("lang".into(), Value::String(lang.into())),
307            ]),
308        ),
309        ("inputs".into(), Value::Array(inputs.iter().map(|s| Value::String(s.clone())).collect())),
310        ("signature".into(), sig_v),
311    ])
312}
313
314fn unit_shape(v: &Value, path: &str) -> Result<()> {
315    let bad = || err(ErrorCode::Schema, path, "a unit is a string, {\"arg\": name} or {\"of\", \"per\"}");
316    match v {
317        Value::String(_) => Ok(()),
318        Value::Object(m) if m.len() == 1 && m[0].0 == "arg" => match m[0].1 {
319            Value::String(_) => Ok(()),
320            _ => bad(),
321        },
322        Value::Object(m) if m.len() == 2 && schema::opt(m, "of").is_some() && schema::opt(m, "per").is_some() => {
323            for side in ["of", "per"] {
324                let ok = match schema::get(m, side) {
325                    Value::String(_) => true,
326                    Value::Object(a) => a.len() == 1 && a[0].0 == "arg" && matches!(a[0].1, Value::String(_)),
327                    _ => false,
328                };
329                if !ok {
330                    return err(
331                        ErrorCode::Schema,
332                        &member(path, side),
333                        "a rate side is a currency code or {\"arg\": name}",
334                    );
335                }
336            }
337            Ok(())
338        }
339        _ => bad(),
340    }
341}
342
343/// The shape of an argument's `extent` (`E_SCHEMA`): an array of strings, or `{"hi", "lo"}`
344/// with string ends.
345fn extent_shape(v: &Value, path: &str) -> Result<()> {
346    match v {
347        Value::Array(items) => {
348            if !items.iter().all(|i| matches!(i, Value::String(_))) {
349                return err(ErrorCode::Schema, path, "an extent array holds lexical values as strings");
350            }
351            Ok(())
352        }
353        Value::Object(m) => {
354            schema::exact(m, &["hi", "lo"], &[], path, ErrorCode::Schema)?;
355            schema::string(m, "hi", path)?;
356            schema::string(m, "lo", path)?;
357            Ok(())
358        }
359        _ => err(ErrorCode::Schema, path, "an extent is an array of values or a range {\"hi\", \"lo\"}"),
360    }
361}
362
363/// The structure of a descriptor (`E_SCHEMA`), over the whole descriptor; with `extended`,
364/// the members of `spec-0.4.0` (`signature.order`, argument `extent`) are allowed.
365pub(crate) fn descriptor_structure(v: &Value, extended: bool) -> Result<()> {
366    let new_sig: &[&str] = if extended { &["order"] } else { &[] };
367    let new_arg: &[&str] = if extended { &["extent", "unit"] } else { &["unit"] };
368    let m = schema::object(v, "$")?;
369    schema::exact(m, &["code", "inputs", "signature"], &[], "$", ErrorCode::Schema)?;
370    let code = schema::object(schema::get(m, "code"), "$.code")?;
371    schema::exact(code, &["body", "lang"], &[], "$.code", ErrorCode::Schema)?;
372    schema::string(code, "body", "$.code")?;
373    schema::string(code, "lang", "$.code")?;
374    match schema::get(m, "inputs") {
375        Value::Array(items) if items.iter().all(|i| matches!(i, Value::String(_))) => {}
376        _ => return err(ErrorCode::Schema, "$.inputs", "inputs must be an array of strings"),
377    }
378    let sig = schema::object(schema::get(m, "signature"), "$.signature")?;
379    schema::exact(sig, &["args", "returns"], new_sig, "$.signature", ErrorCode::Schema)?;
380    let args = schema::object(schema::get(sig, "args"), "$.signature.args")?;
381    for (k, d) in jcs_sorted(args) {
382        let p = member("$.signature.args", k);
383        let dm = schema::object(d, &p)?;
384        schema::exact(dm, &["type", "valid"], new_arg, &p, ErrorCode::Schema)?;
385        if let Some(e) = schema::opt(dm, "extent") {
386            extent_shape(e, &member(&p, "extent"))?;
387        }
388        schema::string(dm, "type", &p)?;
389        if !matches!(schema::get(dm, "valid"), Value::Bool(_)) {
390            return err(ErrorCode::Schema, &member(&p, "valid"), "valid must be a boolean");
391        }
392        if let Some(u) = schema::opt(dm, "unit") {
393            unit_shape(u, &member(&p, "unit"))?;
394        }
395    }
396    if let Some(o) = schema::opt(sig, "order") {
397        if !matches!(o, Value::Array(items) if items.iter().all(|i| matches!(i, Value::String(_)))) {
398            return err(ErrorCode::Schema, "$.signature.order", "order is an array of argument names");
399        }
400    }
401    let rp = "$.signature.returns";
402    match schema::get(sig, "returns") {
403        Value::String(_) => {}
404        Value::Object(rm) => {
405            schema::exact(rm, &["type"], &["unit"], rp, ErrorCode::Schema)?;
406            schema::string(rm, "type", rp)?;
407            if let Some(u) = schema::opt(rm, "unit") {
408                unit_shape(u, &member(rp, "unit"))?;
409            }
410        }
411        _ => return err(ErrorCode::Schema, rp, "returns must be a type name or an object"),
412    }
413    Ok(())
414}
415
416fn unit_arg_ref(name: &str, args: &[(String, Value)], path: &str) -> Result<()> {
417    let is_string = schema::opt(args, name)
418        .and_then(Value::as_object)
419        .map(|d| schema::get(d, "type").as_str() == Some("string"))
420        .unwrap_or(false);
421    if !is_string {
422        return err(ErrorCode::CanonUnit, path, format!("a unit names a string argument of the signature: {name:?}"));
423    }
424    Ok(())
425}
426
427fn side_value(v: &Value) -> RateSide {
428    match v {
429        Value::String(s) => RateSide::Currency(s.clone()),
430        other => RateSide::Arg(other.get("arg").and_then(Value::as_str).unwrap_or("").into()),
431    }
432}
433
434/// Checks a structurally valid unit against 7.1 and returns it.
435fn unit_canonical(v: &Value, args: &[(String, Value)], path: &str) -> Result<Unit> {
436    match v {
437        Value::String(s) => {
438            if !lexical::is_literal_unit(s) {
439                return err(ErrorCode::CanonUnit, path, format!("not a unit listed in units.md: {s:?}"));
440            }
441            Ok(Unit::Literal(s.clone()))
442        }
443        Value::Object(m) if m.len() == 1 => {
444            let name = schema::get(m, "arg").as_str().unwrap_or("");
445            unit_arg_ref(name, args, path)?;
446            Ok(Unit::Arg(name.into()))
447        }
448        Value::Object(m) => {
449            for side in ["of", "per"] {
450                let sp = member(path, side);
451                match schema::get(m, side) {
452                    Value::String(s) => {
453                        if !lexical::is_currency(s) {
454                            return err(ErrorCode::CanonUnit, &sp, format!("a rate side is a currency code: {s:?}"));
455                        }
456                    }
457                    other => unit_arg_ref(other.get("arg").and_then(Value::as_str).unwrap_or(""), args, &sp)?,
458                }
459            }
460            Ok(Unit::Rate { of: side_value(schema::get(m, "of")), per: side_value(schema::get(m, "per")) })
461        }
462        _ => err(ErrorCode::Schema, path, "not a unit"),
463    }
464}
465
466/// Compares two lexical values of `ty` in the type's order (7): numbers numerically, dates
467/// and instants chronologically (their canonical text order), strings by code point (UTF-8
468/// byte order), `false` before `true`.
469pub(crate) fn cmp_lexical(ty: Type, a: &str, b: &str) -> core::cmp::Ordering {
470    match ty {
471        Type::Number => lexical::cmp_numbers(a, b),
472        Type::Boolean => (a == "true").cmp(&(b == "true")),
473        _ => a.cmp(b),
474    }
475}
476
477/// The canonical form of a structurally valid `extent` of an argument of type `ty` (7):
478/// the type, then each value's lexical form, their order, their count; for a range, `hi`
479/// then `lo` (lexical form, integer for numbers), then `lo <= hi`.
480fn extent_canonical(v: &Value, ty: Type, path: &str) -> Result<Extent> {
481    if matches!(ty, Type::Boolean | Type::Blob) {
482        return err(ErrorCode::CanonType, path, format!("a {} argument has no extent", ty.as_str()));
483    }
484    match v {
485        Value::Array(items) => {
486            let values: Vec<&str> = items.iter().filter_map(Value::as_str).collect();
487            for (i, x) in values.iter().enumerate() {
488                model::check_lexical(ty, x, &index(path, i))?;
489            }
490            if values.windows(2).any(|w| cmp_lexical(ty, w[0], w[1]) != core::cmp::Ordering::Less) {
491                return err(ErrorCode::CanonSet, path, "extent values are strictly ascending in their type's order");
492            }
493            if values.len() > limits::EXTENT {
494                return err(ErrorCode::CanonLimit, path, "more than 4096 extent values");
495            }
496            Ok(Extent::Values(values.iter().map(|x| x.to_string()).collect()))
497        }
498        Value::Object(m) => {
499            if !matches!(ty, Type::Date | Type::Number) {
500                return err(ErrorCode::CanonType, path, "an extent range is for date and number arguments");
501            }
502            for side in ["hi", "lo"] {
503                let sp = member(path, side);
504                let x = schema::string(m, side, path)?;
505                model::check_lexical(ty, x, &sp)?;
506                if ty == Type::Number && !lexical::is_integer(x) {
507                    return err(ErrorCode::CanonNumber, &sp, "an extent range over numbers has integer ends");
508                }
509            }
510            let (lo, hi) = (schema::string(m, "lo", path)?, schema::string(m, "hi", path)?);
511            if cmp_lexical(ty, lo, hi) == core::cmp::Ordering::Greater {
512                return err(ErrorCode::CanonSet, path, "an extent range has lo <= hi");
513            }
514            Ok(Extent::Range { lo: lo.into(), hi: hi.into() })
515        }
516        _ => err(ErrorCode::Schema, path, "not an extent"),
517    }
518}
519
520pub(crate) fn descriptor_canonical(v: &Value) -> Result<FunctionDescriptor> {
521    let m = schema::object(v, "$")?;
522    let code = schema::object(schema::get(m, "code"), "$.code")?;
523    let body = schema::string(code, "body", "$.code")?;
524    lexical::check_text(body, limits::BODY, true, "$.code.body")?;
525    let lang = schema::string(code, "lang", "$.code")?;
526    lexical::check_name(lang, "$.code.lang")?;
527
528    let items: Vec<&str> = match schema::get(m, "inputs") {
529        Value::Array(items) => items.iter().filter_map(Value::as_str).collect(),
530        _ => Vec::new(),
531    };
532    for (i, s) in items.iter().enumerate() {
533        lexical::check_function_name(s, &index("$.inputs", i))?;
534    }
535    if items.windows(2).any(|w| w[0] >= w[1]) {
536        return err(ErrorCode::CanonSet, "$.inputs", "entries must be strictly ascending");
537    }
538    if items.len() > limits::INPUTS {
539        return err(ErrorCode::CanonLimit, "$.inputs", "more than 4096 inputs");
540    }
541
542    let sig = schema::object(schema::get(m, "signature"), "$.signature")?;
543    let args = schema::object(schema::get(sig, "args"), "$.signature.args")?;
544    if args.len() > limits::ARGS {
545        return err(ErrorCode::CanonLimit, "$.signature.args", "more than 32 arguments");
546    }
547    let mut decls = BTreeMap::new();
548    let mut extents = BTreeMap::new();
549    for (k, d) in jcs_sorted(args) {
550        let p = member("$.signature.args", k);
551        lexical::check_arg_name(k, &p)?;
552        let dm = schema::object(d, &p)?;
553        let ty = Type::parse(schema::string(dm, "type", &p)?, &member(&p, "type"))?;
554        let unit = match (ty, schema::opt(dm, "unit")) {
555            (Type::Number, None) => return err(ErrorCode::CanonUnit, &p, "a number argument declares its unit"),
556            (Type::Number, Some(u)) => Some(unit_canonical(u, args, &member(&p, "unit"))?),
557            (_, Some(_)) => return err(ErrorCode::CanonUnit, &member(&p, "unit"), "only number arguments have a unit"),
558            (_, None) => None,
559        };
560        let valid = matches!(schema::get(dm, "valid"), Value::Bool(true));
561        if valid && !ty.may_be_valid_time() {
562            return err(ErrorCode::CanonType, &p, "a valid-time argument must be a date, instant or string");
563        }
564        if let Some(e) = schema::opt(dm, "extent") {
565            extents.insert(k.clone(), extent_canonical(e, ty, &member(&p, "extent"))?);
566        }
567        decls.insert(k.clone(), ArgDecl { ty, unit, valid });
568    }
569    let order = match schema::opt(sig, "order") {
570        Some(Value::Array(items)) => {
571            let names: Vec<&str> = items.iter().filter_map(Value::as_str).collect();
572            let mut sorted = names.clone();
573            sorted.sort_unstable();
574            sorted.dedup();
575            if sorted.len() != names.len()
576                || names.len() != decls.len()
577                || !names.iter().all(|n| decls.contains_key(*n))
578            {
579                return err(ErrorCode::CanonSet, "$.signature.order", "order lists every argument exactly once");
580            }
581            Some(names.iter().map(|n| n.to_string()).collect())
582        }
583        _ => None,
584    };
585
586    let rp = "$.signature.returns";
587    let returns = match schema::get(sig, "returns") {
588        Value::String(name) => {
589            let ty = Type::parse(name, rp)?;
590            if ty == Type::Number {
591                return err(
592                    ErrorCode::CanonUnit,
593                    rp,
594                    "a number return is written {\"type\": \"number\", \"unit\": ...}",
595                );
596            }
597            Returns { ty, unit: None }
598        }
599        Value::Object(rm) => {
600            let ty = Type::parse(schema::string(rm, "type", rp)?, &member(rp, "type"))?;
601            if ty != Type::Number {
602                return err(ErrorCode::CanonUnit, rp, "only a number return is written as an object");
603            }
604            let Some(u) = schema::opt(rm, "unit") else {
605                return err(ErrorCode::CanonUnit, rp, "a number return declares its unit");
606            };
607            Returns { ty, unit: Some(unit_canonical(u, args, &member(rp, "unit"))?) }
608        }
609        _ => return err(ErrorCode::Schema, rp, "returns must be a type name or an object"),
610    };
611    Ok(FunctionDescriptor {
612        lang: lang.into(),
613        body: body.into(),
614        inputs: items.iter().map(|s| s.to_string()).collect(),
615        signature: Signature { args: decls, returns },
616        order,
617        extents,
618    })
619}
620
621/// The unit of a cell's value (7.1, `units.md` 3), from the descriptor and cell JSON texts.
622///
623/// Checks, in order: the cell (structure, canonical form; slices allowed), that the
624/// descriptor hashes to `cell.fn_hash` (`E_FN_HASH_MISMATCH`), the descriptor, then
625/// substitution. Returns `None` when the function does not return a number.
626pub fn resolved_unit(descriptor: &[u8], cell: &[u8]) -> Result<Option<String>> {
627    let cell_v = json::parse(cell)?;
628    let desc_v = json::parse(descriptor)?;
629    resolved_unit_value(&desc_v, &cell_v)
630}
631
632/// [`resolved_unit`] over parsed JSON values.
633pub fn resolved_unit_value(descriptor: &Value, cell: &Value) -> Result<Option<String>> {
634    json::check_value_depth(descriptor)?;
635    json::check_value_depth(cell)?;
636    let cell = Cell::from_value(cell)?;
637    let hash = crate::crypto::sha256_hex(&descriptor.to_jcs());
638    if hash != cell.fn_hash {
639        return err(ErrorCode::FnHashMismatch, "$.descriptor", "the descriptor does not hash to cell.fn_hash");
640    }
641    let d = FunctionDescriptor::from_value(descriptor)?;
642    d.resolve_unit(&cell)
643}
644
645/// Substitutes a checked cell's arguments into a checked descriptor's return unit.
646pub fn resolve_with(d: &Descriptor, cell: &Cell) -> Result<Option<String>> {
647    resolve_returns(&d.signature.returns, cell)
648}
649
650fn resolve_returns(returns: &Returns, cell: &Cell) -> Result<Option<String>> {
651    let Some(unit) = &returns.unit else { return Ok(None) };
652    let arg_value = |name: &str| -> Result<String> {
653        let p = member("$.cell.args", name);
654        match cell.args.get(name) {
655            None => err(
656                ErrorCode::ArgMissing,
657                &p,
658                format!("the unit refers to argument {name:?}, which the cell does not bind"),
659            ),
660            Some(Arg::Slice) => err(ErrorCode::Slice, &p, "a slice has no single unit"),
661            Some(Arg::Value(Scalar::String(s))) => Ok(s.clone()),
662            Some(Arg::Value(other)) => {
663                err(ErrorCode::ArgType, &p, format!("bound as {}, not string", other.ty().as_str()))
664            }
665        }
666    };
667    let side = |s: &RateSide| -> Result<String> {
668        match s {
669            RateSide::Currency(c) => Ok(c.clone()),
670            RateSide::Arg(n) => {
671                let v = arg_value(n)?;
672                if !lexical::is_currency(&v) {
673                    return err(
674                        ErrorCode::CanonUnit,
675                        &member("$.cell.args", n),
676                        format!("a rate side must be a currency code: {v:?}"),
677                    );
678                }
679                Ok(v)
680            }
681        }
682    };
683    Ok(Some(match unit {
684        Unit::Literal(s) => s.clone(),
685        Unit::Arg(n) => {
686            let v = arg_value(n)?;
687            if !lexical::is_literal_unit(&v) {
688                return err(ErrorCode::CanonUnit, &member("$.cell.args", n), format!("not a unit: {v:?}"));
689            }
690            v
691        }
692        Unit::Rate { of, per } => {
693            let o = side(of)?;
694            let p = side(per)?;
695            format!("{o}/{p}")
696        }
697    }))
698}