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) => match items.split_first() {
None => "array<any>".to_string(),
Some((first, rest)) => {
let first = observe(first);
if rest.iter().all(|item| shape_is(item, &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(),
}
}
fn shape_is(v: &Value, expected: &str) -> bool {
let want = expected.as_bytes();
let mut pos = 0usize;
let matched = emit_fragments(v, &mut |frag: &str| {
let end = pos + frag.len();
if want.get(pos..end) == Some(frag.as_bytes()) {
pos = end;
true
} else {
false
}
});
matched && pos == want.len()
}
fn emit_fragments<F: FnMut(&str) -> bool>(v: &Value, sink: &mut F) -> bool {
match v {
Value::Null => sink("null"),
Value::Bool(_) => sink("bool"),
Value::Int(_) => sink("int"),
Value::Float(_) => sink("float"),
Value::Str(_) => sink("str"),
Value::Uri(_) => sink("uri"),
Value::Table(_) => sink("table"),
Value::Array(_) => sink(&observe(v)),
Value::Record(map) => {
if !sink("record{") {
return false;
}
for (i, (k, val)) in map.iter().take(MAX_FIELDS).enumerate() {
if i > 0 && !sink(",") {
return false;
}
if !sink(k.as_str()) || !sink(":") || !emit_fragments(val, sink) {
return false;
}
}
let more = map.len().saturating_sub(MAX_FIELDS);
if more > 0 {
let marker = format!("+{more}");
if !sink(",") || !sink(marker.as_str()) {
return false;
}
}
sink("}")
}
_ => sink("any"),
}
}
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 ELEMENT_VAR: &str = "T";
pub const POLYMORPHIC: &[(&str, &str)] = &[
("first", "T"),
("last", "T"),
("reverse", "array<T>"),
("take", "array<T>"),
("unique", "array<T>"),
("values", "array<T>"),
];
pub fn polymorphic_shape_of(name: &str) -> Option<&'static str> {
POLYMORPHIC
.iter()
.find(|(n, _)| *n == name)
.map(|(_, s)| *s)
}
pub fn instantiate(shape: &str, element: &str) -> String {
shape.replace(ELEMENT_VAR, element)
}
pub fn element_of(v: &Value) -> Option<String> {
match v {
Value::Array(items) => {
let mut it = items.iter().map(observe);
let first = it.next()?;
if it.all(|s| s == first) {
Some(first)
} else {
None
}
}
Value::Record(m) => {
let mut it = m.values().map(observe);
let first = it.next()?;
if it.all(|s| s == first) {
Some(first)
} else {
None
}
}
_ => None,
}
}
#[cfg(windows)]
pub const LS_PATH_EXAMPLE: &str = "\\\\?\\C:\\...\\src\\agent.rs";
#[cfg(not(windows))]
pub const LS_PATH_EXAMPLE: &str = "/.../src/agent.rs";
pub const FIELD_EXAMPLES: &[(&str, &str, &str)] = &[
("ls", "ext", ".rs"),
("ls", "path", LS_PATH_EXAMPLE),
("ls", "modified", "1770766991"),
];
pub fn field_examples(name: &str) -> Vec<(&'static str, &'static str)> {
FIELD_EXAMPLES
.iter()
.filter(|(b, _, _)| *b == name)
.map(|(_, field, example)| (*field, *example))
.collect()
}
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,modules: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);
}
fn observe_reference(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_reference);
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_reference(val)));
}
let more = map.len().saturating_sub(MAX_FIELDS);
if more > 0 {
parts.push(format!("+{more}"));
}
format!("record{{{}}}", parts.join(","))
}
_ => "any".to_string(),
}
}
fn int_row(id: i64) -> Value {
rec(&[("id", Value::Int(id)), ("name", Value::Str("row".into()))])
}
fn wide(extra: usize, last: Value) -> Value {
let mut m = BTreeMap::new();
for i in 0..(MAX_FIELDS + extra) {
m.insert(format!("f{i:02}"), Value::Int(i as i64));
}
m.insert(format!("f{:02}", MAX_FIELDS + extra - 1), last);
Value::Record(m)
}
fn varied_values() -> Vec<Value> {
let table = Value::Table(crate::value::Table {
rows: vec![],
schema: vec!["a".into()],
});
let mut differing_last: Vec<Value> = (0..1000).map(int_row).collect();
differing_last.push(rec(&[
("id", Value::Str("x".into())),
("name", Value::Str("row".into())),
]));
let mut extra_field_last: Vec<Value> = (0..1000).map(int_row).collect();
extra_field_last.push(rec(&[
("id", Value::Int(1)),
("name", Value::Str("row".into())),
("extra", Value::Bool(true)),
]));
vec![
Value::Null,
Value::Bool(false),
Value::Int(-7),
Value::Float(1.5),
Value::Str("s".into()),
Value::Uri("https://example.com".into()),
table.clone(),
Value::Array(vec![]),
Value::Array(vec![table.clone(), table.clone()]),
Value::Array(vec![Value::Int(1), Value::Int(2)]),
Value::Array(vec![Value::Int(1), Value::Str("a".into())]),
Value::Array(vec![Value::Null, Value::Null]),
Value::Array(vec![
Value::Array(vec![]),
Value::Array(vec![Value::Int(1)]),
]),
Value::Array(vec![
Value::Array(vec![Value::Int(1)]),
Value::Array(vec![Value::Int(2)]),
]),
Value::Array(vec![
Value::Array(vec![Value::Int(1)]),
Value::Array(vec![Value::Str("x".into())]),
]),
rec(&[]),
rec(&[("n", Value::Null), ("s", Value::Str("".into()))]),
rec(&[("inner", rec(&[("deep", Value::Array(vec![int_row(1)]))]))]),
Value::Array(vec![int_row(1), rec(&[("id", Value::Int(2))])]),
Value::Array((0..50).map(int_row).collect()),
Value::Array(differing_last),
Value::Array(extra_field_last),
Value::Array(vec![
rec(&[("a:int,b", Value::Str("x".into()))]),
rec(&[("a", Value::Int(1)), ("b", Value::Str("x".into()))]),
]),
Value::Array(vec![
wide(2, Value::Int(9)),
wide(2, Value::Str("hidden".into())),
]),
Value::Array(vec![wide(2, Value::Int(9)), {
let mut m = BTreeMap::new();
for i in 0..(MAX_FIELDS + 2) {
let val = if i == 0 {
Value::Str("x".into())
} else {
Value::Int(i as i64)
};
m.insert(format!("f{i:02}"), val);
}
Value::Record(m)
}]),
Value::Array(vec![wide(0, Value::Int(0)), wide(3, Value::Int(0))]),
]
}
#[test]
fn observe_matches_the_reference_on_varied_values() {
for v in varied_values() {
assert_eq!(observe(&v), observe_reference(&v), "value {v:?}");
}
}
#[test]
fn a_differing_row_at_the_very_end_is_not_skipped() {
let mut rows: Vec<Value> = (0..100_000).map(int_row).collect();
assert_eq!(
observe(&Value::Array(rows.clone())),
"array<record{id:int,name:str}>"
);
rows.push(rec(&[
("id", Value::Str("x".into())),
("name", Value::Str("row".into())),
]));
assert_eq!(observe(&Value::Array(rows)), "array<any>");
}
struct Lcg(u64);
impl Lcg {
fn next(&mut self) -> u64 {
self.0 = self
.0
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
self.0 >> 33
}
fn below(&mut self, n: u64) -> u64 {
self.next() % n
}
}
fn gen_key(g: &mut Lcg) -> String {
const ALPHABET: &[&str] = &["a", "b", "i", "n", "t", ":", ",", "{", "}", "+"];
let len = 1 + g.below(3);
(0..len)
.map(|_| ALPHABET[g.below(ALPHABET.len() as u64) as usize])
.collect()
}
fn gen_value(g: &mut Lcg, depth: u32) -> Value {
let kinds = if depth == 0 { 7 } else { 9 };
match g.below(kinds) {
0 => Value::Null,
1 => Value::Bool(g.below(2) == 0),
2 => Value::Int(g.below(100) as i64),
3 => Value::Float(0.5),
4 => Value::Str(gen_key(g)),
5 => Value::Uri("u".into()),
6 => Value::Table(crate::value::Table {
rows: vec![],
schema: vec![],
}),
7 => {
let proto = gen_value(g, depth - 1);
let len = g.below(6);
let items = (0..len)
.map(|_| {
if g.below(5) == 0 {
gen_value(g, depth - 1)
} else {
proto.clone()
}
})
.collect();
Value::Array(items)
}
_ => {
let mut m = BTreeMap::new();
for _ in 0..g.below(16) {
m.insert(gen_key(g), gen_value(g, depth - 1));
}
Value::Record(m)
}
}
}
#[test]
fn observe_matches_the_reference_on_generated_values() {
let mut g = Lcg(0x5eed);
for _ in 0..4000 {
let v = gen_value(&mut g, 3);
assert_eq!(observe(&v), observe_reference(&v), "value {v:?}");
}
}
#[test]
fn the_comparator_agrees_with_string_equality_on_generated_pairs() {
let mut g = Lcg(0xfeed);
let mut equal = 0;
for _ in 0..20_000 {
let v = gen_value(&mut g, 2);
let w = gen_value(&mut g, 2);
let expected = observe_reference(&v) == observe_reference(&w);
assert_eq!(shape_is(&v, &observe(&w)), expected, "v {v:?} w {w:?}");
equal += usize::from(expected);
}
assert!(equal > 0, "the generator never produced equal shapes");
}
}