use crate::value::Value;
const MAX_FIELDS: usize = 12;
pub fn observe(v: &Value) -> String {
match v {
Value::Null => "null".to_string(),
Value::Bool(_) => "bool".to_string(),
Value::Int(_) => "int".to_string(),
Value::Float(_) => "float".to_string(),
Value::Str(_) => "str".to_string(),
Value::Uri(_) => "uri".to_string(),
Value::Table(_) => "table".to_string(),
Value::Array(items) => {
let mut it = items.iter().map(observe);
match it.next() {
None => "array<any>".to_string(),
Some(first) => {
if it.all(|s| s == first) {
format!("array<{first}>")
} else {
"array<any>".to_string()
}
}
}
}
Value::Record(map) => {
let mut parts = Vec::new();
for (k, val) in map.iter().take(MAX_FIELDS) {
parts.push(format!("{k}:{}", observe(val)));
}
let more = map.len().saturating_sub(MAX_FIELDS);
if more > 0 {
parts.push(format!("+{more}"));
}
format!("record{{{}}}", parts.join(","))
}
_ => "any".to_string(),
}
}
pub fn shape_of(name: &str) -> Option<&'static str> {
DECLARED
.iter()
.find(|(n, _)| *n == name)
.map(|(_, shape)| *shape)
}
pub fn declared_count() -> usize {
DECLARED.len()
}
pub const DECLARED: &[(&str, &str)] = &[
("aecon", "str"),
("keys", "array<str>"),
("len", "int"),
(
"ls",
"array<record{ext:str,is_dir:bool,modified:int,name:str,path:str,size:int}>",
),
(
"ontology_manifest",
"record{categories:array<record{builtins:int,category:str,effects:array<str>}>,effect_legend:array<str>,hint:str,ontology:str,total_builtins:int}",
),
("pwd", "str"),
("range", "array<int>"),
("split", "array<str>"),
("tokens", "int"),
("type_of", "str"),
("upper", "str"),
];
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeMap;
fn rec(pairs: &[(&str, Value)]) -> Value {
let mut m = BTreeMap::new();
for (k, v) in pairs {
m.insert(k.to_string(), v.clone());
}
Value::Record(m)
}
#[test]
fn scalars_observe_as_their_type() {
assert_eq!(observe(&Value::Int(1)), "int");
assert_eq!(observe(&Value::Str("a".into())), "str");
assert_eq!(observe(&Value::Bool(true)), "bool");
assert_eq!(observe(&Value::Null), "null");
}
#[test]
fn a_uniform_array_reports_its_element_type() {
let v = Value::Array(vec![Value::Int(1), Value::Int(2)]);
assert_eq!(observe(&v), "array<int>");
}
#[test]
fn a_ragged_array_is_reported_as_ragged_not_smoothed_over() {
let v = Value::Array(vec![Value::Int(1), Value::Str("a".into())]);
assert_eq!(observe(&v), "array<any>");
}
#[test]
fn an_empty_array_claims_nothing_about_its_elements() {
assert_eq!(observe(&Value::Array(vec![])), "array<any>");
}
#[test]
fn records_list_fields_with_their_types() {
let v = rec(&[("b", Value::Str("x".into())), ("a", Value::Int(1))]);
assert_eq!(observe(&v), "record{a:int,b:str}");
}
#[test]
fn nested_records_nest() {
let v = rec(&[("inner", rec(&[("n", Value::Int(1))]))]);
assert_eq!(observe(&v), "record{inner:record{n:int}}");
}
#[test]
fn a_wide_record_is_elided_with_a_count_rather_than_truncated_silently() {
let mut m = BTreeMap::new();
for i in 0..(MAX_FIELDS + 3) {
m.insert(format!("f{i:02}"), Value::Int(i as i64));
}
let s = observe(&Value::Record(m));
assert!(s.ends_with("+3}"), "expected an elision count, got {s}");
}
#[test]
fn shape_of_is_silent_where_nothing_is_proven() {
assert_eq!(shape_of("pwd"), Some("str"));
assert_eq!(shape_of("first"), None);
assert_eq!(shape_of("no_such_builtin"), None);
}
}