This commit is contained in:
2026-04-07 15:16:19 +03:00
parent 953b39d433
commit 0fa388abeb
25 changed files with 1370 additions and 38 deletions
+50 -1
View File
@@ -15,6 +15,7 @@ pub enum Value {
NumberList(Rc<Vec<Decimal>>),
StringList(Rc<Vec<SmolStr>>),
Object(Rc<IndexMap<SmolStr, Value>>),
List(Rc<Vec<Value>>),
}
/// Type tag constants for serialization
@@ -25,6 +26,7 @@ pub const TYPE_BOOLEAN: u8 = 0x03;
pub const TYPE_NUMBER_LIST: u8 = 0x04;
pub const TYPE_STRING_LIST: u8 = 0x05;
pub const TYPE_OBJECT: u8 = 0x06;
pub const TYPE_LIST: u8 = 0x07;
impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -63,6 +65,16 @@ impl fmt::Display for Value {
}
write!(f, "}}")
}
Value::List(list) => {
write!(f, "[")?;
for (i, val) in list.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}", val)?;
}
write!(f, "]")
}
}
}
}
@@ -78,6 +90,7 @@ impl Value {
Value::NumberList(_) => TYPE_NUMBER_LIST,
Value::StringList(_) => TYPE_STRING_LIST,
Value::Object(_) => TYPE_OBJECT,
Value::List(_) => TYPE_LIST,
}
}
@@ -91,6 +104,7 @@ impl Value {
Value::NumberList(_) => "NumberList",
Value::StringList(_) => "StringList",
Value::Object(_) => "Object",
Value::List(_) => "List",
}
}
@@ -150,6 +164,15 @@ impl Value {
bytes.extend_from_slice(&val.serialize());
}
}
Value::List(list) => {
// List length (2 bytes)
bytes.push((list.len() >> 8) as u8);
bytes.push(list.len() as u8);
// List items (recursive serialization)
for val in list.iter() {
bytes.extend_from_slice(&val.serialize());
}
}
}
bytes
@@ -226,6 +249,12 @@ impl From<IndexMap<SmolStr, Value>> for Value {
}
}
impl From<Vec<Value>> for Value {
fn from(v: Vec<Value>) -> Self {
Value::List(Rc::new(v))
}
}
impl Value {
/// Deserialize a value from bytes
pub fn deserialize(bytes: &[u8]) -> Result<(Value, usize), String> {
@@ -357,6 +386,22 @@ impl Value {
Ok((Value::Object(Rc::new(map)), pos))
}
TYPE_LIST => {
if bytes.len() < pos + 2 {
return Err("Insufficient bytes for List length".to_string());
}
let len = u16::from_be_bytes([bytes[pos], bytes[pos + 1]]) as usize;
pos += 2;
let mut list = Vec::with_capacity(len);
for _ in 0..len {
let (val, val_bytes) = Value::deserialize(&bytes[pos..])?;
pos += val_bytes;
list.push(val);
}
Ok((Value::List(Rc::new(list)), pos))
}
_ => Err(format!("Unknown type tag: {}", type_tag)),
}
}
@@ -426,7 +471,11 @@ impl Value {
.collect();
Ok(Value::StringList(Rc::new(strings)))
} else {
Err("Arrays must contain all numbers or all strings".to_string())
let mut items = Vec::with_capacity(arr.len());
for item in arr {
items.push(Self::from_json_value(item)?);
}
Ok(Value::List(Rc::new(items)))
}
}
serde_json::Value::Object(obj) => {
+30
View File
@@ -121,6 +121,19 @@ impl LanguageInfo {
type_name: v.type_name().to_string(),
}).collect())
}
Value::List(list) => {
// For List of Objects, derive fields from the first Object element
list.iter().find_map(|item| {
if let Value::Object(map) = item {
Some(map.iter().map(|(k, v)| FieldInfo {
name: k.to_string(),
type_name: v.type_name().to_string(),
}).collect())
} else {
None
}
})
}
_ => None,
};
self.variables.push(VariableInfo {
@@ -276,6 +289,23 @@ fn builtin_methods() -> Vec<(&'static str, Vec<MethodInfo>)> {
MethodInfo { name: "contains", signature: "(key: String) -> Boolean", doc: Some("Check if key exists") },
MethodInfo { name: "get", signature: "(key: String) -> any", doc: Some("Get value by key") },
]),
("List", vec![
MethodInfo { name: "length", signature: "() -> Number", doc: Some("Number of elements") },
MethodInfo { name: "len", signature: "() -> Number", doc: None },
MethodInfo { name: "isEmpty", signature: "() -> Boolean", doc: None },
MethodInfo { name: "first", signature: "() -> any", doc: Some("First element") },
MethodInfo { name: "last", signature: "() -> any", doc: Some("Last element") },
MethodInfo { name: "get", signature: "(index: Number) -> any", doc: None },
MethodInfo { name: "contains", signature: "(value: any) -> Boolean", doc: None },
MethodInfo { name: "indexOf", signature: "(value: any) -> Number", doc: None },
MethodInfo { name: "slice", signature: "(start: Number, end?: Number) -> List", doc: None },
MethodInfo { name: "reverse", signature: "() -> List", doc: None },
MethodInfo { name: "join", signature: "(delim?: String) -> String", doc: None },
MethodInfo { name: "map", signature: "(field: String) -> NumberList | StringList | List", doc: Some("Extract field from each Object element") },
MethodInfo { name: "filter", signature: "(field: String, value?: any) -> List", doc: Some("Filter by field value or truthy field") },
MethodInfo { name: "find", signature: "(field: String, value?: any) -> any", doc: Some("Find first element matching field condition") },
MethodInfo { name: "sort", signature: "(field: String) -> List", doc: Some("Sort by field value") },
]),
("StringList", vec![
MethodInfo { name: "length", signature: "() -> Number", doc: None },
MethodInfo { name: "len", signature: "() -> Number", doc: None },
+1
View File
@@ -160,6 +160,7 @@ impl<'a> VM<'a> {
Value::StringList(l) => Decimal::from(l.len()),
Value::NumberList(l) => Decimal::from(l.len()),
Value::Object(m) => Decimal::from(m.len()),
Value::List(l) => Decimal::from(l.len()),
other => {
return Err(VMError::RuntimeError(format!(
"len() not supported for type {}",
+252 -3
View File
@@ -26,6 +26,7 @@ impl<'a> VM<'a> {
Value::StringList(_) => self.dispatch_string_list_method_inner(&obj_val, method, args),
Value::NumberList(_) => self.dispatch_number_list_method_inner(&obj_val, method, args),
Value::Object(_) => self.dispatch_object_method_inner(&obj_val, method, args),
Value::List(_) => self.dispatch_list_method_inner(&obj_val, method, args),
_ => {
// Try external methods for any type
let type_name: SmolStr = obj_val.type_name().into();
@@ -452,9 +453,7 @@ impl<'a> VM<'a> {
.collect();
Ok(Value::NumberList(Rc::new(numbers)))
} else {
Err(VMError::RuntimeError(
"values() only works when all values are the same type (String or Number)".to_string(),
))
Ok(Value::List(Rc::new(vals)))
}
}
"length" | "len" => Ok(Value::Number(Decimal::from(map.len()))),
@@ -497,4 +496,254 @@ impl<'a> VM<'a> {
}
}
}
fn dispatch_list_method_inner(
&self,
obj_val: &Value,
method: &str,
args: &[Value],
) -> Result<Value, VMError> {
let list = match obj_val {
Value::List(l) => l,
_ => unreachable!(),
};
match method {
"length" | "len" => Ok(Value::Number(Decimal::from(list.len()))),
"isEmpty" => Ok(Value::Boolean(list.is_empty())),
"first" => Ok(list.first().cloned().unwrap_or(Value::Null)),
"last" => Ok(list.last().cloned().unwrap_or(Value::Null)),
"get" => {
if args.is_empty() {
return Err(VMError::RuntimeError("get() requires an index".to_string()));
}
match &args[0] {
Value::Number(idx) => {
let index = idx.to_usize().unwrap_or(usize::MAX);
Ok(list.get(index).cloned().unwrap_or(Value::Null))
}
_ => Err(VMError::RuntimeError("get() requires a number index".to_string())),
}
}
"contains" => {
if args.is_empty() {
return Err(VMError::RuntimeError("contains() requires an argument".to_string()));
}
Ok(Value::Boolean(list.contains(&args[0])))
}
"indexOf" => {
if args.is_empty() {
return Err(VMError::RuntimeError("indexOf() requires an argument".to_string()));
}
let idx = list.iter().position(|item| item == &args[0]);
Ok(idx.map(|i| Value::Number(Decimal::from(i))).unwrap_or(Value::Number(Decimal::from(-1))))
}
"slice" => {
if args.is_empty() {
return Err(VMError::RuntimeError("slice() requires at least a start index".to_string()));
}
match &args[0] {
Value::Number(start_idx) => {
let start = start_idx.to_usize().unwrap_or(0).min(list.len());
let end = if args.len() > 1 {
match &args[1] {
Value::Number(end_idx) => end_idx.to_usize().unwrap_or(list.len()).min(list.len()),
_ => list.len(),
}
} else {
list.len()
};
Ok(Value::List(Rc::new(list[start..end].to_vec())))
}
_ => Err(VMError::RuntimeError("slice() requires a number index".to_string())),
}
}
"reverse" => {
let mut reversed = list.to_vec();
reversed.reverse();
Ok(Value::List(Rc::new(reversed)))
}
"join" => {
let delim = if args.is_empty() {
""
} else {
match &args[0] {
Value::String(s) => s.as_str(),
_ => return Err(VMError::RuntimeError("join() requires a string delimiter".to_string())),
}
};
let strings: Vec<String> = list.iter().map(|v| format!("{}", v)).collect();
Ok(Value::String(SmolStr::new(strings.join(delim))))
}
"map" => {
// Property shorthand: list.map("fieldName") extracts a field from each Object element
if args.is_empty() {
return Err(VMError::RuntimeError("map() requires a field name argument".to_string()));
}
match &args[0] {
Value::String(field) => {
let mut values: Vec<Value> = Vec::with_capacity(list.len());
for item in list.iter() {
match item {
Value::Object(map) => {
values.push(map.get(field.as_str()).cloned().unwrap_or(Value::Null));
}
_ => {
return Err(VMError::RuntimeError(format!(
"map() requires all elements to be Objects, got {}",
item.type_name()
)));
}
}
}
// Smart return type: if all values are the same primitive type, return typed list
if values.is_empty() {
return Ok(Value::List(Rc::new(values)));
}
if values.iter().all(|v| matches!(v, Value::Number(_))) {
let nums: Vec<Decimal> = values
.into_iter()
.map(|v| match v {
Value::Number(n) => n,
_ => unreachable!(),
})
.collect();
Ok(Value::NumberList(Rc::new(nums)))
} else if values.iter().all(|v| matches!(v, Value::String(_))) {
let strings: Vec<SmolStr> = values
.into_iter()
.map(|v| match v {
Value::String(s) => s,
_ => unreachable!(),
})
.collect();
Ok(Value::StringList(Rc::new(strings)))
} else {
Ok(Value::List(Rc::new(values)))
}
}
_ => Err(VMError::RuntimeError("map() requires a string field name".to_string())),
}
}
"filter" => {
// Property shorthand:
// list.filter("active") — filter by truthy boolean field
// list.filter("field", value) — filter where field == value
if args.is_empty() {
return Err(VMError::RuntimeError("filter() requires a field name argument".to_string()));
}
match &args[0] {
Value::String(field) => {
let filtered: Result<Vec<Value>, VMError> = list
.iter()
.filter_map(|item| {
match item {
Value::Object(map) => {
let field_val = map.get(field.as_str()).cloned().unwrap_or(Value::Null);
if args.len() > 1 {
// filter("field", value) — equality check
if field_val == args[1] {
Some(Ok(item.clone()))
} else {
None
}
} else {
// filter("field") — truthy check
match &field_val {
Value::Boolean(b) => if *b { Some(Ok(item.clone())) } else { None },
Value::Null => None,
_ => Some(Ok(item.clone())),
}
}
}
_ => Some(Err(VMError::RuntimeError(format!(
"filter() requires all elements to be Objects, got {}",
item.type_name()
)))),
}
})
.collect();
Ok(Value::List(Rc::new(filtered?)))
}
_ => Err(VMError::RuntimeError("filter() requires a string field name".to_string())),
}
}
"find" => {
// Property shorthand: list.find("field", value) — first element where field == value
if args.is_empty() {
return Err(VMError::RuntimeError("find() requires a field name argument".to_string()));
}
match &args[0] {
Value::String(field) => {
for item in list.iter() {
match item {
Value::Object(map) => {
let field_val = map.get(field.as_str()).cloned().unwrap_or(Value::Null);
if args.len() > 1 {
if field_val == args[1] {
return Ok(item.clone());
}
} else {
// find("field") — first with truthy field
match &field_val {
Value::Boolean(b) => if *b { return Ok(item.clone()); },
Value::Null => {},
_ => return Ok(item.clone()),
}
}
}
_ => {
return Err(VMError::RuntimeError(format!(
"find() requires all elements to be Objects, got {}",
item.type_name()
)));
}
}
}
Ok(Value::Null)
}
_ => Err(VMError::RuntimeError("find() requires a string field name".to_string())),
}
}
"sort" => {
// Property shorthand: list.sort("field") — sort by field value
if args.is_empty() {
return Err(VMError::RuntimeError("sort() on List requires a field name argument".to_string()));
}
match &args[0] {
Value::String(field) => {
let mut sorted = list.to_vec();
sorted.sort_by(|a, b| {
let a_val = match a {
Value::Object(map) => map.get(field.as_str()).cloned().unwrap_or(Value::Null),
_ => Value::Null,
};
let b_val = match b {
Value::Object(map) => map.get(field.as_str()).cloned().unwrap_or(Value::Null),
_ => Value::Null,
};
match (&a_val, &b_val) {
(Value::Number(a), Value::Number(b)) => a.cmp(b),
(Value::String(a), Value::String(b)) => a.cmp(b),
_ => std::cmp::Ordering::Equal,
}
});
Ok(Value::List(Rc::new(sorted)))
}
_ => Err(VMError::RuntimeError("sort() requires a string field name".to_string())),
}
}
_ => {
let key = (SmolStr::new_static("List"), SmolStr::from(method));
if let Some(ext_method) = self.external_methods.as_ref().and_then(|m| m.get(&key)) {
ext_method(obj_val, args).map_err(VMError::RuntimeError)
} else {
Err(VMError::MethodNotFound {
type_name: "List",
method: SmolStr::from(method),
})
}
}
}
}
}
+38
View File
@@ -597,6 +597,8 @@ impl<'a> VM<'a> {
}
// String in Object (key check)
(Value::String(key), Value::Object(map)) => map.contains_key(key),
// Value in List
(needle_val, Value::List(list)) => list.contains(needle_val),
(needle_val, haystack_val) => {
return Err(VMError::InvalidOperation {
operation: "in",
@@ -684,6 +686,42 @@ impl<'a> VM<'a> {
let value = map.get(prop).cloned().unwrap_or(Value::Null);
self.registers[dest] = value;
}
Value::List(list) => {
// Property projection: list.field → extract field from each Object element
let mut values: Vec<Value> = Vec::with_capacity(list.len());
for item in list.iter() {
match item {
Value::Object(map) => {
values.push(map.get(prop).cloned().unwrap_or(Value::Null));
}
_ => {
return Err(VMError::RuntimeError(format!(
"Cannot access property '{}' on non-Object element in List (got {})",
prop,
item.type_name()
)));
}
}
}
// Smart return type: NumberList/StringList when homogeneous
if values.is_empty() {
self.registers[dest] = Value::List(Rc::new(values));
} else if values.iter().all(|v| matches!(v, Value::Number(_))) {
let nums: Vec<Decimal> = values.into_iter().map(|v| match v {
Value::Number(n) => n,
_ => unreachable!(),
}).collect();
self.registers[dest] = Value::NumberList(Rc::new(nums));
} else if values.iter().all(|v| matches!(v, Value::String(_))) {
let strings: Vec<SmolStr> = values.into_iter().map(|v| match v {
Value::String(s) => s,
_ => unreachable!(),
}).collect();
self.registers[dest] = Value::StringList(Rc::new(strings));
} else {
self.registers[dest] = Value::List(Rc::new(values));
}
}
other => {
return Err(VMError::RuntimeError(format!(
"Cannot access property '{}' on type {}",