Skip to content

Commit 9271fb9

Browse files
committed
feat(vm): add borrowed map iteration
1 parent 917e304 commit 9271fb9

21 files changed

Lines changed: 1295 additions & 28 deletions

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ regex = "1"
6767
serde = "1"
6868
serde_json = "1"
6969
rt-format = "0.3.1"
70+
self_cell = "1"
7071
rustyline = { version = "14", optional = true }
7172

7273
[target.'cfg(windows)'.dependencies]

build.rs

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ fn render_builtin_catalog(
346346
.and_then(|value| value.checked_add(1))
347347
.expect("builtin call base should fit in u16");
348348
assert!(
349-
builtin_call_base >= 8,
349+
builtin_call_base >= 14,
350350
"builtin call base must leave room for reserved special builtins"
351351
);
352352

@@ -494,6 +494,31 @@ fn render_builtin_catalog(
494494
" (BUILTIN_CALL_BASE - 8, BuiltinFunction::StringSplitLiteral),"
495495
)
496496
.unwrap();
497+
writeln!(
498+
&mut out,
499+
" (BUILTIN_CALL_BASE - 9, BuiltinFunction::MapIterInit),"
500+
)
501+
.unwrap();
502+
writeln!(
503+
&mut out,
504+
" (BUILTIN_CALL_BASE - 10, BuiltinFunction::MapIterNext),"
505+
)
506+
.unwrap();
507+
writeln!(
508+
&mut out,
509+
" (BUILTIN_CALL_BASE - 11, BuiltinFunction::MapIterTakeKey),"
510+
)
511+
.unwrap();
512+
writeln!(
513+
&mut out,
514+
" (BUILTIN_CALL_BASE - 12, BuiltinFunction::MapIterTakeValue),"
515+
)
516+
.unwrap();
517+
writeln!(
518+
&mut out,
519+
" (BUILTIN_CALL_BASE - 13, BuiltinFunction::MapIterClose),"
520+
)
521+
.unwrap();
497522
writeln!(&mut out, "];").unwrap();
498523
writeln!(&mut out).unwrap();
499524

@@ -662,6 +687,31 @@ fn render_builtin_catalog(
662687
" BuiltinFunction::StringSplitLiteral => BUILTIN_CALL_BASE - 8,"
663688
)
664689
.unwrap();
690+
writeln!(
691+
&mut out,
692+
" BuiltinFunction::MapIterInit => BUILTIN_CALL_BASE - 9,"
693+
)
694+
.unwrap();
695+
writeln!(
696+
&mut out,
697+
" BuiltinFunction::MapIterNext => BUILTIN_CALL_BASE - 10,"
698+
)
699+
.unwrap();
700+
writeln!(
701+
&mut out,
702+
" BuiltinFunction::MapIterTakeKey => BUILTIN_CALL_BASE - 11,"
703+
)
704+
.unwrap();
705+
writeln!(
706+
&mut out,
707+
" BuiltinFunction::MapIterTakeValue => BUILTIN_CALL_BASE - 12,"
708+
)
709+
.unwrap();
710+
writeln!(
711+
&mut out,
712+
" BuiltinFunction::MapIterClose => BUILTIN_CALL_BASE - 13,"
713+
)
714+
.unwrap();
665715
writeln!(
666716
&mut out,
667717
" _ => BUILTIN_CALL_BASE + self as u16,"
@@ -1301,6 +1351,11 @@ fn appended_builtin_order() -> &'static [&'static str] {
13011351
"string_replace_literal",
13021352
"string_lower_ascii",
13031353
"string_split_literal",
1354+
"__map_iter_init",
1355+
"__map_iter_next",
1356+
"__map_iter_take_key",
1357+
"__map_iter_take_value",
1358+
"__map_iter_close",
13041359
]
13051360
}
13061361

@@ -1466,6 +1521,11 @@ fn main_range_builtin_variants(builtin_variant_order: &[String]) -> Vec<String>
14661521
| "StringReplaceLiteral"
14671522
| "StringLowerAscii"
14681523
| "StringSplitLiteral"
1524+
| "MapIterInit"
1525+
| "MapIterNext"
1526+
| "MapIterTakeKey"
1527+
| "MapIterTakeValue"
1528+
| "MapIterClose"
14691529
)
14701530
})
14711531
.cloned()

src/builtins/runtime/core.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,11 +815,66 @@ pub(crate) fn builtin_string_split_literal_impl(
815815
.collect()
816816
}
817817

