Skip to content

Commit fe548d4

Browse files
committed
pd-vm: map uses hashmap
1 parent 114b14c commit fe548d4

20 files changed

Lines changed: 526 additions & 189 deletions

File tree

pd-edge/src/abi_impl/mod.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ use edge_abi::FUNCTIONS as EDGE_ABI_FUNCTIONS;
1515
use http_body_util::BodyExt;
1616
use tokio::sync::oneshot;
1717
use url::Url;
18-
use vm::{CallOutcome, HostAsyncBridge, HostFunction, HostOpId, Value, Vm, VmError};
18+
use vm::{
19+
CallOutcome, HostAsyncBridge, HostFunction, HostOpId, Value, Vm, VmError, bytecode::VmMap,
20+
};
1921

2022
mod http;
2123
mod io;
@@ -921,7 +923,7 @@ fn expect_int(args: &[Value], index: usize) -> Result<i64, VmError> {
921923
}
922924
}
923925

924-
fn expect_map(args: &[Value], index: usize) -> Result<Vec<(Value, Value)>, VmError> {
926+
fn expect_map(args: &[Value], index: usize) -> Result<VmMap, VmError> {
925927
match args.get(index) {
926928
Some(Value::Map(entries)) => Ok(entries.as_ref().clone()),
927929
_ => Err(VmError::TypeMismatch("map")),

pd-vm/build.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1190,6 +1190,7 @@ fn core_prefix_builtin_order() -> &'static [&'static str] {
11901190
"array_push",
11911191
"map_new",
11921192
"get",
1193+
"has",
11931194
"set",
11941195
"keys",
11951196
]
@@ -1212,6 +1213,7 @@ fn required_language_builtin_stubs() -> &'static [&'static str] {
12121213
"array_push",
12131214
"map_new",
12141215
"get",
1216+
"has",
12151217
"set",
12161218
"keys",
12171219
"count",

pd-vm/pd-vm-wasm/src/lib.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ use std::path::Path;
99
use serde::{Deserialize, Serialize};
1010
use vm::{
1111
CompileSourceFileOptions, InferredLocalTypeHint, SourceFlavor,
12-
collect_inferred_local_type_hints_at_path_with_options, collect_inferred_local_type_hints_with_options,
12+
collect_inferred_local_type_hints_at_path_with_options,
13+
collect_inferred_local_type_hints_with_options,
1314
};
1415

1516
use crate::analyzer::{
@@ -724,10 +725,10 @@ pub extern "C" fn completion_catalog_json() -> u64 {
724725
mod lint_tests {
725726
use std::path::Path;
726727

727-
use serde_json::Value;
728728
use super::{parse_flavor, parse_module_overrides};
729729
use crate::analyzer::{lint_source_with_flavor, lint_source_with_flavor_at_path};
730730
use crate::completions::build_completion_catalog;
731+
use serde_json::Value;
731732
use vm::{SourceFlavor, collect_inferred_local_type_hints};
732733

733734
#[test]
@@ -793,8 +794,8 @@ mod lint_tests {
793794
plus_one(2);
794795
"#;
795796

796-
let hints =
797-
collect_inferred_local_type_hints(source, SourceFlavor::RustScript).expect("type hints should succeed");
797+
let hints = collect_inferred_local_type_hints(source, SourceFlavor::RustScript)
798+
.expect("type hints should succeed");
798799
let payload: Value = serde_json::from_slice(&super::local_type_hints_to_json(hints))
799800
.expect("type hints should serialize as json");
800801
let items = payload["hints"]

pd-vm/src/builtins/runtime/core.rs

Lines changed: 80 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ pub(super) fn builtin_array_push(args: Vec<Value>) -> VmResult<Vec<Value>> {
167167
/// Create an empty map.
168168
#[pd_host_function(name = "map_new")]
169169
pub(super) fn builtin_map_new_impl() -> VmMap {
170-
Vec::new()
170+
VmMap::new()
171171
}
172172

173173
/// Read a string entry.
@@ -208,12 +208,43 @@ pub(super) fn builtin_get_array_impl(items: VmArray, index: i64) -> VmResult<Unk
208208
/// Read a map value by key.
209209
#[pd_host_function(name = "get")]
210210
pub(super) fn builtin_get_map_impl(entries: VmMap, key: AnyValue) -> VmResult<UnknownValue> {
211-
for (existing_key, value) in entries {
212-
if existing_key == key {
213-
return Ok(value);
214-
}
211+
entries
212+
.get(&key)
213+
.cloned()
214+
.ok_or_else(|| VmError::HostError("map key not found".to_string()))
215+
}
216+
217+
/// Check whether an array contains a valid index.
218+
#[pd_host_function(name = "has")]
219+
pub(super) fn builtin_has_array_impl(items: VmArray, index: i64) -> bool {
220+
if index < 0 {
221+
return false;
222+
}
223+
usize::try_from(index)
224+
.ok()
225+
.is_some_and(|index| index < items.len())
226+
}
227+
228+
/// Check whether a map contains a key.
229+
#[pd_host_function(name = "has")]
230+
pub(super) fn builtin_has_map_impl(entries: VmMap, key: AnyValue) -> bool {
231+
entries.get(&key).is_some()
232+
}
233+
234+
pub(super) fn builtin_has(args: &[Value]) -> VmResult<Vec<Value>> {
235+
let container = arg::<&Value>(args, 0, "has container")?;
236+
let key = arg::<&Value>(args, 1, "has key")?;
237+
match container {
238+
Value::Array(values) => Ok(return_values(builtin_has_array_impl(
239+
unwrap_or_clone_shared(values.clone()),
240+
key.as_int()?,
241+
))),
242+
Value::Map(entries) => Ok(return_values(builtin_has_map_impl(
243+
unwrap_or_clone_shared(entries.clone()),
244+
key.clone(),
245+
))),
246+
_ => Err(VmError::TypeMismatch("array/map")),
215247
}
216-
Err(VmError::HostError("map key not found".to_string()))
217248
}
218249

219250
pub(super) fn builtin_get(args: Vec<Value>) -> VmResult<Vec<Value>> {
@@ -436,13 +467,10 @@ pub(super) fn builtin_set_array_impl(
436467
/// Update a map entry and return the updated map.
437468
#[pd_host_function(name = "set")]
438469
pub(super) fn builtin_set_map_impl(mut entries: VmMap, key: AnyValue, value: AnyValue) -> VmMap {
439-
if let Some((_, existing_value)) = entries
440-
.iter_mut()
441-
.find(|(existing_key, _)| *existing_key == key)
442-
{
443-
*existing_value = value;
470+
if matches!(value, Value::Null) {
471+
entries.remove(&key);
444472
} else {
445-
entries.push((key, value));
473+
entries.insert(key, value);
446474
}
447475
entries
448476
}
@@ -599,14 +627,51 @@ mod tests {
599627
panic!("expected map alias");
600628
};
601629

630+
assert_eq!(alias_entries.len(), 1);
631+
assert_eq!(alias_entries.get(&Value::string("k")), Some(&Value::Int(1)));
632+
assert_eq!(result.len(), 1);
633+
assert_eq!(result.get(&Value::string("k")), Some(&Value::Int(9)));
634+
assert!(
635+
!Arc::ptr_eq(alias_entries, result),
636+
"mutating a shared map should detach backing storage"
637+
);
638+
}
639+
640+
#[test]
641+
fn set_map_null_removes_entry() {
642+
let shared = Value::map(vec![(Value::string("drop"), Value::Int(1))]);
643+
let alias = shared.clone();
644+
645+
let out = builtin_set(vec![shared, Value::string("drop"), Value::Null])
646+
.expect("map null set should work");
647+
let [Value::Map(result)] = out.as_slice() else {
648+
panic!("expected map result");
649+
};
650+
let Value::Map(alias_entries) = &alias else {
651+
panic!("expected map alias");
652+
};
653+
654+
assert_eq!(alias_entries.len(), 1);
602655
assert_eq!(
603-
alias_entries.as_ref(),
604-
&vec![(Value::string("k"), Value::Int(1))]
656+
alias_entries.get(&Value::string("drop")),
657+
Some(&Value::Int(1))
605658
);
606-
assert_eq!(result.as_ref(), &vec![(Value::string("k"), Value::Int(9))]);
659+
assert_eq!(result.len(), 0);
660+
assert_eq!(result.get(&Value::string("drop")), None);
607661
assert!(
608662
!Arc::ptr_eq(alias_entries, result),
609663
"mutating a shared map should detach backing storage"
610664
);
611665
}
666+
667+
#[test]
668+
fn has_map_uses_identity_for_heap_keys() {
669+
let key = Value::array(vec![Value::Int(1), Value::Int(2)]);
670+
let alias = key.clone();
671+
let structural_peer = Value::array(vec![Value::Int(1), Value::Int(2)]);
672+
let map = VmMap::from(vec![(key, Value::Bool(true))]);
673+
674+
assert!(builtin_has_map_impl(map.clone(), alias));
675+
assert!(!builtin_has_map_impl(map, structural_peer));
676+
}
612677
}

pd-vm/src/builtins/runtime/jit.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ use crate::vm::{Value, Vm, VmResult};
66
fn config_as_map(vm: &Vm) -> VmMap {
77
let config = vm.jit_config();
88
let max_trace_len = i64::try_from(config.max_trace_len).unwrap_or(i64::MAX);
9-
vec![
9+
VmMap::from_entries(vec![
1010
(Value::string("enabled"), Value::Bool(config.enabled)),
1111
(
1212
Value::string("hot_loop_threshold"),
1313
Value::Int(i64::from(config.hot_loop_threshold)),
1414
),
1515
(Value::string("max_trace_len"), Value::Int(max_trace_len)),
16-
]
16+
])
1717
}
1818

1919
#[pd_host_function(name = "jit::set_config")]

pd-vm/src/builtins/runtime/json.rs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ use crate::vm::{Value, VmError, VmResult};
88
use pd_host_function::pd_host_function;
99

1010
/// Encodes a `Value` into a JSON string.
11-
/// Note: When encoding a `Value::Map`, this function enforces a strict unique-keys contract.
12-
/// If the map contains duplicate keys, it will return an error rather than silently omitting data.
1311
#[pd_host_function(name = "json::encode")]
1412
pub(super) fn builtin_json_encode(value: &AnyValue) -> VmResult<String> {
1513
let json_value = vm_to_json_value(value)?;
@@ -55,11 +53,6 @@ fn vm_to_json_value(value: &Value) -> VmResult<JsonValue> {
5553
));
5654
}
5755
};
58-
if out.contains_key(key.as_str()) {
59-
return Err(VmError::HostError(format!(
60-
"json_encode map keys must be unique strings; duplicate key '{key}'"
61-
)));
62-
}
6356
out.insert(key.as_str().to_string(), vm_to_json_value(value)?);
6457
}
6558
Ok(JsonValue::Object(out))

pd-vm/src/builtins/runtime/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ pub(crate) fn execute_builtin_call(
5656
core::builtin_map_new_impl(),
5757
))),
5858
BuiltinFunction::Get => core::builtin_get(args).map(BuiltinCallOutcome::Return),
59+
BuiltinFunction::Has => core::builtin_has(&args).map(BuiltinCallOutcome::Return),
5960
BuiltinFunction::Set => core::builtin_set(args).map(BuiltinCallOutcome::Return),
6061
BuiltinFunction::Keys => core::builtin_keys(args).map(BuiltinCallOutcome::Return),
6162
BuiltinFunction::Count => core::builtin_count(&args).map(BuiltinCallOutcome::Return),

pd-vm/src/builtins/runtime/typed.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
use super::BuiltinCallOutcome;
2+
pub(super) use crate::bytecode::VmMap;
23
use crate::vm::{CallOutcome, HostOpId, Value, VmError, VmResult};
34

45
pub(super) type AnyValue = Value;
56
pub(super) type UnknownValue = Value;
67
pub(super) type VmArray = Vec<Value>;
7-
pub(super) type VmMap = Vec<(Value, Value)>;
88

99
#[derive(Clone, Copy, Debug, PartialEq)]
1010
pub(super) enum NumberValue {
@@ -245,6 +245,12 @@ impl IntoVmValue for Vec<(Value, Value)> {
245245
}
246246
}
247247

248+
impl IntoVmValue for VmMap {
249+
fn into_vm_value(self) -> Value {
250+
Value::Map(self.into())
251+
}
252+
}
253+
248254
impl IntoVmValue for NumberValue {
249255
fn into_vm_value(self) -> Value {
250256
match self {

0 commit comments

Comments
 (0)