Add CLI builtins: args, env, http_post, http_get, str ops, json_get, cwd; fix block tail expr and wildcard match codegen; add run-file command

This commit is contained in:
Will Anderson
2026-04-27 19:41:33 -05:00
parent 0a36a454f9
commit 46d5650e45
5 changed files with 793 additions and 57 deletions
+84 -45
View File
@@ -65,6 +65,27 @@ impl Codegen {
// ── Statement code generation ─────────────────────────────────────────────
/// Generate a statement in tail position (the last stmt of a block).
/// Expression statements leave their value on the stack instead of popping it.
fn gen_stmt_tail(&mut self, stmt: &Stmt) -> CompileResult<()> {
match stmt {
Stmt::Expr(expr, _) => {
// In tail position, leave the value on the stack.
self.gen_expr(expr)?;
}
Stmt::Return(expr, _) => {
self.gen_expr(expr)?;
self.emit(Bytecode::Return);
}
// All other statement kinds behave the same as non-tail; push Nil as block value.
other => {
self.gen_stmt(other)?;
self.emit(Bytecode::Push(Value::Nil));
}
}
Ok(())
}
fn gen_stmt(&mut self, stmt: &Stmt) -> CompileResult<()> {
// Record the source span for this statement in the source map
if self.emit_source_map {
@@ -187,20 +208,18 @@ impl Codegen {
self.emit(Bytecode::Call { name: fn_name, arity: args.len() as u32 });
}
Expr::Block(stmts) => {
for (i, s) in stmts.iter().enumerate() {
self.gen_stmt(s)?;
// The last expression statement is the block's value
if i == stmts.len() - 1 {
if let Stmt::Expr(_, _) = s {
// Already on stack from gen_stmt (before the Pop)
// We need to not pop it — handled by gen_stmt not
// popping Block results; but we already did Pop.
// Push nil as fallback for empty/void blocks.
}
}
}
if stmts.is_empty() {
self.emit(Bytecode::Push(Value::Nil));
} else {
for (i, s) in stmts.iter().enumerate() {
let is_last = i == stmts.len() - 1;
if is_last {
// Last statement: emit as a tail expression (leave value on stack).
self.gen_stmt_tail(s)?;
} else {
self.gen_stmt(s)?;
}
}
}
}
Expr::If { cond, then, else_ } => {
@@ -225,48 +244,68 @@ impl Codegen {
// compare, branch. A full implementation would use a jump table.
let mut end_jumps = Vec::new();
for arm in arms {
self.emit(Bytecode::Dup);
// Push pattern value
match &arm.pattern {
el_parser::Pattern::Literal(lit) => {
let v = match lit {
Literal::Int(n) => Value::Int(*n),
Literal::Str(s) => Value::Str(s.clone()),
Literal::Bool(b) => Value::Bool(*b),
Literal::Float(f) => Value::Float(*f),
};
self.emit(Bytecode::Push(v));
}
el_parser::Pattern::EnumVariant { variant, payload, .. } => {
// Push the variant name as a string for comparison
self.emit(Bytecode::Push(Value::Str(variant.clone())));
if let Some(bind) = payload {
// Store the payload in a local (simplified: store subject as payload)
self.emit(Bytecode::StoreLocal(bind.clone()));
}
el_parser::Pattern::Wildcard => {
// Wildcard always matches — pop subject and run body directly.
self.emit(Bytecode::Pop);
self.gen_expr(&arm.body)?;
end_jumps.push(self.emit(Bytecode::Jump(0)));
// No jump_no_match needed — wildcard always matches.
// But we still need to patch the end jumps at the end.
// Nothing else to do; break out of the loop since wildcard
// is a catch-all and subsequent arms are unreachable.
break;
}
el_parser::Pattern::Binding(name) => {
// Bind and always match — push duplicate and store
// Bind and always match.
self.emit(Bytecode::Dup);
self.emit(Bytecode::StoreLocal(name.clone()));
// Fall through — will compare to itself (always true)
// Dup'd subject is still on stack; compare to itself.
self.emit(Bytecode::Dup);
self.emit(Bytecode::Eq);
let jump_no_match = self.emit(Bytecode::JumpIfNot(0));
self.emit(Bytecode::Pop);
self.gen_expr(&arm.body)?;
end_jumps.push(self.emit(Bytecode::Jump(0)));
let next_arm = self.current_idx();
self.patch_jump(jump_no_match, next_arm);
}
el_parser::Pattern::Wildcard => {
// Wildcard — push nil (always "matches")
self.emit(Bytecode::Push(Value::Nil));
_ => {
self.emit(Bytecode::Dup);
// Push pattern value
match &arm.pattern {
el_parser::Pattern::Literal(lit) => {
let v = match lit {
Literal::Int(n) => Value::Int(*n),
Literal::Str(s) => Value::Str(s.clone()),
Literal::Bool(b) => Value::Bool(*b),
Literal::Float(f) => Value::Float(*f),
};
self.emit(Bytecode::Push(v));
}
el_parser::Pattern::EnumVariant { variant, payload, .. } => {
// Push the variant name as a string for comparison
self.emit(Bytecode::Push(Value::Str(variant.clone())));
if let Some(bind) = payload {
// Store the subject (simplified: payload = subject)
self.emit(Bytecode::StoreLocal(bind.clone()));
}
}
_ => unreachable!("wildcard and binding handled above"),
}
self.emit(Bytecode::Eq);
let jump_no_match = self.emit(Bytecode::JumpIfNot(0));
// Pop subject from stack
self.emit(Bytecode::Pop);
// Generate arm body
self.gen_expr(&arm.body)?;
end_jumps.push(self.emit(Bytecode::Jump(0)));
let next_arm = self.current_idx();
self.patch_jump(jump_no_match, next_arm);
}
}
self.emit(Bytecode::Eq);
let jump_no_match = self.emit(Bytecode::JumpIfNot(0));
// Pop subject from stack
self.emit(Bytecode::Pop);
// Generate arm body
self.gen_expr(&arm.body)?;
end_jumps.push(self.emit(Bytecode::Jump(0)));
let next_arm = self.current_idx();
self.patch_jump(jump_no_match, next_arm);
}
// Default: pop subject, push nil
// Default fallthrough: pop subject, push nil
self.emit(Bytecode::Pop);
self.emit(Bytecode::Push(Value::Nil));
let end = self.current_idx();