818+
/// Initialize the hidden iterator used by borrowed map for-in loops.
819+
#[allow(dead_code)]
820+
#[pd_host_function(name = "__map_iter_init")]
821+
fn builtin_map_iter_init_metadata(map: VmMapRef<'_>, _slot: i64) -> VmMapHandle {
822+
Arc::new(map.clone())
823+
}
824+
825+
/// Advance a hidden borrowed-map iterator.
826+
#[allow(dead_code)]
827+
#[pd_host_function(name = "__map_iter_next")]
828+
fn builtin_map_iter_next_metadata(_slot: i64) -> bool {
829+
false
830+
}
831+
832+
/// Move the current iterator key into the loop binding.
833+
#[allow(dead_code)]
834+
#[pd_host_function(name = "__map_iter_take_key")]
835+
fn builtin_map_iter_take_key_metadata(_slot: i64) -> Value {
836+
Value::Null
837+
}
838+
839+
/// Move the current iterator value into the loop binding.
840+
#[allow(dead_code)]
841+
#[pd_host_function(name = "__map_iter_take_value")]
842+
fn builtin_map_iter_take_value_metadata(_slot: i64) -> Value {
843+
Value::Null
844+
}
845+
846+
/// Release a hidden borrowed-map iterator after loop exit.
847+
#[allow(dead_code)]
848+
#[pd_host_function(name = "__map_iter_close")]
849+
fn builtin_map_iter_close_metadata(map: VmMapRef<'_>, _slot: i64) -> VmMapHandle {
850+
Arc::new(map.clone())
851+
}
852+
818853
#[cfg(test)]
819854
mod tests {
820855
use super::*;
856+
use crate::builtins::{BUILTIN_CALL_BASE, BUILTIN_CALL_COUNT, BuiltinFunction};
821857
use std::sync::Arc;
822858

859+
#[test]
860+
fn map_iterator_builtins_have_unique_reserved_call_indices() {
861+
let reserved = [
862+
(BuiltinFunction::MapIterInit, BUILTIN_CALL_BASE - 9),
863+
(BuiltinFunction::MapIterNext, BUILTIN_CALL_BASE - 10),
864+
(BuiltinFunction::MapIterTakeKey, BUILTIN_CALL_BASE - 11),
865+
(BuiltinFunction::MapIterTakeValue, BUILTIN_CALL_BASE - 12),
866+
(BuiltinFunction::MapIterClose, BUILTIN_CALL_BASE - 13),
867+
];
868+
for (builtin, index) in reserved {
869+
assert_eq!(builtin.call_index(), index);
870+
assert_eq!(BuiltinFunction::from_call_index(index), Some(builtin));
871+
}
872+
assert_eq!(BUILTIN_CALL_COUNT, 89);
873+
for alias in BUILTIN_CALL_BASE + 89..=BUILTIN_CALL_BASE + 92 {
874+
assert_eq!(BuiltinFunction::from_call_index(alias), None);
875+
}
876+
}
877+
823878
#[test]
824879
fn array_push_detaches_shared_array_before_write() {
825880
let shared = Value::array(vec![Value::Int(1)]);

src/builtins/runtime/map_iter.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
use crate::bytecode::Value;
2+
use crate::vm::{CallReturn, Vm, VmError, VmResult};
3+
4+
use super::{arg, return_one};
5+
6+
fn iterator_slot(args: &[Value], index: usize, label: &'static str) -> VmResult<usize> {
7+
let raw = arg::<i64>(args, index, label)?;
8+
usize::try_from(raw).map_err(|_| VmError::HostError(format!("invalid map iterator slot {raw}")))
9+
}
10+
11+
pub(super) fn init(vm: &mut Vm, args: &[Value]) -> VmResult<CallReturn> {
12+
let map = match args.first() {
13+
Some(Value::Map(map)) => map.clone(),
14+
_ => return Err(VmError::TypeMismatch("map")),
15+
};
16+
let slot = iterator_slot(args, 1, "map iterator slot")?;
17+
if map.iter().any(|(key, _)| !matches!(key, Value::String(_))) {
18+
return Err(VmError::HostError(
19+
"borrowed map iteration requires string keys".to_string(),
20+
));
21+
}
22+
vm.init_map_iterator(slot, map.clone())?;
23+
Ok(return_one(Value::Map(map)))
24+
}
25+
26+
pub(super) fn next(vm: &mut Vm, args: &[Value]) -> VmResult<CallReturn> {
27+
let slot = iterator_slot(args, 0, "map iterator slot")?;
28+
vm.advance_map_iterator(slot).map(return_one)
29+
}
30+
31+
pub(super) fn take_key(vm: &mut Vm, args: &[Value]) -> VmResult<CallReturn> {
32+
let slot = iterator_slot(args, 0, "map iterator slot")?;
33+
vm.take_map_iterator_key(slot).map(return_one)
34+
}
35+
36+
pub(super) fn take_value(vm: &mut Vm, args: &[Value]) -> VmResult<CallReturn> {
37+
let slot = iterator_slot(args, 0, "map iterator slot")?;
38+
vm.take_map_iterator_value(slot).map(return_one)
39+
}
40+
41+
pub(super) fn close(vm: &mut Vm, args: &[Value]) -> VmResult<CallReturn> {
42+
let map = match args.first() {
43+
Some(Value::Map(map)) => map.clone(),
44+
_ => return Err(VmError::TypeMismatch("map")),
45+
};
46+
let slot = iterator_slot(args, 1, "map iterator slot")?;
47+
vm.close_map_iterator(slot)?;
48+
Ok(return_one(Value::Map(map)))
49+
}
50+
51+
#[cfg(test)]
52+
mod tests {
53+
use super::*;
54+
use crate::{OpCode, Program};
55+
56+
#[test]
57+
fn init_accepts_compaction_independent_ids_and_rejects_oversized_ids() {
58+
let program = Program::new(Vec::new(), vec![OpCode::Ret as u8]).with_local_count(1);
59+
let mut vm = Vm::new(program);
60+
61+
init(&mut vm, &[Value::map(Vec::new()), Value::Int(2)])
62+
.expect("logical iterator ids must not depend on compacted local count");
63+
let err = init(
64+
&mut vm,
65+
&[Value::map(Vec::new()), Value::Int(i64::from(u8::MAX) + 1)],
66+
)
67+
.expect_err("oversized iterator id should fail without allocating");
68+
assert!(err.to_string().contains("invalid map iterator id"));
69+
}
70+
}

src/builtins/runtime/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ mod io;
1515
mod io_wasm;
1616
mod jit;
1717
mod json;
18+
mod map_iter;
1819
mod math;
1920
pub(crate) mod print;
2021
pub(crate) mod regex;
@@ -64,6 +65,15 @@ pub(crate) fn execute_builtin_call(
6465
BuiltinFunction::Set => core::builtin_set(args).map(BuiltinCallOutcome::Return),
6566
BuiltinFunction::Keys => core::builtin_keys(args).map(BuiltinCallOutcome::Return),
6667
BuiltinFunction::Count => core::builtin_count(args).map(BuiltinCallOutcome::Return),
68+
BuiltinFunction::MapIterInit => map_iter::init(vm, args).map(BuiltinCallOutcome::Return),
69+
BuiltinFunction::MapIterNext => map_iter::next(vm, args).map(BuiltinCallOutcome::Return),
70+
BuiltinFunction::MapIterTakeKey => {
71+
map_iter::take_key(vm, args).map(BuiltinCallOutcome::Return)
72+
}
73+
BuiltinFunction::MapIterTakeValue => {
74+
map_iter::take_value(vm, args).map(BuiltinCallOutcome::Return)
75+
}
76+
BuiltinFunction::MapIterClose => map_iter::close(vm, args).map(BuiltinCallOutcome::Return),
6777
BuiltinFunction::StringContains => core::builtin_string_contains(args)
6878
.map(IntoBuiltinCallOutcome::into_builtin_call_outcome),
6979
BuiltinFunction::StringReplaceLiteral => core::builtin_string_replace_literal(args)

src/compiler/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ impl CompileError {
113113
CompileError::StrictTypingRequired { line, .. } => {
114114
line.and_then(|value| usize::try_from(value).ok())
115115
}
116+
116117
_ => None,
117118
}
118119
}

src/compiler/parser/expressions.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,20 @@ impl Parser {
232232
line: u32,
233233
action: &str,
234234
) -> Result<(), ParseError> {
235+
if self.borrowed_map_iter_locals.contains(&index) {
236+
let display = name_hint
237+
.map(str::to_string)
238+
.or_else(|| self.find_local_name_by_slot(index))
239+
.unwrap_or_else(|| format!("#{index}"));
240+
return Err(ParseError {
241+
span: None,
242+
code: Some("E_BORROW_CONFLICT".to_string()),
243+
line: line as usize,
244+
message: format!(
245+
"cannot {action} local '{display}' while it is borrowed by a map iterator"
246+
),
247+
});
248+
}
235249
if !self.enforce_mutable_bindings || self.is_local_slot_mutable(index) {
236250
return Ok(());
237251
}

src/compiler/parser/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ pub(super) struct Parser {
138138
direct_host_call_aliases: HashMap<String, String>,
139139
direct_host_wildcard_imports: HashSet<String>,
140140
mutable_locals: Vec<bool>,
141+
borrowed_map_iter_locals: Vec<LocalSlot>,
142+
local_schemas: HashMap<LocalSlot, TypeSchema>,
141143
}
142144

143145
struct ClosureCaptureContext {
@@ -190,6 +192,8 @@ impl Parser {
190192
direct_host_call_aliases: HashMap::new(),
191193
direct_host_wildcard_imports: HashSet::new(),
192194
mutable_locals: Vec::new(),
195+
borrowed_map_iter_locals: Vec::new(),
196+
local_schemas: HashMap::new(),
193197
})
194198
}
195199

0 commit comments

Comments
 (0)