Skip to content

Commit 9316dfa

Browse files
authored
Merge pull request #199 from ryanbreen/feat/breenish-phase5-completion
feat: add tab completion, nullish coalescing, and increment/decrement
2 parents fdc092c + c9a8e29 commit 9316dfa

File tree

6 files changed

+420
-5
lines changed

6 files changed

+420
-5
lines changed

docs/planning/BREENISH_SHELL_PLAN.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@
5454
- Interactive line editing: cursor movement, Home/End, Ctrl+A/E/U/K/W/C/D
5555
- Command history with Up/Down arrow navigation
5656
- Raw mode terminal handling via libbreenix termios
57-
- 149 passing tests, bsh v0.5.0 with full shell builtins
57+
- Tab completion for commands (PATH scan) and filenames (directory listing)
58+
- Nullish coalescing operator (??) with IsNullish opcode
59+
- Prefix/postfix increment/decrement operators (++/--)
60+
- 162 passing tests, bsh v0.5.0 with full shell builtins
5861
- **Phase 6**: PLANNED -- Advanced features (class, Proxy, JIT)
5962

6063
## Architecture

libs/breenish-js/src/bytecode.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,11 @@ pub enum Op {
197197
/// Used by for...in loops to iterate over object keys.
198198
GetKeys = 133,
199199

200+
/// Check if the top of stack is null or undefined.
201+
/// Stack: [value] -> [boolean]
202+
/// Pushes true (1) if null or undefined, false (0) otherwise.
203+
IsNullish = 134,
204+
200205
/// Halt execution.
201206
Halt = 255,
202207
}
@@ -254,6 +259,7 @@ impl Op {
254259
131 => Some(Op::Await),
255260
132 => Some(Op::WrapPromise),
256261
133 => Some(Op::GetKeys),
262+
134 => Some(Op::IsNullish),
257263
255 => Some(Op::Halt),
258264
_ => None,
259265
}

libs/breenish-js/src/compiler.rs

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ pub struct Compiler<'a> {
7878
function_stack: Vec<FunctionContext>,
7979
/// Whether the current function being compiled is async.
8080
is_async: bool,
81+
/// Tracks the last identifier compiled in compile_primary, for postfix ++/--.
82+
last_primary_name: Option<String>,
8183
}
8284

8385
impl<'a> Compiler<'a> {
@@ -96,6 +98,7 @@ impl<'a> Compiler<'a> {
9698
upvalues: Vec::new(),
9799
function_stack: Vec::new(),
98100
is_async: false,
101+
last_primary_name: None,
99102
}
100103
}
101104

@@ -1344,14 +1347,35 @@ impl<'a> Compiler<'a> {
13441347
}
13451348

