Skip to content

Commit 30facf3

Browse files
authored
Merge pull request #200 from ryanbreen/feat/breenish-phase5-collections
feat: add Map, Set, do-while fix, and collection builtins
2 parents 9316dfa + 71175f7 commit 30facf3

File tree

6 files changed

+493
-6
lines changed

6 files changed

+493
-6
lines changed

docs/planning/BREENISH_SHELL_PLAN.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@
5757
- Tab completion for commands (PATH scan) and filenames (directory listing)
5858
- Nullish coalescing operator (??) with IsNullish opcode
5959
- Prefix/postfix increment/decrement operators (++/--)
60-
- 162 passing tests, bsh v0.5.0 with full shell builtins
60+
- Map and Set collections with full method support (get/set/has/delete/size/clear/keys/values/forEach)
61+
- do...while loops with continue fix (deferred forward-jump patching)
62+
- 182 passing tests, bsh v0.5.0 with full shell builtins
6163
- **Phase 6**: PLANNED -- Advanced features (class, Proxy, JIT)
6264

6365
## Architecture

libs/breenish-js/src/compiler.rs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ struct LoopContext {
3737
break_jumps: Vec<usize>,
3838
/// The bytecode offset of the loop start (for continue).
3939
continue_target: usize,
40+
/// Offsets of continue jumps that need patching (for do-while where
41+
/// the continue target is not known until after body compilation).
42+
continue_jumps: Vec<usize>,
4043
}
4144

