diff --git a/awk/src/compiler.rs b/awk/src/compiler.rs index d6a97807..e5eed68c 100644 --- a/awk/src/compiler.rs +++ b/awk/src/compiler.rs @@ -139,6 +139,11 @@ lazy_static::lazy_static! { min_args: 1, max_args: 1, }), + (Rule::fflush, BuiltinFunctionInfo { + function: BuiltinFunction::FFlush, + min_args: 0, + max_args: 1, + }), (Rule::system, BuiltinFunctionInfo { function: BuiltinFunction::System, min_args: 1, @@ -1142,16 +1147,19 @@ impl Compiler { let stmt = first_child(simple_stmt); let stmt_line_col = stmt.line_col(); match stmt.as_rule() { - Rule::delete_element => { + Rule::array_delete => { let mut inner = stmt.into_inner(); let name = inner.next().unwrap(); let get_instruction = self .get_var(name.as_str(), locals) .map_err(|msg| pest_error_from_span(name.as_span(), msg))?; instructions.push(get_instruction, stmt_line_col); - let index = inner.next().unwrap(); - self.compile_expr(index, instructions, locals)?; - instructions.push(OpCode::Delete, stmt_line_col); + if let Some(index) = inner.next() { + self.compile_expr(index, instructions, locals)?; + instructions.push(OpCode::DeleteElement, stmt_line_col); + } else { + instructions.push(OpCode::ClearArray, stmt_line_col); + } } Rule::expr => { self.compile_expr(stmt, instructions, locals)?; @@ -1451,6 +1459,10 @@ impl Compiler { Rule::ut_for => self.compile_for(stmt, instructions, locals), Rule::ut_foreach => self.compile_for_each(stmt, instructions, locals), Rule::simple_statement => self.compile_simple_statement(stmt, instructions, locals), + Rule::nextfile => { + instructions.push(OpCode::NextFile, stmt.line_col()); + Ok(()) + } Rule::next => { instructions.push(OpCode::Next, stmt.line_col()); Ok(()) @@ -3022,6 +3034,12 @@ mod test { assert_eq!(instructions, vec![OpCode::Next]); } + #[test] + fn test_compile_nextfile() { + let (instructions, _) = compile_stmt("nextfile;"); + assert_eq!(instructions, vec![OpCode::NextFile]); + } + #[test] fn test_compile_exit() { let (instructions, _) = compile_stmt("exit;"); @@ -3047,18 +3065,27 @@ mod test { } #[test] - fn test_compile_delete() { + fn test_compile_delete_element() { let (instructions, _) = compile_stmt("delete a[1];"); assert_eq!( instructions, vec![ OpCode::GetGlobal(FIRST_GLOBAL_VAR), OpCode::PushConstant(0), - OpCode::Delete, + OpCode::DeleteElement, ] ); } + #[test] + fn test_compile_clear_array() { + let (instructions, _) = compile_stmt("delete a"); + assert_eq!( + instructions, + vec![OpCode::GetGlobal(FIRST_GLOBAL_VAR), OpCode::ClearArray] + ); + } + #[test] fn test_compile_simple_print() { let (instructions, constant) = compile_stmt("print 1;"); @@ -3885,6 +3912,8 @@ mod test { r#" BEGIN { close("file"); + fflush("file"); + fflush(); system("ls"); } "#, @@ -3899,6 +3928,17 @@ mod test { }, OpCode::Pop, OpCode::PushConstant(1), + OpCode::CallBuiltin { + function: BuiltinFunction::FFlush, + argc: 1 + }, + OpCode::Pop, + OpCode::CallBuiltin { + function: BuiltinFunction::FFlush, + argc: 0 + }, + OpCode::Pop, + OpCode::PushConstant(2), OpCode::CallBuiltin { function: BuiltinFunction::System, argc: 1, diff --git a/awk/src/grammar.pest b/awk/src/grammar.pest index 5534f93d..7af988f2 100644 --- a/awk/src/grammar.pest +++ b/awk/src/grammar.pest @@ -46,6 +46,7 @@ builtin_func = _{ | tolower | toupper | close + | fflush | system } @@ -69,6 +70,7 @@ substr = { "substr" } tolower = { "tolower" } toupper = { "toupper" } close = { "close" } +fflush = { "fflush" } system = { "system" } keyword = { @@ -161,17 +163,19 @@ ut_for = { "for" ~ "(" ~ simple_statement? ~ ";" ~ expr? ~ ";" ~ simple_stat ut_foreach = { "for" ~ "(" ~ name ~ in_op ~ name ~ ")" ~ opt_newline ~ unterminated_statement } terminatable_statement = _{ - simple_statement + nextfile | next | break_stmt | continue_stmt | exit_stmt | return_stmt | do_while + | simple_statement } do_while = { "do" ~ opt_newline ~ terminated_statement ~ "while" ~ "(" ~ expr ~ ")" } next = { "next" } +nextfile = { "nextfile" } break_stmt = { "break" } continue_stmt = { "continue" } @@ -179,13 +183,13 @@ return_stmt = { "return" ~ expr? } exit_stmt = { "exit" ~ expr? } simple_statement = { - delete_element + array_delete | print_stmt | expr } -delete_element = { - "delete" ~ name ~ "[" ~ expr ~ "]" +array_delete = { + "delete" ~ name ~ ("[" ~ expr ~ "]")? } print_stmt = { diff --git a/awk/src/interpreter/io.rs b/awk/src/interpreter/io.rs index 57f94c95..c7da3d66 100644 --- a/awk/src/interpreter/io.rs +++ b/awk/src/interpreter/io.rs @@ -224,6 +224,22 @@ impl WriteFiles { Ok(()) } + pub fn flush_file(&mut self, filename: &str) -> bool { + if let Some(file) = self.files.get_mut(filename) { + file.flush().is_ok() + } else { + false + } + } + + pub fn flush_all(&mut self) -> bool { + let mut success = true; + for file in self.files.values_mut() { + success = success && file.flush().is_ok(); + } + success + } + pub fn close_file(&mut self, filename: &str) { self.files.remove(filename); } @@ -288,6 +304,22 @@ impl WritePipes { Ok(()) } + pub fn flush_file(&mut self, filename: &str) -> bool { + if let Some(file) = self.pipes.get(filename) { + unsafe { libc::fflush(*file) == 0 } + } else { + false + } + } + + pub fn flush_all(&mut self) -> bool { + let mut success = true; + for file in self.pipes.values() { + success = success && unsafe { libc::fflush(*file) == 0 }; + } + success + } + pub fn close_pipe(&mut self, filename: &str) { if let Some(file) = self.pipes.remove(filename) { unsafe { diff --git a/awk/src/interpreter/mod.rs b/awk/src/interpreter/mod.rs index 407da891..ea5bd2d6 100644 --- a/awk/src/interpreter/mod.rs +++ b/awk/src/interpreter/mod.rs @@ -351,10 +351,16 @@ fn call_simple_builtin( stack.push_value(index)?; } BuiltinFunction::Length => { - let value = stack - .pop_scalar_value()? - .scalar_to_string(&global_env.convfmt)?; - stack.push_value(value.len() as f64)?; + let value = stack.pop_value(); + match &value.value { + AwkValueVariant::Array(array) => { + stack.push_value(array.len() as f64)?; + } + _ => { + let value_str = value.scalar_to_string(&global_env.convfmt)?; + stack.push_value(value_str.len() as f64)?; + } + } } BuiltinFunction::Split => { let separator = if argc == 2 { @@ -1171,6 +1177,7 @@ impl<'i, 's> Stack<'i, 's> { enum ExecutionResult { Expression(AwkValue), Next, + NextFile, Exit(i32), } @@ -1215,7 +1222,11 @@ macro_rules! compare_op { $stack.push_value(bool_to_f64(lhs $op rhs))?; } (AwkValueVariant::String(lhs), AwkValueVariant::String(rhs)) => { - $stack.push_value(bool_to_f64(lhs.as_str() $op rhs.as_str()))?; + if lhs.is_numeric && rhs.is_numeric { + $stack.push_value(bool_to_f64(strtod(lhs) $op strtod(rhs)))?; + } else { + $stack.push_value(bool_to_f64(lhs.as_str() $op rhs.as_str()))?; + } } (AwkValueVariant::Number(lhs), AwkValueVariant::UninitializedScalar) => { $stack.push_value(bool_to_f64(*lhs $op 0.0))?; @@ -1520,13 +1531,17 @@ impl Interpreter { fields_state = lvalue.assign(value.clone(), global_env)?; stack.push_value(value)?; } - OpCode::Delete => { + OpCode::DeleteElement => { let key = stack .pop_scalar_value()? .scalar_to_string(&global_env.convfmt)?; let array = stack.pop_ref().as_array()?; array.delete(&key); } + OpCode::ClearArray => { + let array = stack.pop_ref().as_array()?; + array.clear(); + } OpCode::JumpIfFalse(offset) => { let condition = stack.pop_scalar_value()?.scalar_as_bool(); if !condition { @@ -1601,6 +1616,22 @@ impl Interpreter { self.write_pipes.close_pipe(&filename); self.read_pipes.close_pipe(&filename); } + BuiltinFunction::FFlush => { + let expr_str = if argc == 1 { + stack + .pop_scalar_value()? + .scalar_to_string(&global_env.convfmt)? + } else { + AwkString::default() + }; + let result = if expr_str.is_empty() { + self.write_files.flush_file(&expr_str) + && self.write_pipes.flush_file(&expr_str) + } else { + self.write_files.flush_all() && self.write_pipes.flush_all() + }; + stack.push_value(bool_to_f64(result))?; + } BuiltinFunction::GetLine => { let var = stack.pop_ref(); if let Some(next_record) = current_file.read_next_record(&global_env.rs)? { @@ -1689,6 +1720,7 @@ impl Interpreter { stack.pop(); } OpCode::Next => return Ok(ExecutionResult::Next), + OpCode::NextFile => return Ok(ExecutionResult::NextFile), OpCode::Exit => { let exit_code = stack.pop_scalar_value()?.scalar_as_f64(); return Ok(ExecutionResult::Exit(exit_code as i32)); @@ -1936,7 +1968,7 @@ pub fn interpret( input_read = true; global_env.fnr = 1; - while let Some(record) = reader.read_next_record(&global_env.rs)? { + 'record_loop: while let Some(record) = reader.read_next_record(&global_env.rs)? { current_record.reset(record, &global_env.fs)?; interpreter.globals[SpecialVar::Nf as usize].get_mut().value = AwkValue::from(current_record.get_last_field() as f64).value; @@ -2003,6 +2035,7 @@ pub fn interpret( )?; match rule_result { ExecutionResult::Next => break, + ExecutionResult::NextFile => break 'record_loop, ExecutionResult::Exit(val) => { return_value = val; break 'file_loop; @@ -2441,7 +2474,7 @@ mod tests { OpCode::Assign, OpCode::GetGlobal(FIRST_GLOBAL_VAR), OpCode::PushConstant(0), - OpCode::Delete, + OpCode::DeleteElement, ]; let constant = vec![Constant::from("key"), Constant::Number(123.0)]; assert_eq!(test_global(instructions, constant), Array::default().into()); @@ -2459,7 +2492,7 @@ mod tests { OpCode::GetGlobal(FIRST_GLOBAL_VAR), OpCode::GetLocal(0), OpCode::PushConstant(0), - OpCode::Delete, + OpCode::DeleteElement, ]; let constant = vec![Constant::from("key"), Constant::Number(123.0)]; assert_eq!(test_global(instructions, constant), Array::default().into()); @@ -2470,12 +2503,48 @@ mod tests { let instructions = vec![ OpCode::GetGlobal(FIRST_GLOBAL_VAR), OpCode::PushConstant(0), - OpCode::Delete, + OpCode::DeleteElement, ]; let constant = vec![Constant::from("key")]; assert_eq!(test_global(instructions, constant), Array::default().into()); } + #[test] + fn test_clear_array() { + let instructions = vec![ + OpCode::GetGlobal(FIRST_GLOBAL_VAR), + OpCode::PushConstant(0), + OpCode::IndexArrayGetRef, + OpCode::PushZero, + OpCode::Assign, + OpCode::Pop, + OpCode::GetGlobal(FIRST_GLOBAL_VAR), + OpCode::PushConstant(1), + OpCode::IndexArrayGetRef, + OpCode::PushZero, + OpCode::Assign, + OpCode::Pop, + OpCode::GetGlobal(FIRST_GLOBAL_VAR), + OpCode::PushConstant(2), + OpCode::IndexArrayGetRef, + OpCode::PushZero, + OpCode::Assign, + OpCode::Pop, + OpCode::GetGlobal(FIRST_GLOBAL_VAR), + OpCode::ClearArray, + ]; + let constants = vec![ + Constant::from("key1"), + Constant::from("key2"), + Constant::from("key3"), + ]; + let result = Test::new(instructions, constants).run_correct(); + assert_eq!( + result.globals[FIRST_GLOBAL_VAR as usize], + Array::default().into() + ); + } + #[test] fn test_assign_to_local_var() { let instructions = vec![ @@ -2750,6 +2819,38 @@ mod tests { .execution_result .unwrap_expr(); assert_eq!(value, AwkValue::from(11.0)); + + let instructions = vec![ + OpCode::GetGlobal(FIRST_GLOBAL_VAR), + OpCode::PushConstant(0), + OpCode::IndexArrayGetRef, + OpCode::PushZero, + OpCode::Assign, + OpCode::Pop, + OpCode::GetGlobal(FIRST_GLOBAL_VAR), + OpCode::PushConstant(1), + OpCode::IndexArrayGetRef, + OpCode::PushZero, + OpCode::Assign, + OpCode::Pop, + OpCode::GetGlobal(FIRST_GLOBAL_VAR), + OpCode::PushConstant(2), + OpCode::IndexArrayGetRef, + OpCode::PushZero, + OpCode::Assign, + OpCode::Pop, + OpCode::GetGlobal(FIRST_GLOBAL_VAR), + OpCode::CallBuiltin { + function: BuiltinFunction::Length, + argc: 1, + }, + ]; + let constants = vec![ + Constant::from("key1"), + Constant::from("key2"), + Constant::from("key3"), + ]; + assert_eq!(interpret_expr(instructions, constants), AwkValue::from(3.0)); } #[test] diff --git a/awk/src/program.rs b/awk/src/program.rs index 997f1e81..f841a54c 100644 --- a/awk/src/program.rs +++ b/awk/src/program.rs @@ -72,7 +72,9 @@ pub enum OpCode { Assign, // deletes the key on top of the stack from the array preceding it - Delete, + DeleteElement, + // clears the array on top of the stack + ClearArray, // jump forwards or backwards by the given offset. // Offset 0 is the jump instruction @@ -98,6 +100,7 @@ pub enum OpCode { PushUninitializedScalar, Next, + NextFile, Exit, Return, @@ -306,6 +309,7 @@ pub enum BuiltinFunction { // I/O functions Close, + FFlush, GetLine, GetLineFromFile, GetLineFromPipe, diff --git a/awk/tests/awk/builtin_string_functions.awk b/awk/tests/awk/builtin_string_functions.awk index 3b82a534..2c9704cf 100644 --- a/awk/tests/awk/builtin_string_functions.awk +++ b/awk/tests/awk/builtin_string_functions.awk @@ -1,6 +1,10 @@ BEGIN { print index("hello", "l"); - print length("hello"); + print length("hello"); + a[0] = 0; + a[1] = 1; + a[2] = 2; + print length(a); print match("hello", /l+/); print RSTART; print RLENGTH; diff --git a/awk/tests/awk/builtin_string_functions.out b/awk/tests/awk/builtin_string_functions.out index 0540a199..6464ce9c 100644 --- a/awk/tests/awk/builtin_string_functions.out +++ b/awk/tests/awk/builtin_string_functions.out @@ -2,6 +2,7 @@ 5 3 3 +3 2 hello test diff --git a/awk/tests/awk/delete.awk b/awk/tests/awk/delete.awk index 20a2d7d8..82bb8247 100644 --- a/awk/tests/awk/delete.awk +++ b/awk/tests/awk/delete.awk @@ -6,4 +6,18 @@ BEGIN { delete a["test"]; print a["test"]; print a["other"]; -} \ No newline at end of file + + b[0] = 1; + b[1] = 2; + b[2] = 3; + b[3] = 4; + print b[0]; + print b[1]; + print b[2]; + print b[3]; + delete b; + print b[0]; + print b[1]; + print b[2]; + print b[3]; +} diff --git a/awk/tests/awk/delete.out b/awk/tests/awk/delete.out index 5c54e483..fa6590e0 100644 --- a/awk/tests/awk/delete.out +++ b/awk/tests/awk/delete.out @@ -2,3 +2,11 @@ test other other +1 +2 +3 +4 + + + + diff --git a/awk/tests/awk/nextfile.awk b/awk/tests/awk/nextfile.awk new file mode 100644 index 00000000..ce589c83 --- /dev/null +++ b/awk/tests/awk/nextfile.awk @@ -0,0 +1,3 @@ +$1 == 3 { nextfile } +{ print $0 } +END { print "end" } diff --git a/awk/tests/awk/nextfile.out b/awk/tests/awk/nextfile.out new file mode 100644 index 00000000..1ce45870 --- /dev/null +++ b/awk/tests/awk/nextfile.out @@ -0,0 +1,5 @@ +1 Jane janitor 30 +2 Smith sailor 45 +1 1 1 +2 2 2 +end diff --git a/awk/tests/integration.rs b/awk/tests/integration.rs index b0d47c20..0c4f5bf3 100644 --- a/awk/tests/integration.rs +++ b/awk/tests/integration.rs @@ -301,6 +301,15 @@ fn test_awk_next() { test_awk!(next, "tests/awk/test_data.txt"); } +#[test] +fn test_awk_nextfile() { + test_awk!( + nextfile, + "tests/awk/test_data.txt", + "tests/awk/test_data2.txt" + ); +} + #[test] fn test_awk_exit() { test_awk!(exit, "tests/awk/test_data.txt");