13461349
fn compile_or(&mut self) -> JsResult<()> {
1347-
self.compile_and()?;
1350+
self.compile_nullish_coalesce()?;
13481351

13491352
while self.lexer.peek().kind == TokenKind::Or {
13501353
self.lexer.next_token();
13511354
// Short-circuit: if truthy, skip RHS
13521355
self.code.emit_op(Op::Dup);
13531356
let skip = self.code.emit_jump(Op::JumpIfTrue);
13541357
self.code.emit_op(Op::Pop);
1358+
self.compile_nullish_coalesce()?;
1359+
let end = self.code.current_offset();
1360+
self.code.patch_jump(skip, end);
1361+
}
1362+
1363+
Ok(())
1364+
}
1365+
1366+
fn compile_nullish_coalesce(&mut self) -> JsResult<()> {
1367+
self.compile_and()?;
1368+
1369+
while self.lexer.peek().kind == TokenKind::NullishCoalesce {
1370+
self.lexer.next_token();
1371+
// Nullish coalescing: if left is NOT nullish, keep it; otherwise use right.
1372+
// Stack: [left]
1373+
// Dup left, check IsNullish, if false (not nullish) jump over right side
1374+
self.code.emit_op(Op::Dup);
1375+
self.code.emit_op(Op::IsNullish);
1376+
let skip = self.code.emit_jump(Op::JumpIfFalse);
1377+
// Left is nullish: pop the duplicated left, compile right
1378+
self.code.emit_op(Op::Pop);
13551379
self.compile_and()?;
13561380
let end = self.code.current_offset();
13571381
self.code.patch_jump(skip, end);
@@ -1556,13 +1580,30 @@ impl<'a> Compiler<'a> {
15561580
fn compile_postfix(&mut self) -> JsResult<()> {
15571581
self.compile_call()?;
15581582

1559-
// Postfix ++ and -- (Phase 1: handle simple identifier case)
1583+
// Postfix ++ and -- for simple identifiers
15601584
if matches!(
15611585
self.lexer.peek().kind,
15621586
TokenKind::PlusPlus | TokenKind::MinusMinus
15631587
) {
1564-
// For Phase 1, just consume and ignore postfix ops on non-identifiers
1565-
self.lexer.next_token();
1588+
let is_increment = self.lexer.peek().kind == TokenKind::PlusPlus;
1589+
self.lexer.next_token(); // consume ++ or --
1590+
1591+
if let Some(name) = self.last_primary_name.take() {
1592+
// Stack currently has: [old_value]
1593+
// For postfix: the expression result is the OLD value.
1594+
// We need to: dup (keep old for expression result), add/sub 1, store back.
1595+
// Stack: [old_value]
1596+
self.code.emit_op(Op::Dup); // [old_value, old_value]
1597+
let one = self.code.add_number(1.0);
1598+
self.code.emit_op_u16(Op::LoadConst, one); // [old_value, old_value, 1]
1599+
if is_increment {
1600+
self.code.emit_op(Op::Add); // [old_value, new_value]
1601+
} else {
1602+
self.code.emit_op(Op::Sub); // [old_value, new_value]
1603+
}
1604+
self.emit_store_var(&name); // [old_value] (new_value stored)
1605+
}
1606+
// If not a simple identifier, postfix op is silently ignored (like before)
15661607
}
15671608

15681609
Ok(())
@@ -1573,6 +1614,13 @@ impl<'a> Compiler<'a> {
15731614

15741615
// Handle postfix operations: calls, property access, indexing
15751616
loop {
1617+
match &self.lexer.peek().kind {
1618+
TokenKind::LeftParen | TokenKind::Dot | TokenKind::LeftBracket => {
1619+
// Any postfix operation invalidates the simple identifier tracking
1620+
self.last_primary_name = None;
1621+
}
1622+
_ => {}
1623+
}
15761624
match &self.lexer.peek().kind {
15771625
TokenKind::LeftParen => {
15781626
self.lexer.next_token(); // consume '('
@@ -1675,6 +1723,7 @@ impl<'a> Compiler<'a> {
16751723
}
16761724

16771725
fn compile_primary(&mut self) -> JsResult<()> {
1726+
self.last_primary_name = None;
16781727
let tok = self.lexer.peek().clone();
16791728
match &tok.kind {
16801729
TokenKind::Number(n) => {
@@ -1782,6 +1831,7 @@ impl<'a> Compiler<'a> {
17821831
let idx = self.code.add_string(name_id.0);
17831832
self.code.emit_op_u16(Op::LoadGlobal, idx);
17841833
}
1834+
self.last_primary_name = Some(name);
17851835
Ok(())
17861836
}
17871837

@@ -2099,6 +2149,19 @@ impl<'a> Compiler<'a> {
20992149
}
21002150
}
21012151

2152+
/// Emit code to store the top of stack into a variable (pops the value).
2153+
fn emit_store_var(&mut self, name: &str) {
2154+
if let Some((slot, _)) = self.resolve_local(name) {
2155+
self.code.emit_op_u16(Op::StoreLocal, slot);
2156+
} else if let Some(upval_idx) = self.resolve_upvalue(name) {
2157+
self.code.emit_op_u16(Op::StoreUpvalue, upval_idx);
2158+
} else {
2159+
let name_id = self.strings.intern(name);
2160+
let idx = self.code.add_string(name_id.0);
2161+
self.code.emit_op_u16(Op::StoreGlobal, idx);
2162+
}
2163+
}
2164+
21022165
/// Emit code to store a value and leave it on the stack (for assignment expressions).
21032166
fn emit_store_and_load_var(&mut self, name: &str) {
21042167
if let Some((slot, _)) = self.resolve_local(name) {

libs/breenish-js/src/lib.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2434,4 +2434,118 @@ mod tests {
24342434
"boolean\nboolean\n"
24352435
);
24362436
}
2437+
2438+
// --- Nullish coalescing tests ---
2439+
2440+
#[test]
2441+
fn test_nullish_coalescing() {
2442+
assert_eq!(
2443+
eval_and_capture("let a = null; print(a ?? 42);"),
2444+
"42\n"
2445+
);
2446+
}
2447+
2448+
#[test]
2449+
fn test_nullish_coalescing_non_null() {
2450+
assert_eq!(
2451+
eval_and_capture("let a = 10; print(a ?? 42);"),
2452+
"10\n"
2453+
);
2454+
}
2455+
2456+
#[test]
2457+
fn test_nullish_undefined() {
2458+
assert_eq!(
2459+
eval_and_capture("let a = undefined; print(a ?? \"default\");"),
2460+
"default\n"
2461+
);
2462+
}
2463+
2464+
#[test]
2465+
fn test_nullish_zero() {
2466+
// 0 is NOT nullish, so ?? should return 0
2467+
assert_eq!(
2468+
eval_and_capture("let a = 0; print(a ?? 42);"),
2469+
"0\n"
2470+
);
2471+
}
2472+
2473+
#[test]
2474+
fn test_nullish_empty_string() {
2475+
// Empty string is NOT nullish, so ?? should return ""
2476+
assert_eq!(
2477+
eval_and_capture("let a = \"\"; print(a ?? \"default\");"),
2478+
"\n"
2479+
);
2480+
}
2481+
2482+
#[test]
2483+
fn test_nullish_false() {
2484+
// false is NOT nullish, so ?? should return false
2485+
assert_eq!(
2486+
eval_and_capture("let a = false; print(a ?? true);"),
2487+
"false\n"
2488+
);
2489+
}
2490+
2491+
#[test]
2492+
fn test_nullish_chained() {
2493+
assert_eq!(
2494+
eval_and_capture("let a = null; let b = null; print(a ?? b ?? 99);"),
2495+
"99\n"
2496+
);
2497+
}
2498+
2499+
// --- Increment/Decrement tests ---
2500+
2501+
#[test]
2502+
fn test_prefix_increment() {
2503+
assert_eq!(
2504+
eval_and_capture("let x = 5; ++x; print(x);"),
2505+
"6\n"
2506+
);
2507+
}
2508+
2509+
#[test]
2510+
fn test_prefix_decrement() {
2511+
assert_eq!(
2512+
eval_and_capture("let x = 5; --x; print(x);"),
2513+
"4\n"
2514+
);
2515+
}
2516+
2517+
#[test]
2518+
fn test_prefix_increment_expression() {
2519+
// ++x returns the NEW value
2520+
assert_eq!(
2521+
eval_and_capture("let x = 5; let y = ++x; print(x, y);"),
2522+
"6 6\n"
2523+
);
2524+
}
2525+
2526+
#[test]
2527+
fn test_postfix_increment() {
2528+
// x++ returns the OLD value, but x is updated
2529+
assert_eq!(
2530+
eval_and_capture("let x = 5; let y = x++; print(x, y);"),
2531+
"6 5\n"
2532+
);
2533+
}
2534+
2535+
#[test]
2536+
fn test_postfix_decrement() {
2537+
// x-- returns the OLD value, but x is updated
2538+
assert_eq!(
2539+
eval_and_capture("let x = 5; let y = x--; print(x, y);"),
2540+
"4 5\n"
2541+
);
2542+
}
2543+
2544+
#[test]
2545+
fn test_increment_in_loop() {
2546+
assert_eq!(
2547+
eval_and_capture("let sum = 0; for (let i = 0; i < 5; i++) { sum += i; } print(sum);"),
2548+
"10\n"
2549+
);
2550+
}
24372551
}

libs/breenish-js/src/vm.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,16 @@ impl Vm {
586586
self.push(val)?;
587587
}
588588

589+
Op::IsNullish => {
590+
let val = self.pop();
591+
let result = if val.is_nullish() {
592+
JsValue::boolean(true)
593+
} else {
594+
JsValue::boolean(false)
595+
};
596+
self.push(result)?;
597+
}
598+
589599
Op::Print => {
590600
let argc = self.read_u8_advance(code) as usize;
591601

0 commit comments

Comments
 (0)