feat: add built-in Map type with String keys
Add Map<String, V> as a first-class built-in type for key-value storage, needed for self-hosting the compiler (parser/typechecker/interpreter all rely heavily on hashmaps). - types.rs: Type::Map(K,V) variant, all match arms (unify, apply, etc.) - interpreter.rs: Value::Map, 12 BuiltinFn variants (new/set/get/contains/ remove/keys/values/size/isEmpty/fromList/toList/merge), immutable semantics - typechecker.rs: Map<K,V> resolution in resolve_type - js_backend.rs: Map as JS Map with emit_map_operation() - c_backend.rs: LuxMap struct (linear-scan), runtime fns, emit_map_operation() - main.rs: 12 tests covering all Map operations - validate.sh: now checks all projects/ directories too Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -100,7 +100,9 @@ pub enum BuiltinFn {
|
||||
|
||||
// Int/Float operations
|
||||
IntToString,
|
||||
IntToFloat,
|
||||
FloatToString,
|
||||
FloatToInt,
|
||||
|
||||
// JSON operations
|
||||
JsonParse,
|
||||
@@ -122,6 +124,20 @@ pub enum BuiltinFn {
|
||||
JsonString,
|
||||
JsonArray,
|
||||
JsonObject,
|
||||
|
||||
// Map operations
|
||||
MapNew,
|
||||
MapSet,
|
||||
MapGet,
|
||||
MapContains,
|
||||
MapRemove,
|
||||
MapKeys,
|
||||
MapValues,
|
||||
MapSize,
|
||||
MapIsEmpty,
|
||||
MapFromList,
|
||||
MapToList,
|
||||
MapMerge,
|
||||
}
|
||||
|
||||
/// Runtime value
|
||||
@@ -136,6 +152,7 @@ pub enum Value {
|
||||
List(Vec<Value>),
|
||||
Tuple(Vec<Value>),
|
||||
Record(HashMap<String, Value>),
|
||||
Map(HashMap<String, Value>),
|
||||
Function(Rc<Closure>),
|
||||
Handler(Rc<HandlerValue>),
|
||||
/// Built-in function
|
||||
@@ -167,6 +184,7 @@ impl Value {
|
||||
Value::List(_) => "List",
|
||||
Value::Tuple(_) => "Tuple",
|
||||
Value::Record(_) => "Record",
|
||||
Value::Map(_) => "Map",
|
||||
Value::Function(_) => "Function",
|
||||
Value::Handler(_) => "Handler",
|
||||
Value::Builtin(_) => "Function",
|
||||
@@ -215,6 +233,11 @@ impl Value {
|
||||
ys.get(k).map(|yv| Value::values_equal(v, yv)).unwrap_or(false)
|
||||
})
|
||||
}
|
||||
(Value::Map(xs), Value::Map(ys)) => {
|
||||
xs.len() == ys.len() && xs.iter().all(|(k, v)| {
|
||||
ys.get(k).map(|yv| Value::values_equal(v, yv)).unwrap_or(false)
|
||||
})
|
||||
}
|
||||
(Value::Constructor { name: n1, fields: f1 }, Value::Constructor { name: n2, fields: f2 }) => {
|
||||
n1 == n2 && f1.len() == f2.len() && f1.iter().zip(f2.iter()).all(|(x, y)| Value::values_equal(x, y))
|
||||
}
|
||||
@@ -285,6 +308,16 @@ impl TryFromValue for Vec<Value> {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFromValue for HashMap<String, Value> {
|
||||
const TYPE_NAME: &'static str = "Map";
|
||||
fn try_from_value(value: &Value) -> Option<Self> {
|
||||
match value {
|
||||
Value::Map(m) => Some(m.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFromValue for Value {
|
||||
const TYPE_NAME: &'static str = "any";
|
||||
fn try_from_value(value: &Value) -> Option<Self> {
|
||||
@@ -331,6 +364,18 @@ impl fmt::Display for Value {
|
||||
}
|
||||
write!(f, " }}")
|
||||
}
|
||||
Value::Map(entries) => {
|
||||
write!(f, "Map {{")?;
|
||||
let mut sorted: Vec<_> = entries.iter().collect();
|
||||
sorted.sort_by_key(|(k, _)| (*k).clone());
|
||||
for (i, (key, value)) in sorted.iter().enumerate() {
|
||||
if i > 0 {
|
||||
write!(f, ", ")?;
|
||||
}
|
||||
write!(f, "\"{}\": {}", key, value)?;
|
||||
}
|
||||
write!(f, "}}")
|
||||
}
|
||||
Value::Function(_) => write!(f, "<function>"),
|
||||
Value::Builtin(b) => write!(f, "<builtin:{:?}>", b),
|
||||
Value::Handler(_) => write!(f, "<handler>"),
|
||||
@@ -1084,12 +1129,14 @@ impl Interpreter {
|
||||
// Int module
|
||||
let int_module = Value::Record(HashMap::from([
|
||||
("toString".to_string(), Value::Builtin(BuiltinFn::IntToString)),
|
||||
("toFloat".to_string(), Value::Builtin(BuiltinFn::IntToFloat)),
|
||||
]));
|
||||
env.define("Int", int_module);
|
||||
|
||||
// Float module
|
||||
let float_module = Value::Record(HashMap::from([
|
||||
("toString".to_string(), Value::Builtin(BuiltinFn::FloatToString)),
|
||||
("toInt".to_string(), Value::Builtin(BuiltinFn::FloatToInt)),
|
||||
]));
|
||||
env.define("Float", float_module);
|
||||
|
||||
@@ -1116,6 +1163,23 @@ impl Interpreter {
|
||||
("object".to_string(), Value::Builtin(BuiltinFn::JsonObject)),
|
||||
]));
|
||||
env.define("Json", json_module);
|
||||
|
||||
// Map module
|
||||
let map_module = Value::Record(HashMap::from([
|
||||
("new".to_string(), Value::Builtin(BuiltinFn::MapNew)),
|
||||
("set".to_string(), Value::Builtin(BuiltinFn::MapSet)),
|
||||
("get".to_string(), Value::Builtin(BuiltinFn::MapGet)),
|
||||
("contains".to_string(), Value::Builtin(BuiltinFn::MapContains)),
|
||||
("remove".to_string(), Value::Builtin(BuiltinFn::MapRemove)),
|
||||
("keys".to_string(), Value::Builtin(BuiltinFn::MapKeys)),
|
||||
("values".to_string(), Value::Builtin(BuiltinFn::MapValues)),
|
||||
("size".to_string(), Value::Builtin(BuiltinFn::MapSize)),
|
||||
("isEmpty".to_string(), Value::Builtin(BuiltinFn::MapIsEmpty)),
|
||||
("fromList".to_string(), Value::Builtin(BuiltinFn::MapFromList)),
|
||||
("toList".to_string(), Value::Builtin(BuiltinFn::MapToList)),
|
||||
("merge".to_string(), Value::Builtin(BuiltinFn::MapMerge)),
|
||||
]));
|
||||
env.define("Map", map_module);
|
||||
}
|
||||
|
||||
/// Execute a program
|
||||
@@ -2364,6 +2428,26 @@ impl Interpreter {
|
||||
}
|
||||
}
|
||||
|
||||
BuiltinFn::IntToFloat => {
|
||||
if args.len() != 1 {
|
||||
return Err(err("Int.toFloat requires 1 argument"));
|
||||
}
|
||||
match &args[0] {
|
||||
Value::Int(n) => Ok(EvalResult::Value(Value::Float(*n as f64))),
|
||||
v => Err(err(&format!("Int.toFloat expects Int, got {}", v.type_name()))),
|
||||
}
|
||||
}
|
||||
|
||||
BuiltinFn::FloatToInt => {
|
||||
if args.len() != 1 {
|
||||
return Err(err("Float.toInt requires 1 argument"));
|
||||
}
|
||||
match &args[0] {
|
||||
Value::Float(f) => Ok(EvalResult::Value(Value::Int(*f as i64))),
|
||||
v => Err(err(&format!("Float.toInt expects Float, got {}", v.type_name()))),
|
||||
}
|
||||
}
|
||||
|
||||
BuiltinFn::TypeOf => {
|
||||
if args.len() != 1 {
|
||||
return Err(err("typeOf requires 1 argument"));
|
||||
@@ -3068,6 +3152,128 @@ impl Interpreter {
|
||||
}
|
||||
Ok(EvalResult::Value(Value::Json(serde_json::Value::Object(map))))
|
||||
}
|
||||
|
||||
// Map operations
|
||||
BuiltinFn::MapNew => {
|
||||
Ok(EvalResult::Value(Value::Map(HashMap::new())))
|
||||
}
|
||||
|
||||
BuiltinFn::MapSet => {
|
||||
if args.len() != 3 {
|
||||
return Err(err("Map.set requires 3 arguments: map, key, value"));
|
||||
}
|
||||
let mut map = match &args[0] {
|
||||
Value::Map(m) => m.clone(),
|
||||
v => return Err(err(&format!("Map.set expects Map as first argument, got {}", v.type_name()))),
|
||||
};
|
||||
let key = match &args[1] {
|
||||
Value::String(s) => s.clone(),
|
||||
v => return Err(err(&format!("Map.set expects String key, got {}", v.type_name()))),
|
||||
};
|
||||
map.insert(key, args[2].clone());
|
||||
Ok(EvalResult::Value(Value::Map(map)))
|
||||
}
|
||||
|
||||
BuiltinFn::MapGet => {
|
||||
let (map, key) = Self::expect_args_2::<HashMap<String, Value>, String>(&args, "Map.get", span)?;
|
||||
match map.get(&key) {
|
||||
Some(v) => Ok(EvalResult::Value(Value::Constructor {
|
||||
name: "Some".to_string(),
|
||||
fields: vec![v.clone()],
|
||||
})),
|
||||
None => Ok(EvalResult::Value(Value::Constructor {
|
||||
name: "None".to_string(),
|
||||
fields: vec![],
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
BuiltinFn::MapContains => {
|
||||
let (map, key) = Self::expect_args_2::<HashMap<String, Value>, String>(&args, "Map.contains", span)?;
|
||||
Ok(EvalResult::Value(Value::Bool(map.contains_key(&key))))
|
||||
}
|
||||
|
||||
BuiltinFn::MapRemove => {
|
||||
let (mut map, key) = Self::expect_args_2::<HashMap<String, Value>, String>(&args, "Map.remove", span)?;
|
||||
map.remove(&key);
|
||||
Ok(EvalResult::Value(Value::Map(map)))
|
||||
}
|
||||
|
||||
BuiltinFn::MapKeys => {
|
||||
let map = Self::expect_arg_1::<HashMap<String, Value>>(&args, "Map.keys", span)?;
|
||||
let mut keys: Vec<String> = map.keys().cloned().collect();
|
||||
keys.sort();
|
||||
Ok(EvalResult::Value(Value::List(
|
||||
keys.into_iter().map(Value::String).collect(),
|
||||
)))
|
||||
}
|
||||
|
||||
BuiltinFn::MapValues => {
|
||||
let map = Self::expect_arg_1::<HashMap<String, Value>>(&args, "Map.values", span)?;
|
||||
let mut entries: Vec<(String, Value)> = map.into_iter().collect();
|
||||
entries.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||
Ok(EvalResult::Value(Value::List(
|
||||
entries.into_iter().map(|(_, v)| v).collect(),
|
||||
)))
|
||||
}
|
||||
|
||||
BuiltinFn::MapSize => {
|
||||
let map = Self::expect_arg_1::<HashMap<String, Value>>(&args, "Map.size", span)?;
|
||||
Ok(EvalResult::Value(Value::Int(map.len() as i64)))
|
||||
}
|
||||
|
||||
BuiltinFn::MapIsEmpty => {
|
||||
let map = Self::expect_arg_1::<HashMap<String, Value>>(&args, "Map.isEmpty", span)?;
|
||||
Ok(EvalResult::Value(Value::Bool(map.is_empty())))
|
||||
}
|
||||
|
||||
BuiltinFn::MapFromList => {
|
||||
let list = Self::expect_arg_1::<Vec<Value>>(&args, "Map.fromList", span)?;
|
||||
let mut map = HashMap::new();
|
||||
for item in list {
|
||||
match item {
|
||||
Value::Tuple(fields) if fields.len() == 2 => {
|
||||
let key = match &fields[0] {
|
||||
Value::String(s) => s.clone(),
|
||||
v => return Err(err(&format!("Map.fromList expects (String, V) tuples, got {} key", v.type_name()))),
|
||||
};
|
||||
map.insert(key, fields[1].clone());
|
||||
}
|
||||
_ => return Err(err("Map.fromList expects List<(String, V)>")),
|
||||
}
|
||||
}
|
||||
Ok(EvalResult::Value(Value::Map(map)))
|
||||
}
|
||||
|
||||
BuiltinFn::MapToList => {
|
||||
let map = Self::expect_arg_1::<HashMap<String, Value>>(&args, "Map.toList", span)?;
|
||||
let mut entries: Vec<(String, Value)> = map.into_iter().collect();
|
||||
entries.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||
Ok(EvalResult::Value(Value::List(
|
||||
entries
|
||||
.into_iter()
|
||||
.map(|(k, v)| Value::Tuple(vec![Value::String(k), v]))
|
||||
.collect(),
|
||||
)))
|
||||
}
|
||||
|
||||
BuiltinFn::MapMerge => {
|
||||
if args.len() != 2 {
|
||||
return Err(err("Map.merge requires 2 arguments: map1, map2"));
|
||||
}
|
||||
let mut map1 = match &args[0] {
|
||||
Value::Map(m) => m.clone(),
|
||||
v => return Err(err(&format!("Map.merge expects Map as first argument, got {}", v.type_name()))),
|
||||
};
|
||||
let map2 = match &args[1] {
|
||||
Value::Map(m) => m.clone(),
|
||||
v => return Err(err(&format!("Map.merge expects Map as second argument, got {}", v.type_name()))),
|
||||
};
|
||||
for (k, v) in map2 {
|
||||
map1.insert(k, v);
|
||||
}
|
||||
Ok(EvalResult::Value(Value::Map(map1)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3233,6 +3439,11 @@ impl Interpreter {
|
||||
b.get(k).map(|bv| self.values_equal(v, bv)).unwrap_or(false)
|
||||
})
|
||||
}
|
||||
(Value::Map(a), Value::Map(b)) => {
|
||||
a.len() == b.len() && a.iter().all(|(k, v)| {
|
||||
b.get(k).map(|bv| self.values_equal(v, bv)).unwrap_or(false)
|
||||
})
|
||||
}
|
||||
(
|
||||
Value::Constructor {
|
||||
name: n1,
|
||||
|
||||
Reference in New Issue
Block a user