4245
/// Describes how a closure captures a variable.
@@ -389,6 +392,7 @@ impl<'a> Compiler<'a> {
389392
self.loop_stack.push(LoopContext {
390393
break_jumps: Vec::new(),
391394
continue_target: loop_start,
395+
continue_jumps: Vec::new(),
392396
});
393397

394398
self.lexer.expect(&TokenKind::LeftParen)?;
@@ -480,6 +484,7 @@ impl<'a> Compiler<'a> {
480484
self.loop_stack.push(LoopContext {
481485
break_jumps: Vec::new(),
482486
continue_target: increment_offset,
487+
continue_jumps: Vec::new(),
483488
});
484489

485490
self.compile_statement()?;
@@ -579,6 +584,7 @@ impl<'a> Compiler<'a> {
579584
self.loop_stack.push(LoopContext {
580585
break_jumps: Vec::new(),
581586
continue_target: increment_offset,
587+
continue_jumps: Vec::new(),
582588
});
583589

584590
self.compile_statement()?;
@@ -679,6 +685,7 @@ impl<'a> Compiler<'a> {
679685
self.loop_stack.push(LoopContext {
680686
break_jumps: Vec::new(),
681687
continue_target: increment_offset,
688+
continue_jumps: Vec::new(),
682689
});
683690

684691
self.compile_statement()?;
@@ -704,15 +711,19 @@ impl<'a> Compiler<'a> {
704711
let loop_start = self.code.current_offset();
705712
self.loop_stack.push(LoopContext {
706713
break_jumps: Vec::new(),
707-
continue_target: loop_start, // Will be updated
714+
continue_target: usize::MAX, // Not yet known; continue_jumps will be patched
715+
continue_jumps: Vec::new(),
708716
});
709717

710718
self.compile_statement()?;
711719

712-
// continue target is the condition check
720+
// continue target is the condition check; patch deferred continue jumps
713721
let condition_start = self.code.current_offset();
714-
if let Some(ctx) = self.loop_stack.last_mut() {
715-
ctx.continue_target = condition_start;
722+
let continue_patches: Vec<usize> = self.loop_stack.last()
723+
.map(|ctx| ctx.continue_jumps.clone())
724+
.unwrap_or_default();
725+
for cont in continue_patches {
726+
self.code.patch_jump(cont, condition_start);
716727
}
717728

718729
self.lexer.expect(&TokenKind::While)?;
@@ -760,6 +771,7 @@ impl<'a> Compiler<'a> {
760771
self.loop_stack.push(LoopContext {
761772
break_jumps: Vec::new(),
762773
continue_target: 0, // switch doesn't support continue
774+
continue_jumps: Vec::new(),
763775
});
764776

765777
// We need to save and restore the lexer position to do two passes.
@@ -1238,7 +1250,14 @@ impl<'a> Compiler<'a> {
12381250
));
12391251
}
12401252
let target = self.loop_stack.last().unwrap().continue_target;
1241-
self.code.emit_op_u16(Op::Jump, target as u16);
1253+
if target == usize::MAX {
1254+
// Continue target not yet known (do-while); emit a forward jump to be patched
1255+
let jump = self.code.emit_jump(Op::Jump);
1256+
self.loop_stack.last_mut().unwrap().continue_jumps.push(jump);
1257+
} else {
1258+
// Continue target is known (while/for); emit a direct backward jump
1259+
self.code.emit_op_u16(Op::Jump, target as u16);
1260+
}
12421261
self.eat_semicolon();
12431262
Ok(())
12441263
}

libs/breenish-js/src/lib.rs

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -997,6 +997,32 @@ impl Context {
997997
self.vm.set_global_by_name("Number", JsValue::object(number_idx), &mut self.strings);
998998
}
999999

1000+
/// Register built-in Map and Set collection constructors.
1001+
///
1002+
/// This registers `Map()` and `Set()` as global factory functions that
1003+
/// create Map and Set objects respectively.
1004+
pub fn register_collection_builtins(&mut self) {
1005+
use crate::object::{JsObject, ObjectHeap};
1006+
use crate::string::StringPool as SP;
1007+
use crate::value::JsValue;
1008+
use crate::error::JsResult;
1009+
1010+
fn map_constructor(_args: &[JsValue], _strings: &mut SP, heap: &mut ObjectHeap) -> JsResult<JsValue> {
1011+
let obj = JsObject::new_map();
1012+
let idx = heap.alloc(obj);
1013+
Ok(JsValue::object(idx))
1014+
}
1015+
1016+
fn set_constructor(_args: &[JsValue], _strings: &mut SP, heap: &mut ObjectHeap) -> JsResult<JsValue> {
1017+
let obj = JsObject::new_set();
1018+
let idx = heap.alloc(obj);
1019+
Ok(JsValue::object(idx))
1020+
}
1021+
1022+
self.vm.register_native("Map", map_constructor);
1023+
self.vm.register_native("Set", set_constructor);
1024+
}
1025+
10001026
/// Get a mutable reference to the string pool (for native functions).
10011027
pub fn strings_mut(&mut self) -> &mut StringPool {
10021028
&mut self.strings
@@ -2548,4 +2574,176 @@ mod tests {
25482574
"10\n"
25492575
);
25502576
}
2577+
2578+
// --- do...while tests ---
2579+
2580+
#[test]
2581+
fn test_do_while() {
2582+
assert_eq!(
2583+
eval_and_capture("let i = 0; do { i++; } while (i < 5); print(i);"),
2584+
"5\n"
2585+
);
2586+
}
2587+
2588+
#[test]
2589+
fn test_do_while_runs_once() {
2590+
assert_eq!(
2591+
eval_and_capture("let i = 10; do { i++; } while (i < 5); print(i);"),
2592+
"11\n"
2593+
);
2594+
}
2595+
2596+
#[test]
2597+
fn test_do_while_with_break() {
2598+
assert_eq!(
2599+
eval_and_capture("let i = 0; do { i++; if (i === 3) break; } while (i < 10); print(i);"),
2600+
"3\n"
2601+
);
2602+
}
2603+
2604+
#[test]
2605+
fn test_do_while_with_continue() {
2606+
assert_eq!(
2607+
eval_and_capture("let sum = 0; let i = 0; do { i++; if (i % 2 === 0) continue; sum += i; } while (i < 6); print(sum);"),
2608+
"9\n"
2609+
);
2610+
}
2611+
2612+
// --- Map and Set tests ---
2613+
2614+
fn eval_collections(source: &str) -> String {
2615+
let mut ctx = Context::new();
2616+
ctx.set_print_fn(capture_print);
2617+
ctx.register_collection_builtins();
2618+
ctx.eval(source).unwrap();
2619+
take_output()
2620+
}
2621+
2622+
#[test]
2623+
fn test_map_basic() {
2624+
assert_eq!(
2625+
eval_collections("let m = Map(); m.set('a', 1); m.set('b', 2); print(m.get('a'), m.get('b'));"),
2626+
"1 2\n"
2627+
);
2628+
}
2629+
2630+
#[test]
2631+
fn test_map_has_delete() {
2632+
assert_eq!(
2633+
eval_collections("let m = Map(); m.set('x', 42); print(m.has('x')); m.delete('x'); print(m.has('x'));"),
2634+
"true\nfalse\n"
2635+
);
2636+
}
2637+
2638+
#[test]
2639+
fn test_map_size() {
2640+
assert_eq!(
2641+
eval_collections("let m = Map(); m.set('a', 1); m.set('b', 2); print(m.size());"),
2642+
"2\n"
2643+
);
2644+
}
2645+
2646+
#[test]
2647+
fn test_map_overwrite() {
2648+
assert_eq!(
2649+
eval_collections("let m = Map(); m.set('a', 1); m.set('a', 99); print(m.get('a')); print(m.size());"),
2650+
"99\n1\n"
2651+
);
2652+
}
2653+
2654+
#[test]
2655+
fn test_map_get_missing() {
2656+
assert_eq!(
2657+
eval_collections("let m = Map(); print(m.get('missing'));"),
2658+
"undefined\n"
2659+
);
2660+
}
2661+
2662+
#[test]
2663+
fn test_map_clear() {
2664+
assert_eq!(
2665+
eval_collections("let m = Map(); m.set('a', 1); m.set('b', 2); m.clear(); print(m.size());"),
2666+
"0\n"
2667+
);
2668+
}
2669+
2670+
#[test]
2671+
fn test_map_keys_values() {
2672+
assert_eq!(
2673+
eval_collections("let m = Map(); m.set('x', 10); m.set('y', 20); let k = m.keys(); let v = m.values(); print(k[0], k[1]); print(v[0], v[1]);"),
2674+
"x y\n10 20\n"
2675+
);
2676+
}
2677+
2678+
#[test]
2679+
fn test_map_numeric_keys() {
2680+
assert_eq!(
2681+
eval_collections("let m = Map(); m.set(1, 'one'); m.set(2, 'two'); print(m.get(1)); print(m.has(2));"),
2682+
"one\ntrue\n"
2683+
);
2684+
}
2685+
2686+
#[test]
2687+
fn test_map_chaining() {
2688+
assert_eq!(
2689+
eval_collections("let m = Map(); m.set('a', 1).set('b', 2).set('c', 3); print(m.size());"),
2690+
"3\n"
2691+
);
2692+
}
2693+
2694+
#[test]
2695+
fn test_set_basic() {
2696+
assert_eq!(
2697+
eval_collections("let s = Set(); s.add(1); s.add(2); s.add(1); print(s.size()); print(s.has(1)); print(s.has(3));"),
2698+
"2\ntrue\nfalse\n"
2699+
);
2700+
}
2701+
2702+
#[test]
2703+
fn test_set_delete() {
2704+
assert_eq!(
2705+
eval_collections("let s = Set(); s.add(42); s.delete(42); print(s.has(42));"),
2706+
"false\n"
2707+
);
2708+
}
2709+
2710+
#[test]
2711+
fn test_set_clear() {
2712+
assert_eq!(
2713+
eval_collections("let s = Set(); s.add(1); s.add(2); s.add(3); s.clear(); print(s.size());"),
2714+
"0\n"
2715+
);
2716+
}
2717+
2718+
#[test]
2719+
fn test_set_values() {
2720+
assert_eq!(
2721+
eval_collections("let s = Set(); s.add(10); s.add(20); s.add(30); let v = s.values(); print(v[0], v[1], v[2]);"),
2722+
"10 20 30\n"
2723+
);
2724+
}
2725+
2726+
#[test]
2727+
fn test_set_string_values() {
2728+
assert_eq!(
2729+
eval_collections("let s = Set(); s.add('hello'); s.add('world'); s.add('hello'); print(s.size()); print(s.has('hello')); print(s.has('missing'));"),
2730+
"2\ntrue\nfalse\n"
2731+
);
2732+
}
2733+
2734+
#[test]
2735+
fn test_set_chaining() {
2736+
assert_eq!(
2737+
eval_collections("let s = Set(); s.add(1).add(2).add(3); print(s.size());"),
2738+
"3\n"
2739+
);
2740+
}
2741+
2742+
#[test]
2743+
fn test_set_delete_return() {
2744+
assert_eq!(
2745+
eval_collections("let s = Set(); s.add(1); print(s.delete(1)); print(s.delete(1));"),
2746+
"true\nfalse\n"
2747+
);
2748+
}
25512749
}

libs/breenish-js/src/object.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ pub enum ObjectKind {
3737
NativeFunction(u32),
3838
/// A Promise with its fulfillment state.
3939
Promise(PromiseState),
40+
/// A Map object (ordered key-value pairs).
41+
Map(Vec<(JsValue, JsValue)>),
42+
/// A Set object (ordered unique values).
43+
Set(Vec<JsValue>),
4044
}
4145

4246
/// The state of a Promise.
@@ -132,6 +136,28 @@ impl JsObject {
132136
}
133137
}
134138

139+
/// Create a new Map object.
140+
pub fn new_map() -> Self {
141+
Self {
142+
kind: ObjectKind::Map(Vec::new()),
143+
properties: Vec::new(),
144+
elements: Vec::new(),
145+
prototype: None,
146+
marked: false,
147+
}
148+
}
149+
150+
/// Create a new Set object.
151+
pub fn new_set() -> Self {
152+
Self {
153+
kind: ObjectKind::Set(Vec::new()),
154+
properties: Vec::new(),
155+
elements: Vec::new(),
156+
prototype: None,
157+
marked: false,
158+
}
159+
}
160+
135161
/// Create a new native function object.
136162
pub fn new_native_function(native_index: u32) -> Self {
137163
Self {
@@ -267,6 +293,17 @@ impl JsObject {
267293
}
268294
ObjectKind::Promise(PromiseState::Fulfilled(v)) => refs.push(*v),
269295
ObjectKind::Promise(PromiseState::Rejected(v)) => refs.push(*v),
296+
ObjectKind::Map(ref entries) => {
297+
for (k, v) in entries {
298+
refs.push(*k);
299+
refs.push(*v);
300+
}
301+
}
302+
ObjectKind::Set(ref values) => {
303+
for v in values {
304+
refs.push(*v);
305+
}
306+
}
270307
_ => {}
271308
}
272309
refs

0 commit comments

Comments
 (0)