diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a4fcf6f --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# Python cache +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python + +# Test outputs +*.tmp diff --git a/demo.fawk b/demo.fawk new file mode 100644 index 0000000..05b3ce8 --- /dev/null +++ b/demo.fawk @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 fawk.py +# FAWK Demo - Showcasing all major features + +# Define some helper functions +function double(x) { return x * 2 } +function square(x) { return x * x } + +# Note: map, filter, reduce are built-in functions +# We use them directly without defining them + +BEGIN { + print "===================================" + print "FAWK Demo - Functional AWK" + print "===================================" + print "" + + # 1. First-class arrays + print "1. First-class arrays:" + numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + print " Numbers:", numbers + + # 2. First-class functions + print "" + print "2. First-class functions:" + print " double(21) =", double(21) + + # 3. Anonymous functions (lambdas) + print "" + print "3. Anonymous functions:" + cube = (x) => { x * x * x } + print " cube(3) =", cube(3) + + # 4. Higher-order functions + print "" + print "4. Higher-order functions:" + doubled = map(double, [1, 2, 3, 4, 5]) + print " map(double, [1,2,3,4,5]) =", doubled + + evens = filter((n) => { n % 2 == 0 }, [1, 2, 3, 4, 5, 6]) + print " filter(even, [1,2,3,4,5,6]) =", evens + + # 5. Pipeline operator + print "" + print "5. Pipeline operator:" + result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + |> filter((n) => { n % 2 == 0 }) + |> map(square) + |> reduce((a, b) => { a + b }, 0) + print " Sum of squares of evens:", result + + # 6. Associative arrays + print "" + print "6. Associative arrays:" + scores = ["Alice" => 95, "Bob" => 87, "Carol" => 92] + print " Scores:", scores + high_scorers = scores |> filter((s) => { s >= 90 }) + print " High scorers (>=90):", high_scorers + + # 7. Nested arrays and complex operations + print "" + print "7. Nested arrays:" + matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] + print " Matrix:" + for (i in matrix) { + row = matrix[i] + print " ", row[0], row[1], row[2] + } + + print "" + print "===================================" + print "Demo complete!" + print "===================================" +} diff --git a/fawk b/fawk new file mode 100755 index 0000000..04a1dff --- /dev/null +++ b/fawk @@ -0,0 +1,9 @@ +#!/bin/bash +# FAWK Wrapper Script +# Runs the Python FAWK interpreter + +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Run the Python interpreter +exec python3 "$SCRIPT_DIR/fawk.py" "$@" diff --git a/fawk.py b/fawk.py new file mode 100755 index 0000000..c86d511 --- /dev/null +++ b/fawk.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +""" +FAWK - Functional AWK Interpreter +A functional AWK dialect with first-class functions and arrays. +""" + +import sys +from fawk_lexer import Lexer +from fawk_parser import Parser +from fawk_interpreter import Interpreter + + +def main(): + if len(sys.argv) < 2: + print("Usage: fawk [input_file]", file=sys.stderr) + sys.exit(1) + + script_file = sys.argv[1] + + # Read source code + try: + with open(script_file, 'r') as f: + source = f.read() + except FileNotFoundError: + print(f"Error: Script file '{script_file}' not found", file=sys.stderr) + sys.exit(1) + except IOError as e: + print(f"Error reading script file: {e}", file=sys.stderr) + sys.exit(1) + + # Tokenize + try: + lexer = Lexer(source) + tokens = lexer.tokenize() + except SyntaxError as e: + print(f"Lexer error: {e}", file=sys.stderr) + sys.exit(1) + + # Parse + try: + parser = Parser(tokens) + program = parser.parse() + except SyntaxError as e: + print(f"Parser error: {e}", file=sys.stderr) + sys.exit(1) + + # Interpret + # Prepare ARGC and ARGV (mimicking AWK behavior) + argc = len(sys.argv) + argv = sys.argv # [fawk.py, script.fawk, input_file, ...] + interpreter = Interpreter(argc, argv) + + # Read input if provided + input_lines = [] + input_file = None + if len(sys.argv) > 2: + input_file = sys.argv[2] + interpreter.FILENAME = input_file + try: + with open(input_file, 'r') as f: + input_lines = f.readlines() + except FileNotFoundError: + print(f"Error: Input file '{input_file}' not found", file=sys.stderr) + sys.exit(1) + except IOError as e: + print(f"Error reading input file: {e}", file=sys.stderr) + sys.exit(1) + + # Run + try: + interpreter.run(program, input_lines) + except RuntimeError as e: + print(f"Runtime error: {e}", file=sys.stderr) + sys.exit(1) + except KeyboardInterrupt: + print("\nInterrupted", file=sys.stderr) + sys.exit(130) + + +if __name__ == '__main__': + main() diff --git a/fawk_ast.py b/fawk_ast.py new file mode 100644 index 0000000..47ec221 --- /dev/null +++ b/fawk_ast.py @@ -0,0 +1,167 @@ +""" +FAWK AST (Abstract Syntax Tree) +Defines all AST node classes +""" + +from dataclasses import dataclass +from typing import List, Optional, Any + + +@dataclass +class ASTNode: + pass + + +@dataclass +class Program(ASTNode): + functions: List['FunctionDef'] + begin_block: Optional['Block'] + patterns: List['PatternAction'] + end_block: Optional['Block'] + + +@dataclass +class FunctionDef(ASTNode): + name: str + params: List[str] + body: 'Block' + + +@dataclass +class PatternAction(ASTNode): + pattern: Optional[ASTNode] + action: 'Block' + + +@dataclass +class Block(ASTNode): + statements: List[ASTNode] + + +@dataclass +class GlobalDecl(ASTNode): + names: List[str] + + +@dataclass +class IfStmt(ASTNode): + condition: ASTNode + then_block: 'Block' + else_block: Optional['Block'] + + +@dataclass +class ForInStmt(ASTNode): + var: str + iterable: ASTNode + body: 'Block' + + +@dataclass +class WhileStmt(ASTNode): + condition: ASTNode + body: 'Block' + + +@dataclass +class ReturnStmt(ASTNode): + value: Optional[ASTNode] + + +@dataclass +class BreakStmt(ASTNode): + pass + + +@dataclass +class ContinueStmt(ASTNode): + pass + + +@dataclass +class PrintStmt(ASTNode): + args: List[ASTNode] + + +@dataclass +class ExprStmt(ASTNode): + expr: ASTNode + + +@dataclass +class BinaryOp(ASTNode): + op: str + left: ASTNode + right: ASTNode + + +@dataclass +class UnaryOp(ASTNode): + op: str + operand: ASTNode + + +@dataclass +class Assignment(ASTNode): + target: ASTNode + value: ASTNode + + +@dataclass +class ArrayLiteral(ASTNode): + elements: List[ASTNode] + + +@dataclass +class AssocArray(ASTNode): + pairs: List[tuple] # [(key_expr, value_expr), ...] + + +@dataclass +class ArrayAccess(ASTNode): + array: ASTNode + index: ASTNode + + +@dataclass +class FunctionCall(ASTNode): + func: ASTNode + args: List[ASTNode] + + +@dataclass +class Lambda(ASTNode): + params: List[str] + body: 'Block' + + +@dataclass +class Pipeline(ASTNode): + left: ASTNode + right: ASTNode + + +@dataclass +class Identifier(ASTNode): + name: str + + +@dataclass +class Number(ASTNode): + value: float + + +@dataclass +class String(ASTNode): + value: str + + +@dataclass +class Regex(ASTNode): + pattern: str + flags: str + + +@dataclass +class FieldAccess(ASTNode): + index: ASTNode diff --git a/fawk_interpreter.py b/fawk_interpreter.py new file mode 100644 index 0000000..92f91d4 --- /dev/null +++ b/fawk_interpreter.py @@ -0,0 +1,691 @@ +""" +FAWK Interpreter +Executes the Abstract Syntax Tree +""" + +import re +from typing import Any, List, Callable +from fawk_ast import * + + +class BreakException(Exception): + pass + + +class ContinueException(Exception): + pass + + +class ReturnException(Exception): + def __init__(self, value): + self.value = value + + +class Environment: + def __init__(self, parent=None): + self.parent = parent + self.vars = {} + + def get(self, name: str): + if name in self.vars: + return self.vars[name] + elif self.parent: + return self.parent.get(name) + else: + return 0 # AWK default for undefined variables + + def set(self, name: str, value: Any): + self.vars[name] = value + + def set_local(self, name: str, value: Any): + self.vars[name] = value + + def has(self, name: str) -> bool: + if name in self.vars: + return True + elif self.parent: + return self.parent.has(name) + return False + + +class FawkArray: + """Represents a FAWK array (can be regular or associative)""" + def __init__(self): + self.data = {} + + def get(self, key): + # Convert key to appropriate type + if isinstance(key, (int, float)): + key = int(key) + else: + key = str(key) + return self.data.get(key, 0) + + def set(self, key, value): + if isinstance(key, (int, float)): + key = int(key) + else: + key = str(key) + self.data[key] = value + + def keys(self): + return list(self.data.keys()) + + def values(self): + return list(self.data.values()) + + def length(self): + return len(self.data) + + def copy(self): + """Create a deep copy of this array""" + new_arr = FawkArray() + for key, value in self.data.items(): + if isinstance(value, FawkArray): + new_arr.data[key] = value.copy() + else: + new_arr.data[key] = value + return new_arr + + def __repr__(self): + # Display as array-like for regular indices, dict-like for assoc + if all(isinstance(k, int) for k in self.data.keys()): + # Try to display as regular array + if not self.data: + return "[]" + max_idx = max(self.data.keys()) + if all(i in self.data for i in range(max_idx + 1)): + return "[" + ", ".join(str(self.data[i]) for i in range(max_idx + 1)) + "]" + + # Display as associative array + items = [f"{k} => {v}" for k, v in self.data.items()] + return "[" + ", ".join(items) + "]" + + +class UserFunction: + def __init__(self, params: List[str], body: Block, closure_env: Environment): + self.params = params + self.body = body + self.closure_env = closure_env + + +class Interpreter: + def __init__(self, argc=0, argv=None): + self.global_env = Environment() + self.current_env = self.global_env + self.functions = {} + self.globals_declared = set() + + # AWK built-in variables + self.ARGC = argc + self.ARGV = FawkArray() + if argv: + for i, arg in enumerate(argv): + self.ARGV.set(i, arg) + + self.CONVFMT = "%.6g" + + # ENVIRON - environment variables + import os + self.ENVIRON = FawkArray() + for key, value in os.environ.items(): + self.ENVIRON.set(key, value) + + self.FILENAME = "" + self.FNR = 0 # File number of records + self.FS = " " # Field separator + self.NF = 0 # Number of fields + self.NR = 0 # Number of records + self.OFMT = "%.6g" + self.OFS = " " # Output field separator + self.ORS = "\n" # Output record separator + self.RLENGTH = -1 # Length of string matched by match() + self.RS = "\n" # Record separator + self.RSTART = 0 # Start of string matched by match() + self.SUBSEP = "\034" # Subscript separator + + self.fields = [] # Current line fields + + # Built-in functions - single source of truth + self.builtin_functions = { + 'length': lambda arr: arr.length() if isinstance(arr, FawkArray) else len(str(arr)), + 'map': self.builtin_map, + 'filter': self.builtin_filter, + 'reduce': self.builtin_reduce, + 'sum_array': self.builtin_sum_array, + 'match': self.builtin_match, + 'split': self.builtin_split, + } + + # Register built-in functions + self.register_builtins() + + def register_builtins(self): + """Register all built-in functions""" + for name, func in self.builtin_functions.items(): + self.functions[name] = func + + def builtin_map(self, func, arr): + if not isinstance(arr, FawkArray): + raise RuntimeError("map requires an array") + + result = FawkArray() + for key in arr.keys(): + value = arr.get(key) + result.set(key, self.call_function(func, [value])) + return result + + def builtin_filter(self, pred, arr): + if not isinstance(arr, FawkArray): + raise RuntimeError("filter requires an array") + + result = FawkArray() + for key in arr.keys(): + value = arr.get(key) + if self.is_truthy(self.call_function(pred, [value])): + result.set(key, value) + return result + + def builtin_reduce(self, func, initial, arr): + if not isinstance(arr, FawkArray): + raise RuntimeError("reduce requires an array") + + acc = initial + for key in arr.keys(): + value = arr.get(key) + acc = self.call_function(func, [acc, value]) + return acc + + def builtin_sum_array(self, arr): + if not isinstance(arr, FawkArray): + return 0 + total = 0 + for key in arr.keys(): + value = arr.get(key) + total += value if isinstance(value, (int, float)) else 0 + return total + + def builtin_match(self, pattern, text): + """Match a regex pattern and return array with full match and groups""" + text_str = str(text) + match = re.search(pattern, text_str) + + result = FawkArray() + if match: + # Set RSTART and RLENGTH + self.RSTART = match.start() + 1 # AWK uses 1-based indexing + self.RLENGTH = len(match.group(0)) + + # Index 0: full match + result.set(0, match.group(0)) + # Index 1+: captured groups + for i, group in enumerate(match.groups(), 1): + result.set(i, group if group is not None else "") + else: + # No match + self.RSTART = 0 + self.RLENGTH = -1 + + return result + + def builtin_split(self, separator, text): + """Split text by separator and return array""" + text_str = str(text) + sep_str = str(separator) + + parts = text_str.split(sep_str) + + result = FawkArray() + for i, part in enumerate(parts): + result.set(i, part) + + return result + + def error(self, msg: str): + raise RuntimeError(f"Runtime error: {msg}") + + def is_truthy(self, value) -> bool: + if isinstance(value, bool): + return value + elif isinstance(value, (int, float)): + return value != 0 + elif isinstance(value, str): + return value != "" + elif isinstance(value, FawkArray): + return value.length() > 0 + elif value is None: + return False + return True + + def to_number(self, value): + """Convert value to number (like AWK does)""" + if isinstance(value, (int, float)): + return value + elif isinstance(value, str): + # Try to parse as number + try: + if '.' in value: + return float(value) + else: + return int(value) + except (ValueError, AttributeError): + return 0 # AWK default for non-numeric strings + return 0 + + def eval(self, node: ASTNode) -> Any: + method_name = f'eval_{node.__class__.__name__}' + method = getattr(self, method_name, None) + if method: + return method(node) + else: + self.error(f"No eval method for {node.__class__.__name__}") + + def eval_Program(self, node: Program) -> None: + # Program evaluation is handled by run() method + pass + + def eval_Block(self, node: Block) -> Any: + result = None + for stmt in node.statements: + result = self.eval(stmt) + return result + + def eval_GlobalDecl(self, node: GlobalDecl) -> None: + for name in node.names: + self.globals_declared.add(name) + if name not in self.global_env.vars: + self.global_env.set(name, 0) + + def eval_IfStmt(self, node: IfStmt) -> Any: + condition = self.eval(node.condition) + if self.is_truthy(condition): + return self.eval(node.then_block) + elif node.else_block: + return self.eval(node.else_block) + return None + + def eval_ForInStmt(self, node: ForInStmt) -> None: + iterable = self.eval(node.iterable) + + if not isinstance(iterable, FawkArray): + self.error("for-in requires an array") + + for key in iterable.keys(): + self.current_env.set_local(node.var, key) + try: + self.eval(node.body) + except BreakException: + break + except ContinueException: + continue + + def eval_WhileStmt(self, node: WhileStmt) -> None: + while self.is_truthy(self.eval(node.condition)): + try: + self.eval(node.body) + except BreakException: + break + except ContinueException: + continue + + def eval_ReturnStmt(self, node: ReturnStmt) -> None: + value = self.eval(node.value) if node.value else None + raise ReturnException(value) + + def eval_BreakStmt(self, node: BreakStmt) -> None: + raise BreakException() + + def eval_ContinueStmt(self, node: ContinueStmt) -> None: + raise ContinueException() + + def eval_PrintStmt(self, node: PrintStmt) -> None: + if not node.args: + print(end=self.ORS) + else: + values = [self.value_to_string(self.eval(arg)) for arg in node.args] + print(self.OFS.join(values), end=self.ORS) + + def value_to_string(self, value) -> str: + if isinstance(value, FawkArray): + return str(value) + elif isinstance(value, bool): + return "1" if value else "0" + elif isinstance(value, float): + # Format floats nicely + if value == int(value): + return str(int(value)) + return str(value) + elif value is None: + return "" + return str(value) + + def eval_ExprStmt(self, node: ExprStmt) -> Any: + return self.eval(node.expr) + + def eval_BinaryOp(self, node: BinaryOp) -> Any: + left = self.eval(node.left) + right = self.eval(node.right) + + op = node.op + + # Arithmetic operations - convert to numbers + if op == '+': + return self.to_number(left) + self.to_number(right) + elif op == '-': + return self.to_number(left) - self.to_number(right) + elif op == '*': + return self.to_number(left) * self.to_number(right) + elif op == '/': + right_num = self.to_number(right) + if right_num == 0: + self.error("Division by zero") + return self.to_number(left) / right_num + elif op == '%': + return self.to_number(left) % self.to_number(right) + # Comparison operations - use as-is for now + elif op == '==': + return left == right + elif op == '!=': + return left != right + elif op == '<': + return self.to_number(left) < self.to_number(right) + elif op == '<=': + return self.to_number(left) <= self.to_number(right) + elif op == '>': + return self.to_number(left) > self.to_number(right) + elif op == '>=': + return self.to_number(left) >= self.to_number(right) + # Logical operations + elif op == '&&': + return self.is_truthy(left) and self.is_truthy(right) + elif op == '||': + return self.is_truthy(left) or self.is_truthy(right) + else: + self.error(f"Unknown binary operator: {op}") + + def eval_UnaryOp(self, node: UnaryOp) -> Any: + operand = self.eval(node.operand) + + if node.op == '-': + return -self.to_number(operand) + elif node.op == '!': + return not self.is_truthy(operand) + else: + self.error(f"Unknown unary operator: {node.op}") + + def eval_Assignment(self, node: Assignment) -> Any: + value = self.eval(node.value) + + if isinstance(node.target, Identifier): + name = node.target.name + + # Check if it's a built-in variable + if name == 'FS': + self.FS = str(value) + elif name == 'OFS': + self.OFS = str(value) + elif name == 'ORS': + self.ORS = str(value) + elif name == 'RS': + self.RS = str(value) + elif name == 'OFMT': + self.OFMT = str(value) + elif name == 'CONVFMT': + self.CONVFMT = str(value) + elif name == 'SUBSEP': + self.SUBSEP = str(value) + elif name == 'FILENAME': + self.FILENAME = str(value) + # Check if it's a global + elif name in self.globals_declared: + self.global_env.set(name, value) + else: + self.current_env.set_local(name, value) + + elif isinstance(node.target, ArrayAccess): + array = self.eval(node.target.array) + if not isinstance(array, FawkArray): + # Auto-vivify array + array = FawkArray() + if isinstance(node.target.array, Identifier): + name = node.target.array.name + if name in self.globals_declared: + self.global_env.set(name, array) + else: + self.current_env.set_local(name, array) + + index = self.eval(node.target.index) + array.set(index, value) + + else: + self.error("Invalid assignment target") + + return value + + def eval_ArrayLiteral(self, node: ArrayLiteral) -> FawkArray: + arr = FawkArray() + for i, elem in enumerate(node.elements): + arr.set(i, self.eval(elem)) + return arr + + def eval_AssocArray(self, node: AssocArray) -> FawkArray: + arr = FawkArray() + for key_expr, value_expr in node.pairs: + key = self.eval(key_expr) + value = self.eval(value_expr) + arr.set(key, value) + return arr + + def eval_ArrayAccess(self, node: ArrayAccess) -> Any: + array = self.eval(node.array) + if not isinstance(array, FawkArray): + return 0 # AWK behavior + + index = self.eval(node.index) + return array.get(index) + + def eval_FunctionCall(self, node: FunctionCall) -> Any: + func = self.eval(node.func) + args = [self.eval(arg) for arg in node.args] + + return self.call_function(func, args) + + def call_function(self, func, args): + if callable(func) and not isinstance(func, UserFunction): + # Built-in function + return func(*args) + elif isinstance(func, UserFunction): + # User-defined function + if len(args) != len(func.params): + self.error(f"Function expects {len(func.params)} arguments, got {len(args)}") + + # Create new environment for function + func_env = Environment(func.closure_env) + for param, arg in zip(func.params, args): + # Copy arrays when passing as arguments (pass by value) + if isinstance(arg, FawkArray): + func_env.set_local(param, arg.copy()) + else: + func_env.set_local(param, arg) + + # Execute function body + saved_env = self.current_env + self.current_env = func_env + + try: + result = self.eval(func.body) + # For lambdas with single expression, implicitly return the value + if isinstance(func.body, Block) and len(func.body.statements) == 1: + stmt = func.body.statements[0] + if isinstance(stmt, ExprStmt): + # Implicit return for single-expression lambdas + result = self.eval(stmt.expr) + except ReturnException as e: + result = e.value + finally: + self.current_env = saved_env + + return result + else: + self.error(f"Not a function: {func}") + + def eval_Lambda(self, node: Lambda) -> UserFunction: + return UserFunction(node.params, node.body, self.current_env) + + def eval_Pipeline(self, node: Pipeline) -> Any: + left_value = self.eval(node.left) + + # The right side should be a function call + # We append the left value as the last argument + if isinstance(node.right, FunctionCall): + func = self.eval(node.right.func) + args = [self.eval(arg) for arg in node.right.args] + args.append(left_value) # Add piped value as last argument + return self.call_function(func, args) + else: + self.error("Pipeline right side must be a function call") + + def eval_Identifier(self, node: Identifier) -> Any: + name = node.name + + # Check for built-in variables + if name == 'ARGC': + return self.ARGC + elif name == 'ARGV': + return self.ARGV + elif name == 'CONVFMT': + return self.CONVFMT + elif name == 'ENVIRON': + return self.ENVIRON + elif name == 'FILENAME': + return self.FILENAME + elif name == 'FNR': + return self.FNR + elif name == 'FS': + return self.FS + elif name == 'NF': + return self.NF + elif name == 'NR': + return self.NR + elif name == 'OFMT': + return self.OFMT + elif name == 'OFS': + return self.OFS + elif name == 'ORS': + return self.ORS + elif name == 'RLENGTH': + return self.RLENGTH + elif name == 'RS': + return self.RS + elif name == 'RSTART': + return self.RSTART + elif name == 'SUBSEP': + return self.SUBSEP + + # Check for functions + if name in self.functions: + return self.functions[name] + + # Check for variables + return self.current_env.get(name) + + def eval_Number(self, node: Number) -> float: + return node.value + + def eval_String(self, node: String) -> str: + return node.value + + def eval_Regex(self, node: Regex) -> bool: + """Evaluate regex pattern against current line ($0)""" + line = " ".join(self.fields) if self.fields else "" + flags = 0 + if 'i' in node.flags: + flags |= re.IGNORECASE + try: + return bool(re.search(node.pattern, line, flags)) + except re.error as e: + self.error(f"Invalid regex pattern: {e}") + + def eval_FieldAccess(self, node: FieldAccess) -> Any: + index = self.eval(node.index) + index = int(index) + + if index == 0: + return " ".join(self.fields) + elif 1 <= index <= len(self.fields): + return self.fields[index - 1] + else: + return "" + + def run(self, program: Program, input_lines: List[str] = None): + # Register user-defined functions (protect built-ins) + for func_def in program.functions: + if func_def.name in self.builtin_functions: + raise RuntimeError(f"Cannot redefine built-in function '{func_def.name}'") + self.functions[func_def.name] = UserFunction( + func_def.params, func_def.body, self.global_env + ) + + # Execute BEGIN block with its own local environment + if program.begin_block: + begin_env = Environment(self.global_env) + saved_env = self.current_env + self.current_env = begin_env + try: + self.eval(program.begin_block) + finally: + self.current_env = saved_env + + # Process input lines with pattern-action blocks + if input_lines: + for line in input_lines: + self.NR += 1 + self.FNR += 1 + # Split line into fields using FS + line = line.rstrip('\n') + if self.FS == " ": + # Special case: space means any whitespace + self.fields = line.split() + else: + self.fields = line.split(self.FS) + self.NF = len(self.fields) + + # Execute pattern-action blocks + for pattern_action in program.patterns: + # Check if pattern matches (or no pattern) + should_execute = False + if pattern_action.pattern is None: + should_execute = True + else: + # Evaluate pattern + should_execute = self.is_truthy(self.eval(pattern_action.pattern)) + + if should_execute: + action_env = Environment(self.global_env) + saved_env = self.current_env + self.current_env = action_env + try: + self.eval(pattern_action.action) + finally: + self.current_env = saved_env + else: + # No input, just execute pattern-less actions + for pattern_action in program.patterns: + if pattern_action.pattern is None: + action_env = Environment(self.global_env) + saved_env = self.current_env + self.current_env = action_env + try: + self.eval(pattern_action.action) + finally: + self.current_env = saved_env + + # Execute END block with its own local environment + if program.end_block: + end_env = Environment(self.global_env) + saved_env = self.current_env + self.current_env = end_env + try: + self.eval(program.end_block) + finally: + self.current_env = saved_env diff --git a/fawk_lexer.py b/fawk_lexer.py new file mode 100644 index 0000000..42e4cb3 --- /dev/null +++ b/fawk_lexer.py @@ -0,0 +1,358 @@ +""" +FAWK Lexer +Tokenizes FAWK source code +""" + +from enum import Enum, auto +from dataclasses import dataclass +from typing import Any, List, Optional + + +class TokenType(Enum): + # Literals + NUMBER = auto() + STRING = auto() + IDENTIFIER = auto() + + # Keywords + FUNCTION = auto() + RETURN = auto() + IF = auto() + ELSE = auto() + FOR = auto() + IN = auto() + WHILE = auto() + BREAK = auto() + CONTINUE = auto() + PRINT = auto() + BEGIN = auto() + END = auto() + GLOBAL = auto() + + # Operators + PLUS = auto() + MINUS = auto() + MULTIPLY = auto() + DIVIDE = auto() + MODULO = auto() + ASSIGN = auto() + EQ = auto() + NE = auto() + LT = auto() + LE = auto() + GT = auto() + GE = auto() + AND = auto() + OR = auto() + NOT = auto() + ARROW = auto() # => + PIPELINE = auto() # |> + ASSOC_ARROW = auto() # => (for associative arrays) + + # Delimiters + LPAREN = auto() + RPAREN = auto() + LBRACE = auto() + RBRACE = auto() + LBRACKET = auto() + RBRACKET = auto() + COMMA = auto() + SEMICOLON = auto() + DOLLAR = auto() + + # Special + REGEX = auto() + NEWLINE = auto() + EOF = auto() + + +@dataclass +class Token: + type: TokenType + value: Any + line: int + column: int + + +class Lexer: + def __init__(self, text: str): + self.text = text + self.pos = 0 + self.line = 1 + self.column = 1 + self.tokens = [] + + self.keywords = { + 'function': TokenType.FUNCTION, + 'return': TokenType.RETURN, + 'if': TokenType.IF, + 'else': TokenType.ELSE, + 'for': TokenType.FOR, + 'in': TokenType.IN, + 'while': TokenType.WHILE, + 'break': TokenType.BREAK, + 'continue': TokenType.CONTINUE, + 'print': TokenType.PRINT, + 'BEGIN': TokenType.BEGIN, + 'END': TokenType.END, + 'global': TokenType.GLOBAL, + } + + def error(self, msg: str): + raise SyntaxError(f"Lexer error at line {self.line}, column {self.column}: {msg}") + + def peek(self, offset: int = 0) -> Optional[str]: + pos = self.pos + offset + if pos < len(self.text): + return self.text[pos] + return None + + def advance(self) -> Optional[str]: + if self.pos < len(self.text): + char = self.text[self.pos] + self.pos += 1 + if char == '\n': + self.line += 1 + self.column = 1 + else: + self.column += 1 + return char + return None + + def skip_whitespace(self): + while self.peek() and self.peek() in ' \t\r': + self.advance() + + def skip_comment(self): + if self.peek() == '#': + while self.peek() and self.peek() != '\n': + self.advance() + + def read_number(self) -> Token: + start_line = self.line + start_col = self.column + num_str = '' + + while self.peek() and (self.peek().isdigit() or self.peek() == '.'): + num_str += self.advance() + + value = float(num_str) if '.' in num_str else int(num_str) + return Token(TokenType.NUMBER, value, start_line, start_col) + + def read_string(self) -> Token: + start_line = self.line + start_col = self.column + quote = self.advance() # consume opening quote + string = '' + + while self.peek() and self.peek() != quote: + if self.peek() == '\\': + self.advance() + next_char = self.peek() + if next_char == 'n': + string += '\n' + self.advance() + elif next_char == 't': + string += '\t' + self.advance() + elif next_char == '\\': + string += '\\' + self.advance() + elif next_char == quote: + string += quote + self.advance() + else: + # Preserve backslash for unknown escapes (e.g., \$ or \. for regex) + string += '\\' + string += next_char + self.advance() + else: + string += self.advance() + + if self.peek() != quote: + self.error("Unterminated string") + + self.advance() # consume closing quote + return Token(TokenType.STRING, string, start_line, start_col) + + def read_identifier(self) -> Token: + start_line = self.line + start_col = self.column + ident = '' + + while self.peek() and (self.peek().isalnum() or self.peek() == '_'): + ident += self.advance() + + token_type = self.keywords.get(ident, TokenType.IDENTIFIER) + return Token(token_type, ident, start_line, start_col) + + def read_regex(self) -> Token: + start_line = self.line + start_col = self.column + self.advance() # consume opening / + pattern = '' + + while self.peek() and self.peek() != '/': + if self.peek() == '\\': + pattern += self.advance() + if self.peek(): + pattern += self.advance() + else: + pattern += self.advance() + + if self.peek() != '/': + self.error("Unterminated regex") + + self.advance() # consume closing / + + # Check for flags (i for case-insensitive) + flags = '' + while self.peek() and self.peek() in 'igm': + flags += self.advance() + + return Token(TokenType.REGEX, (pattern, flags), start_line, start_col) + + def tokenize(self) -> List[Token]: + while self.pos < len(self.text): + self.skip_whitespace() + + if not self.peek(): + break + + # Skip comments + if self.peek() == '#': + self.skip_comment() + continue + + # Newline + if self.peek() == '\n': + token = Token(TokenType.NEWLINE, '\n', self.line, self.column) + self.tokens.append(token) + self.advance() + continue + + # Numbers + if self.peek().isdigit(): + self.tokens.append(self.read_number()) + continue + + # Strings + if self.peek() in '"\'': + self.tokens.append(self.read_string()) + continue + + # Identifiers and keywords + if self.peek().isalpha() or self.peek() == '_': + self.tokens.append(self.read_identifier()) + continue + + # Regex patterns (only at top level after brace/newline at statement position) + # We need to be careful - regex only appears in pattern position, not in expressions + if self.peek() == '/': + # Check if this could be a regex based on context + # Regex appears after: NEWLINE, RBRACE, or at start + if len(self.tokens) == 0: + self.tokens.append(self.read_regex()) + continue + + # Look backwards past newlines to find meaningful token + i = len(self.tokens) - 1 + while i >= 0 and self.tokens[i].type == TokenType.NEWLINE: + i -= 1 + + if i < 0: + # Only newlines before this + self.tokens.append(self.read_regex()) + continue + + last_meaningful = self.tokens[i] + # Regex can appear after RBRACE (end of block) or BEGIN/END/FUNCTION + if last_meaningful.type in [TokenType.RBRACE, TokenType.BEGIN, TokenType.END, TokenType.FUNCTION]: + self.tokens.append(self.read_regex()) + continue + + # Two-character operators + start_line = self.line + start_col = self.column + + if self.peek() == '=' and self.peek(1) == '>': + self.advance() + self.advance() + self.tokens.append(Token(TokenType.ARROW, '=>', start_line, start_col)) + continue + + if self.peek() == '|' and self.peek(1) == '>': + self.advance() + self.advance() + self.tokens.append(Token(TokenType.PIPELINE, '|>', start_line, start_col)) + continue + + if self.peek() == '=' and self.peek(1) == '=': + self.advance() + self.advance() + self.tokens.append(Token(TokenType.EQ, '==', start_line, start_col)) + continue + + if self.peek() == '!' and self.peek(1) == '=': + self.advance() + self.advance() + self.tokens.append(Token(TokenType.NE, '!=', start_line, start_col)) + continue + + if self.peek() == '<' and self.peek(1) == '=': + self.advance() + self.advance() + self.tokens.append(Token(TokenType.LE, '<=', start_line, start_col)) + continue + + if self.peek() == '>' and self.peek(1) == '=': + self.advance() + self.advance() + self.tokens.append(Token(TokenType.GE, '>=', start_line, start_col)) + continue + + if self.peek() == '&' and self.peek(1) == '&': + self.advance() + self.advance() + self.tokens.append(Token(TokenType.AND, '&&', start_line, start_col)) + continue + + if self.peek() == '|' and self.peek(1) == '|': + self.advance() + self.advance() + self.tokens.append(Token(TokenType.OR, '||', start_line, start_col)) + continue + + # Single-character tokens + char = self.peek() + single_char_tokens = { + '+': TokenType.PLUS, + '-': TokenType.MINUS, + '*': TokenType.MULTIPLY, + '/': TokenType.DIVIDE, + '%': TokenType.MODULO, + '=': TokenType.ASSIGN, + '<': TokenType.LT, + '>': TokenType.GT, + '!': TokenType.NOT, + '(': TokenType.LPAREN, + ')': TokenType.RPAREN, + '{': TokenType.LBRACE, + '}': TokenType.RBRACE, + '[': TokenType.LBRACKET, + ']': TokenType.RBRACKET, + ',': TokenType.COMMA, + ';': TokenType.SEMICOLON, + '$': TokenType.DOLLAR, + } + + if char in single_char_tokens: + token = Token(single_char_tokens[char], char, start_line, start_col) + self.tokens.append(token) + self.advance() + continue + + self.error(f"Unexpected character: {char}") + + self.tokens.append(Token(TokenType.EOF, None, self.line, self.column)) + return self.tokens diff --git a/fawk_parser.py b/fawk_parser.py new file mode 100644 index 0000000..0e88e6a --- /dev/null +++ b/fawk_parser.py @@ -0,0 +1,487 @@ +""" +FAWK Parser +Parses tokens into an Abstract Syntax Tree +""" + +from typing import List, Optional +from fawk_lexer import Token, TokenType +from fawk_ast import * + + +class Parser: + def __init__(self, tokens: List[Token]): + self.tokens = tokens + self.pos = 0 + + def error(self, msg: str): + token = self.current() + raise SyntaxError(f"Parser error at line {token.line}, column {token.column}: {msg}") + + def current(self) -> Token: + if self.pos < len(self.tokens): + return self.tokens[self.pos] + return self.tokens[-1] # EOF + + def peek(self, offset: int = 0) -> Token: + pos = self.pos + offset + if pos < len(self.tokens): + return self.tokens[pos] + return self.tokens[-1] # EOF + + def advance(self) -> Token: + token = self.current() + if token.type != TokenType.EOF: + self.pos += 1 + return token + + def expect(self, token_type: TokenType) -> Token: + token = self.current() + if token.type != token_type: + self.error(f"Expected {token_type}, got {token.type}") + return self.advance() + + def skip_newlines(self): + while self.current().type == TokenType.NEWLINE: + self.advance() + + def parse(self) -> Program: + functions = [] + begin_block = None + patterns = [] + end_block = None + + self.skip_newlines() + + while self.current().type != TokenType.EOF: + self.skip_newlines() + + if self.current().type == TokenType.FUNCTION: + functions.append(self.parse_function_def()) + elif self.current().type == TokenType.BEGIN: + self.advance() + begin_block = self.parse_block() + elif self.current().type == TokenType.END: + self.advance() + end_block = self.parse_block() + elif self.current().type == TokenType.LBRACE: + # Pattern-action with no pattern + action = self.parse_block() + patterns.append(PatternAction(None, action)) + elif self.current().type == TokenType.REGEX: + # Regex pattern with action + token = self.advance() + pattern_node = Regex(token.value[0], token.value[1]) + action = self.parse_block() + patterns.append(PatternAction(pattern_node, action)) + else: + # Could be a pattern-action + # For simplicity, treat remaining blocks as pattern-less actions + break + + self.skip_newlines() + + return Program(functions, begin_block, patterns, end_block) + + def parse_function_def(self) -> FunctionDef: + self.expect(TokenType.FUNCTION) + name = self.expect(TokenType.IDENTIFIER).value + + self.expect(TokenType.LPAREN) + params = [] + + if self.current().type != TokenType.RPAREN: + params.append(self.expect(TokenType.IDENTIFIER).value) + while self.current().type == TokenType.COMMA: + self.advance() + params.append(self.expect(TokenType.IDENTIFIER).value) + + self.expect(TokenType.RPAREN) + body = self.parse_block() + + return FunctionDef(name, params, body) + + def parse_block(self) -> Block: + self.expect(TokenType.LBRACE) + self.skip_newlines() + + statements = [] + while self.current().type != TokenType.RBRACE: + stmt = self.parse_statement() + if stmt: + statements.append(stmt) + self.skip_newlines() + + self.expect(TokenType.RBRACE) + return Block(statements) + + def parse_statement(self) -> Optional[ASTNode]: + self.skip_newlines() + + token = self.current() + + if token.type == TokenType.GLOBAL: + return self.parse_global_decl() + elif token.type == TokenType.IF: + return self.parse_if_stmt() + elif token.type == TokenType.FOR: + return self.parse_for_stmt() + elif token.type == TokenType.WHILE: + return self.parse_while_stmt() + elif token.type == TokenType.RETURN: + return self.parse_return_stmt() + elif token.type == TokenType.BREAK: + self.advance() + self.skip_statement_terminator() + return BreakStmt() + elif token.type == TokenType.CONTINUE: + self.advance() + self.skip_statement_terminator() + return ContinueStmt() + elif token.type == TokenType.PRINT: + return self.parse_print_stmt() + elif token.type == TokenType.LBRACE: + return self.parse_block() + elif token.type in [TokenType.NEWLINE, TokenType.SEMICOLON]: + self.advance() + return None + else: + return self.parse_expr_stmt() + + def parse_global_decl(self) -> GlobalDecl: + self.expect(TokenType.GLOBAL) + names = [self.expect(TokenType.IDENTIFIER).value] + + while self.current().type == TokenType.COMMA: + self.advance() + names.append(self.expect(TokenType.IDENTIFIER).value) + + self.skip_statement_terminator() + return GlobalDecl(names) + + def parse_if_stmt(self) -> IfStmt: + self.expect(TokenType.IF) + self.expect(TokenType.LPAREN) + condition = self.parse_expression() + self.expect(TokenType.RPAREN) + + then_block = self.parse_block() + else_block = None + + if self.current().type == TokenType.ELSE: + self.advance() + else_block = self.parse_block() + + return IfStmt(condition, then_block, else_block) + + def parse_for_stmt(self) -> ForInStmt: + self.expect(TokenType.FOR) + self.expect(TokenType.LPAREN) + var = self.expect(TokenType.IDENTIFIER).value + self.expect(TokenType.IN) + iterable = self.parse_expression() + self.expect(TokenType.RPAREN) + body = self.parse_block() + + return ForInStmt(var, iterable, body) + + def parse_while_stmt(self) -> WhileStmt: + self.expect(TokenType.WHILE) + self.expect(TokenType.LPAREN) + condition = self.parse_expression() + self.expect(TokenType.RPAREN) + body = self.parse_block() + + return WhileStmt(condition, body) + + def parse_return_stmt(self) -> ReturnStmt: + self.expect(TokenType.RETURN) + + value = None + if self.current().type not in [TokenType.NEWLINE, TokenType.SEMICOLON, TokenType.RBRACE]: + value = self.parse_expression() + + self.skip_statement_terminator() + return ReturnStmt(value) + + def parse_print_stmt(self) -> PrintStmt: + self.expect(TokenType.PRINT) + args = [] + + if self.current().type not in [TokenType.NEWLINE, TokenType.SEMICOLON, TokenType.RBRACE]: + args.append(self.parse_expression()) + while self.current().type == TokenType.COMMA: + self.advance() + args.append(self.parse_expression()) + + self.skip_statement_terminator() + return PrintStmt(args) + + def parse_expr_stmt(self) -> ExprStmt: + expr = self.parse_expression() + self.skip_statement_terminator() + return ExprStmt(expr) + + def skip_statement_terminator(self): + while self.current().type in [TokenType.NEWLINE, TokenType.SEMICOLON]: + self.advance() + + def parse_expression(self) -> ASTNode: + return self.parse_pipeline() + + def parse_pipeline(self) -> ASTNode: + left = self.parse_assignment() + + # Skip newlines before pipeline operator to allow multi-line pipelines + self.skip_newlines() + + while self.current().type == TokenType.PIPELINE: + self.advance() + self.skip_newlines() # Skip newlines after pipeline operator too + right = self.parse_assignment() + left = Pipeline(left, right) + self.skip_newlines() # Check for more pipeline operators + + return left + + def parse_assignment(self) -> ASTNode: + expr = self.parse_or() + + if self.current().type == TokenType.ASSIGN: + self.advance() + value = self.parse_expression() + return Assignment(expr, value) + + return expr + + def parse_or(self) -> ASTNode: + left = self.parse_and() + + while self.current().type == TokenType.OR: + op = self.advance().value + right = self.parse_and() + left = BinaryOp(op, left, right) + + return left + + def parse_and(self) -> ASTNode: + left = self.parse_equality() + + while self.current().type == TokenType.AND: + op = self.advance().value + right = self.parse_equality() + left = BinaryOp(op, left, right) + + return left + + def parse_equality(self) -> ASTNode: + left = self.parse_comparison() + + while self.current().type in [TokenType.EQ, TokenType.NE]: + op = self.advance().value + right = self.parse_comparison() + left = BinaryOp(op, left, right) + + return left + + def parse_comparison(self) -> ASTNode: + left = self.parse_additive() + + while self.current().type in [TokenType.LT, TokenType.LE, TokenType.GT, TokenType.GE]: + op = self.advance().value + right = self.parse_additive() + left = BinaryOp(op, left, right) + + return left + + def parse_additive(self) -> ASTNode: + left = self.parse_multiplicative() + + while self.current().type in [TokenType.PLUS, TokenType.MINUS]: + op = self.advance().value + right = self.parse_multiplicative() + left = BinaryOp(op, left, right) + + return left + + def parse_multiplicative(self) -> ASTNode: + left = self.parse_unary() + + while self.current().type in [TokenType.MULTIPLY, TokenType.DIVIDE, TokenType.MODULO]: + op = self.advance().value + right = self.parse_unary() + left = BinaryOp(op, left, right) + + return left + + def parse_unary(self) -> ASTNode: + if self.current().type in [TokenType.NOT, TokenType.MINUS]: + op = self.advance().value + operand = self.parse_unary() + return UnaryOp(op, operand) + + return self.parse_postfix() + + def parse_postfix(self) -> ASTNode: + expr = self.parse_primary() + + while True: + if self.current().type == TokenType.LPAREN: + # Function call + self.advance() + args = [] + + if self.current().type != TokenType.RPAREN: + args.append(self.parse_expression()) + while self.current().type == TokenType.COMMA: + self.advance() + args.append(self.parse_expression()) + + self.expect(TokenType.RPAREN) + expr = FunctionCall(expr, args) + + elif self.current().type == TokenType.LBRACKET: + # Array access + self.advance() + index = self.parse_expression() + self.expect(TokenType.RBRACKET) + expr = ArrayAccess(expr, index) + + else: + break + + return expr + + def parse_primary(self) -> ASTNode: + token = self.current() + + if token.type == TokenType.NUMBER: + self.advance() + return Number(token.value) + + elif token.type == TokenType.STRING: + self.advance() + return String(token.value) + + elif token.type == TokenType.IDENTIFIER: + self.advance() + return Identifier(token.value) + + elif token.type == TokenType.DOLLAR: + self.advance() + index = self.parse_unary() + return FieldAccess(index) + + elif token.type == TokenType.LPAREN: + # Could be grouped expression or lambda + if self.is_lambda(): + return self.parse_lambda() + else: + self.advance() + expr = self.parse_expression() + self.expect(TokenType.RPAREN) + return expr + + elif token.type == TokenType.LBRACKET: + return self.parse_array_literal() + + else: + self.error(f"Unexpected token: {token.type}") + + def is_lambda(self) -> bool: + # Look ahead to see if this is a lambda + saved_pos = self.pos + + try: + if self.current().type != TokenType.LPAREN: + return False + + self.advance() + + # Empty params + if self.current().type == TokenType.RPAREN: + self.advance() + result = self.current().type == TokenType.ARROW + self.pos = saved_pos + return result + + # Check for parameter list + if self.current().type != TokenType.IDENTIFIER: + self.pos = saved_pos + return False + + self.advance() + + while self.current().type == TokenType.COMMA: + self.advance() + if self.current().type != TokenType.IDENTIFIER: + self.pos = saved_pos + return False + self.advance() + + if self.current().type != TokenType.RPAREN: + self.pos = saved_pos + return False + + self.advance() + result = self.current().type == TokenType.ARROW + self.pos = saved_pos + return result + + except: + self.pos = saved_pos + return False + + def parse_lambda(self) -> Lambda: + self.expect(TokenType.LPAREN) + params = [] + + if self.current().type != TokenType.RPAREN: + params.append(self.expect(TokenType.IDENTIFIER).value) + while self.current().type == TokenType.COMMA: + self.advance() + params.append(self.expect(TokenType.IDENTIFIER).value) + + self.expect(TokenType.RPAREN) + self.expect(TokenType.ARROW) + body = self.parse_block() + + return Lambda(params, body) + + def parse_array_literal(self) -> ASTNode: + self.expect(TokenType.LBRACKET) + elements = [] + pairs = [] + is_assoc = False + + if self.current().type != TokenType.RBRACKET: + first_expr = self.parse_expression() + + # Check if it's an associative array + if self.current().type == TokenType.ARROW: + is_assoc = True + self.advance() + value_expr = self.parse_expression() + pairs.append((first_expr, value_expr)) + + while self.current().type == TokenType.COMMA: + self.advance() + if self.current().type == TokenType.RBRACKET: + break + key_expr = self.parse_expression() + self.expect(TokenType.ARROW) + value_expr = self.parse_expression() + pairs.append((key_expr, value_expr)) + else: + elements.append(first_expr) + while self.current().type == TokenType.COMMA: + self.advance() + if self.current().type == TokenType.RBRACKET: + break + elements.append(self.parse_expression()) + + self.expect(TokenType.RBRACKET) + + if is_assoc: + return AssocArray(pairs) + else: + return ArrayLiteral(elements) diff --git a/sales.csv b/sales.csv new file mode 100644 index 0000000..62ac236 --- /dev/null +++ b/sales.csv @@ -0,0 +1,12 @@ +category,product,price +electronics,laptop,1200 +electronics,phone,800 +electronics,tablet,450 +books,novel,15 +books,textbook,85 +books,magazine,8 +clothing,shirt,35 +clothing,pants,60 +clothing,jacket,120 +electronics,headphones,150 +books,cookbook,28 diff --git a/test10_builtin_functions.expected b/test10_builtin_functions.expected new file mode 100644 index 0000000..dade129 --- /dev/null +++ b/test10_builtin_functions.expected @@ -0,0 +1,69 @@ +Test 1: length() function +-------------------------------------- + length("Hello World"): 11 + length([1,2,3,4,5]): 5 + +Test 2: map() function +-------------------------------------- + Original: [1, 2, 3, 4, 5] + Doubled: [2, 4, 6, 8, 10] + +Test 3: filter() function +-------------------------------------- + Original: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + Evens only: [1 => 2, 3 => 4, 5 => 6, 7 => 8, 9 => 10] + +Test 4: reduce() function +-------------------------------------- + Array: [1, 2, 3, 4, 5] + Sum: 15 + Product: 120 + +Test 5: match() function +-------------------------------------- + Text: Contact: alice@example.com + Match: alice@example.com + User: alice + Domain: example.com + +Test 6: split() function +-------------------------------------- + Original: apple:banana:cherry:date + Split by ':': [apple, banana, cherry, date] + Count: 4 + +Test 7: sum_array() function +-------------------------------------- + Array: [10, 20, 30, 40, 50] + Sum: 150 + +Test 8: Pipeline with built-ins +-------------------------------------- + Pipeline: [1..10] |> filter(>3) |> map(square) |> reduce(sum) + Result: 371 + +Test 9: Custom implementations work +-------------------------------------- + (Built-in functions cannot be redefined) + (But we can create our own with different names) + +Test 10: Custom map/filter/reduce +-------------------------------------- + my_map(cube, [1,2,3,4,5]): [1, 8, 27, 64, 125] + my_filter(odd, [1,2,3,4,5]): [0 => 1, 2 => 3, 4 => 5] + my_reduce(sum, 0, [1,2,3,4,5]): 15 + +Test 11: Cannot redefine built-ins +-------------------------------------- + Built-in functions are protected: + - length, map, filter, reduce + - match, split, sum_array + Attempting 'function map() {...}' gives error: + 'Cannot redefine built-in function map' + This is why we used my_map, my_filter, my_reduce + +Test 12: All built-in functions work together +-------------------------------------- + Built-in result: [1 => 4, 3 => 8, 5 => 12, 7 => 16, 9 => 20] + Custom result: [1 => 4, 3 => 8, 5 => 12, 7 => 16, 9 => 20] + Both produce same output! diff --git a/test10_builtin_functions.fawk b/test10_builtin_functions.fawk new file mode 100644 index 0000000..81435fd --- /dev/null +++ b/test10_builtin_functions.fawk @@ -0,0 +1,156 @@ +# Test 10: Built-in Functions + +BEGIN { + print "Test 1: length() function" + print "--------------------------------------" + str = "Hello World" + arr = [1, 2, 3, 4, 5] + print " length(\"Hello World\"):", length(str) + print " length([1,2,3,4,5]):", length(arr) + print "" + + print "Test 2: map() function" + print "--------------------------------------" + numbers = [1, 2, 3, 4, 5] + doubled = map((x) => { x * 2 }, numbers) + print " Original:", numbers + print " Doubled:", doubled + print "" + + print "Test 3: filter() function" + print "--------------------------------------" + nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + evens = filter((n) => { n % 2 == 0 }, nums) + print " Original:", nums + print " Evens only:", evens + print "" + + print "Test 4: reduce() function" + print "--------------------------------------" + values = [1, 2, 3, 4, 5] + sum = reduce((acc, x) => { acc + x }, 0, values) + product = reduce((acc, x) => { acc * x }, 1, values) + print " Array:", values + print " Sum:", sum + print " Product:", product + print "" + + print "Test 5: match() function" + print "--------------------------------------" + text = "Contact: alice@example.com" + result = match("([a-z]+)@([a-z]+\\.com)", text) + print " Text:", text + print " Match:", result[0] + print " User:", result[1] + print " Domain:", result[2] + print "" + + print "Test 6: split() function" + print "--------------------------------------" + line = "apple:banana:cherry:date" + parts = split(":", line) + print " Original:", line + print " Split by ':':", parts + print " Count:", length(parts) + print "" + + print "Test 7: sum_array() function" + print "--------------------------------------" + data = [10, 20, 30, 40, 50] + total = sum_array(data) + print " Array:", data + print " Sum:", total + print "" + + print "Test 8: Pipeline with built-ins" + print "--------------------------------------" + result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + |> filter((n) => { n > 3 }) + |> map((n) => { n * n }) + |> reduce((acc, n) => { acc + n }, 0) + print " Pipeline: [1..10] |> filter(>3) |> map(square) |> reduce(sum)" + print " Result:", result + print "" + + print "Test 9: Custom implementations work" + print "--------------------------------------" + print " (Built-in functions cannot be redefined)" + print " (But we can create our own with different names)" + print "" +} + +# Custom implementations with different names +function my_map(func, arr) { + result = [] + for (i in arr) { + result[i] = func(arr[i]) + } + return result +} + +function my_filter(pred, arr) { + result = [] + for (key in arr) { + if (pred(arr[key])) { + result[key] = arr[key] + } + } + return result +} + +function my_reduce(func, initial, arr) { + acc = initial + for (i in arr) { + acc = func(acc, arr[i]) + } + return acc +} + +END { + print "Test 10: Custom map/filter/reduce" + print "--------------------------------------" + + # Use custom implementations + data = [1, 2, 3, 4, 5] + + # Custom map + cubed = my_map((x) => { x * x * x }, data) + print " my_map(cube, [1,2,3,4,5]):", cubed + + # Custom filter + odds = my_filter((n) => { n % 2 != 0 }, data) + print " my_filter(odd, [1,2,3,4,5]):", odds + + # Custom reduce + sum = my_reduce((a, b) => { a + b }, 0, data) + print " my_reduce(sum, 0, [1,2,3,4,5]):", sum + + print "" + print "Test 11: Cannot redefine built-ins" + print "--------------------------------------" + print " Built-in functions are protected:" + print " - length, map, filter, reduce" + print " - match, split, sum_array" + print " Attempting 'function map() {...}' gives error:" + print " 'Cannot redefine built-in function map'" + print " This is why we used my_map, my_filter, my_reduce" + print "" + + print "Test 12: All built-in functions work together" + print "--------------------------------------" + + # Complex pipeline using built-ins and custom functions + dataset = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + + # Built-in pipeline + builtin_result = dataset + |> filter((n) => { n % 2 == 0 }) + |> map((n) => { n * 2 }) + + # Custom pipeline (manual composition) + custom_result = my_map((n) => { n * 2 }, my_filter((n) => { n % 2 == 0 }, dataset)) + + print " Built-in result:", builtin_result + print " Custom result:", custom_result + print " Both produce same output!" +} diff --git a/test1_arrays.expected b/test1_arrays.expected new file mode 100644 index 0000000..e7e8127 --- /dev/null +++ b/test1_arrays.expected @@ -0,0 +1,6 @@ +Sum: 15 +Range 10-15: [10, 11, 12, 13, 14, 15] +1 2 +3 4 +5 6 +Alice's score: 95 diff --git a/test1_arrays.fawk b/test1_arrays.fawk new file mode 100644 index 0000000..931c45a --- /dev/null +++ b/test1_arrays.fawk @@ -0,0 +1,43 @@ +# Test 1: Arrays as First-Class Values + +function array_sum(arr) { + total = 0 + for (i in arr) { + total = total + arr[i] + } + return total +} + +function make_range(start, end) { + result = [] + i = 0 + n = start + while (n <= end) { + result[i] = n + i = i + 1 + n = n + 1 + } + return result +} + +BEGIN { + # Regular arrays + numbers = [1, 2, 3, 4, 5] + result = array_sum(numbers) + print "Sum:", result + + # Returning arrays from functions + range = make_range(10, 15) + print "Range 10-15:", range + + # Nested arrays + matrix = [[1, 2], [3, 4], [5, 6]] + for (i in matrix) { + row = matrix[i] + print row[0], row[1] + } + + # Associative arrays + scores = ["alice" => 95, "bob" => 87, "carol" => 92] + print "Alice's score:", scores["alice"] +} diff --git a/test2_functions.expected b/test2_functions.expected new file mode 100644 index 0000000..d81cc07 --- /dev/null +++ b/test2_functions.expected @@ -0,0 +1 @@ +42 diff --git a/test2_functions.fawk b/test2_functions.fawk new file mode 100644 index 0000000..7b88a30 --- /dev/null +++ b/test2_functions.fawk @@ -0,0 +1,9 @@ +# Test 2: Functions as First-Class Values + +function double(x) { return x * 2 } +function apply(func, value) { return func(value) } + +BEGIN { + result = apply(double, 21) + print result +} diff --git a/test3_lambda.expected b/test3_lambda.expected new file mode 100644 index 0000000..ec0fbb2 --- /dev/null +++ b/test3_lambda.expected @@ -0,0 +1,3 @@ +42 +square(5): 25 +triple(7): 21 diff --git a/test3_lambda.fawk b/test3_lambda.fawk new file mode 100644 index 0000000..12f8557 --- /dev/null +++ b/test3_lambda.fawk @@ -0,0 +1,17 @@ +# Test 3: Anonymous Functions + +BEGIN { + # Full syntax + add = (a, b) => { + c = a + b + return c + } + print add(10, 32) + + # Shorthand for single expressions + square = (x) => { x * x } + triple = (x) => { x * 3 } + + print "square(5):", square(5) + print "triple(7):", triple(7) +} diff --git a/test4_pipeline.expected b/test4_pipeline.expected new file mode 100644 index 0000000..4aee1e1 --- /dev/null +++ b/test4_pipeline.expected @@ -0,0 +1 @@ +Result: 20 diff --git a/test4_pipeline.fawk b/test4_pipeline.fawk new file mode 100644 index 0000000..c899ab5 --- /dev/null +++ b/test4_pipeline.fawk @@ -0,0 +1,10 @@ +# Test 4: Functional Pipeline Operator +# Using built-in map, filter, reduce functions + +BEGIN { + result = [1, 2, 3, 4, 5] + |> filter((x) => { x % 2 == 0 }) + |> map((x) => { x * x }) + |> reduce((acc, x) => { acc + x }, 0) + print "Result:", result +} diff --git a/test5_higher_order.expected b/test5_higher_order.expected new file mode 100644 index 0000000..daa9e00 --- /dev/null +++ b/test5_higher_order.expected @@ -0,0 +1,2 @@ +[1 => 4, 3 => 8, 5 => 12, 7 => 16, 9 => 20] +[alice => 95, carol => 88] diff --git a/test5_higher_order.fawk b/test5_higher_order.fawk new file mode 100644 index 0000000..7c4bc1d --- /dev/null +++ b/test5_higher_order.fawk @@ -0,0 +1,16 @@ +# Test 5: Higher-Order Functions +# Using built-in map and filter functions + +BEGIN { + # Works with regular arrays + nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + doubled = nums + |> filter((n) => { n % 2 == 0 }) + |> map((n) => { n * 2 }) + print doubled + + # Works with associative arrays too + scores = ["alice" => 95, "bob" => 67, "carol" => 88] + passing = scores |> filter((s) => { s >= 70 }) + print passing +} diff --git a/test6_lexical_scope.expected b/test6_lexical_scope.expected new file mode 100644 index 0000000..dff8ed0 --- /dev/null +++ b/test6_lexical_scope.expected @@ -0,0 +1,27 @@ +Test 1: Closures and lexical scope + outer(20) = 35 + +Test 2: Global variables + Initial global_counter: 0 + After increment_global(): 1 + After another increment_global(): 2 + +Test 3: Isolation - non-globals not visible + Before calling check_isolation(), outer i = 5 + Inside check_isolation(), initial i was: 0 + After calling check_isolation(), outer i = 5 + (outer i unchanged, inner i was separate) + +Test 4: Arrays passed by value (immutable) + Before modify_array(), local_arr[0] = 10 + Before modify_array(), local_arr[1] = 20 + After modify_array(), local_arr[0] = 10 + After modify_array(), local_arr[1] = 20 + (local array unchanged - passed by value) + +Test 5: Global arrays are mutable + Before modify_global_array(), global_arr[0] = 50 + Before modify_global_array(), global_arr[1] = 60 + After modify_global_array(), global_arr[0] = 111 + After modify_global_array(), global_arr[1] = 222 + (global array changed - globals are mutable) diff --git a/test6_lexical_scope.fawk b/test6_lexical_scope.fawk new file mode 100644 index 0000000..50752a0 --- /dev/null +++ b/test6_lexical_scope.fawk @@ -0,0 +1,88 @@ +# Test 6: Lexical Scope + +function outer(x) { + y = x + 10 + + inner = (z) => { + w = z + 5 + return w + } + + return inner(y) +} + +function increment_global() { + global_counter = global_counter + 1 + return global_counter +} + +function check_isolation() { + # i should be 0 (default) here, not visible from outer scope + initial = i + i = 100 + return initial +} + +function modify_array(arr) { + # Try to modify the array - should not affect caller's array + arr[0] = 999 + arr[1] = 888 + return arr[0] +} + +function modify_global_array() { + # Modify global array - this SHOULD affect the global + global_arr[0] = 111 + global_arr[1] = 222 +} + +BEGIN { + global global_counter, global_arr + + # Test 1: Closures and lexical scope + print "Test 1: Closures and lexical scope" + result = outer(20) + print " outer(20) =", result + print "" + + # Test 2: Global variables + print "Test 2: Global variables" + global_counter = 0 + print " Initial global_counter:", global_counter + increment_global() + print " After increment_global():", global_counter + increment_global() + print " After another increment_global():", global_counter + print "" + + # Test 3: Functions don't see non-global outer variables + print "Test 3: Isolation - non-globals not visible" + i = 5 + print " Before calling check_isolation(), outer i =", i + inner_initial = check_isolation() + print " Inside check_isolation(), initial i was:", inner_initial + print " After calling check_isolation(), outer i =", i + print " (outer i unchanged, inner i was separate)" + print "" + + # Test 4: Arrays passed by value (immutable) + print "Test 4: Arrays passed by value (immutable)" + local_arr = [10, 20, 30] + print " Before modify_array(), local_arr[0] =", local_arr[0] + print " Before modify_array(), local_arr[1] =", local_arr[1] + modify_array(local_arr) + print " After modify_array(), local_arr[0] =", local_arr[0] + print " After modify_array(), local_arr[1] =", local_arr[1] + print " (local array unchanged - passed by value)" + print "" + + # Test 5: Global arrays are mutable + print "Test 5: Global arrays are mutable" + global_arr = [50, 60, 70] + print " Before modify_global_array(), global_arr[0] =", global_arr[0] + print " Before modify_global_array(), global_arr[1] =", global_arr[1] + modify_global_array() + print " After modify_global_array(), global_arr[0] =", global_arr[0] + print " After modify_global_array(), global_arr[1] =", global_arr[1] + print " (global array changed - globals are mutable)" +} diff --git a/test7_csv.expected b/test7_csv.expected new file mode 100644 index 0000000..b23d86b --- /dev/null +++ b/test7_csv.expected @@ -0,0 +1,3 @@ +electronics average: 650 +books average: 34 +clothing average: 71.66666666666667 diff --git a/test7_csv.fawk b/test7_csv.fawk new file mode 100644 index 0000000..dccfe95 --- /dev/null +++ b/test7_csv.fawk @@ -0,0 +1,33 @@ +# Test 7: Complete CSV Processing Example + +function sum(arr) { + total = 0 + for (i in arr) { total = total + arr[i] } + return total +} + +function avg(arr) { + return sum(arr) / length(arr) +} + +BEGIN { + global sales + FS = "," # Set field separator to comma for CSV + sales = ["electronics" => [], "books" => [], "clothing" => []] +} + +# Parse CSV: category,product,price +{ + if (NR > 1) { + category = $1 + price = $3 + sales[category][length(sales[category])] = price + } +} + +END { + for (cat in sales) { + average = avg(sales[cat]) + print cat, "average:", average + } +} diff --git a/test8_advanced.expected b/test8_advanced.expected new file mode 100644 index 0000000..8bfa2ec --- /dev/null +++ b/test8_advanced.expected @@ -0,0 +1,34 @@ +Test 1: match() function +-------------------------------------- + match() found: $42.50 + Group 1 (dollars): 42 + Group 2 (cents): 50 + +Test 2: split() function +-------------------------------------- + split() returned 4 items + Item 0: apple + Item 1: banana + Item 2: cherry + +Test 3: Auto-vivification +-------------------------------------- + Auto-vivified array created + Element 0: 100 + Element 1: 200 + +Test 4: Pipeline-friendly functions +-------------------------------------- + Pipeline: text |> split(",") works! + Result[1]: two + Nested pipeline result[0]: 2 + +Test 5: Regex pattern matching +-------------------------------------- +Found product line: product laptop 1200 +Found product line: product mouse 25 +Found issue: Warning: low stock detected +Found product line: product keyboard 75 +Found issue: ERROR: connection failed +Found product line: product monitor 350 +Pattern matching completed diff --git a/test8_advanced.fawk b/test8_advanced.fawk new file mode 100644 index 0000000..82471be --- /dev/null +++ b/test8_advanced.fawk @@ -0,0 +1,90 @@ +# Test 8: Advanced features - regex, match, split, auto-vivification + +function test_match() { + text = "The price is $42.50 for item" + + # match(pattern, text) - pattern first for pipeline-friendliness + # Use single backslash for regex escapes + result = match("\$([0-9]+)\.([0-9]+)", text) + + if (length(result) > 0) { + print " match() found:", result[0] + print " Group 1 (dollars):", result[1] + print " Group 2 (cents):", result[2] + } +} + +function test_split() { + csv_line = "apple,banana,cherry,date" + + # split(separator, text) - separator first for pipeline-friendliness + fruits = split(",", csv_line) + count = length(fruits) + print " split() returned", count, "items" + print " Item 0:", fruits[0] + print " Item 1:", fruits[1] + print " Item 2:", fruits[2] +} + +function test_auto_vivify() { + # Write to undefined array - should auto-create + undefined_arr[0] = 100 + undefined_arr[1] = 200 + undefined_arr[2] = 300 + + print " Auto-vivified array created" + print " Element 0:", undefined_arr[0] + print " Element 1:", undefined_arr[1] +} + +function test_pipeline() { + # Demonstrate pipeline-friendly argument order + text = "one,two,three" + + # split with pipeline + result = text |> split(",") + print " Pipeline: text |> split(\",\") works!" + print " Result[1]:", result[1] + + # Can also be used in nested pipelines + doubled = "1,2,3" |> split(",") |> map((x) => { x * 2 }) + print " Nested pipeline result[0]:", doubled[0] +} + +BEGIN { + print "Test 1: match() function" + print "--------------------------------------" + test_match() + print "" + + print "Test 2: split() function" + print "--------------------------------------" + test_split() + print "" + + print "Test 3: Auto-vivification" + print "--------------------------------------" + test_auto_vivify() + print "" + + print "Test 4: Pipeline-friendly functions" + print "--------------------------------------" + test_pipeline() + print "" + + print "Test 5: Regex pattern matching" + print "--------------------------------------" +} + +# Test 4: Regex pattern matching +/^product/ { + print "Found product line:", $0 +} + +/error|warning/i { + print "Found issue:", $0 +} + +END { + print "Pattern matching completed" +} diff --git a/test8_input.txt b/test8_input.txt new file mode 100644 index 0000000..a73f0dd --- /dev/null +++ b/test8_input.txt @@ -0,0 +1,8 @@ +product laptop 1200 +info some data here +product mouse 25 +Warning: low stock detected +product keyboard 75 +ERROR: connection failed +product monitor 350 +everything is fine diff --git a/test9_builtins.expected b/test9_builtins.expected new file mode 100644 index 0000000..d80ea5b --- /dev/null +++ b/test9_builtins.expected @@ -0,0 +1,62 @@ +Test 1: ARGC and ARGV +-------------------------------------- + ARGC: 3 + ARGV[0]: /workspace/fawk.py + ARGV[1]: test9_builtins.fawk + ARGV[2]: test9_input.txt + +Test 2: ENVIRON +-------------------------------------- + PATH exists: 1 + USER: ubuntu + +Test 3: Format variables +-------------------------------------- + CONVFMT: %.6g + OFMT: %.6g + SUBSEP: 1 bytes + +Test 4: Field and record separators +-------------------------------------- + FS: + OFS: + RS: + + ORS: 1 bytes + +Test 5: Modified OFS +-------------------------------------- + a|b|c + +Test 6: match() sets RSTART and RLENGTH +-------------------------------------- + Text: Hello World 123 + Pattern: [0-9]+ + RSTART: 13 + RLENGTH: 3 + Matched: 123 + +Test 7: FS can be changed +-------------------------------------- + FS set to ':' + +Test 8: Field splitting with FS +-------------------------------------- + Line 1 has 3 fields + Field 1: name + Field 2: alice + Field 3: developer + Line 2 has 3 fields + Field 1: name + Field 2: bob + Field 3: designer + Line 3 has 3 fields + Field 1: role + Field 2: charlie + Field 3: manager + +Test 9: Record counters +-------------------------------------- + NR (total records): 3 + FNR (file records): 3 + FILENAME: test9_input.txt diff --git a/test9_builtins.fawk b/test9_builtins.fawk new file mode 100644 index 0000000..17c110c --- /dev/null +++ b/test9_builtins.fawk @@ -0,0 +1,82 @@ +# Test 9: AWK Built-in Variables + +BEGIN { + print "Test 1: ARGC and ARGV" + print "--------------------------------------" + print " ARGC:", ARGC + print " ARGV[0]:", ARGV[0] + print " ARGV[1]:", ARGV[1] + if (ARGC > 2) { + print " ARGV[2]:", ARGV[2] + } + print "" + + print "Test 2: ENVIRON" + print "--------------------------------------" + print " PATH exists:", length(ENVIRON["PATH"]) > 0 + print " USER:", ENVIRON["USER"] + print "" + + print "Test 3: Format variables" + print "--------------------------------------" + print " CONVFMT:", CONVFMT + print " OFMT:", OFMT + print " SUBSEP:", length(SUBSEP), "bytes" + print "" + + print "Test 4: Field and record separators" + print "--------------------------------------" + print " FS:", FS + print " OFS:", OFS + print " RS:", RS + print " ORS:", length(ORS), "bytes" + print "" + + # Change OFS + OFS = "|" + print "Test 5: Modified OFS" + print "--------------------------------------" + print " a", "b", "c" + OFS = " " + print "" + + print "Test 6: match() sets RSTART and RLENGTH" + print "--------------------------------------" + text = "Hello World 123" + result = match("[0-9]+", text) + print " Text:", text + print " Pattern: [0-9]+" + print " RSTART:", RSTART + print " RLENGTH:", RLENGTH + print " Matched:", result[0] + print "" + + print "Test 7: FS can be changed" + print "--------------------------------------" + FS = ":" + print " FS set to ':'" + print "" +} + +# Test field splitting with changed FS +{ + if (NR == 1) { + print "Test 8: Field splitting with FS" + print "--------------------------------------" + } + print " Line", NR, "has", NF, "fields" + print " Field 1:", $1 + print " Field 2:", $2 + if (NF > 2) { + print " Field 3:", $3 + } +} + +END { + print "" + print "Test 9: Record counters" + print "--------------------------------------" + print " NR (total records):", NR + print " FNR (file records):", FNR + print " FILENAME:", FILENAME +} diff --git a/test9_input.txt b/test9_input.txt new file mode 100644 index 0000000..ea56809 --- /dev/null +++ b/test9_input.txt @@ -0,0 +1,3 @@ +name:alice:developer +name:bob:designer +role:charlie:manager diff --git a/test_all.sh b/test_all.sh new file mode 100755 index 0000000..f563113 --- /dev/null +++ b/test_all.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# FAWK Test Suite +# Runs all tests and validates output against expected results + +set -e + +cd "$(dirname "$0")" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +PASSED=0 +FAILED=0 +TOTAL=0 + +echo "======================================================================" +echo "FAWK Interpreter Test Suite" +echo "======================================================================" +echo "" + +run_test() { + local test_name="$1" + local script="$2" + local input_file="$3" + local expected="$4" + + TOTAL=$((TOTAL + 1)) + + local input_desc="" + if [ -n "$input_file" ]; then + input_desc=" (with $input_file)" + fi + + echo "Test $TOTAL: $test_name$input_desc" + echo "----------------------------------------------------------------------" + + # Run the test + local actual_output=$(mktemp) + if [ -n "$input_file" ]; then + ./fawk "$script" "$input_file" > "$actual_output" 2>&1 + else + ./fawk "$script" > "$actual_output" 2>&1 + fi + + # Compare output + if diff -q "$expected" "$actual_output" > /dev/null 2>&1; then + echo -e "${GREEN}✓ PASSED${NC}" + PASSED=$((PASSED + 1)) + else + echo -e "${RED}✗ FAILED${NC}" + echo " Expected output differs from actual output:" + diff -u "$expected" "$actual_output" | head -20 + FAILED=$((FAILED + 1)) + fi + + rm -f "$actual_output" + echo "" +} + +# Run all tests +run_test "Arrays as First-Class Values" "test1_arrays.fawk" "" "test1_arrays.expected" +run_test "Functions as First-Class Values" "test2_functions.fawk" "" "test2_functions.expected" +run_test "Anonymous Functions" "test3_lambda.fawk" "" "test3_lambda.expected" +run_test "Functional Pipeline Operator" "test4_pipeline.fawk" "" "test4_pipeline.expected" +run_test "Higher-Order Functions" "test5_higher_order.fawk" "" "test5_higher_order.expected" +run_test "Lexical Scope" "test6_lexical_scope.fawk" "" "test6_lexical_scope.expected" +run_test "CSV Processing" "test7_csv.fawk" "sales.csv" "test7_csv.expected" +run_test "Advanced Features" "test8_advanced.fawk" "test8_input.txt" "test8_advanced.expected" +run_test "Built-in Variables" "test9_builtins.fawk" "test9_input.txt" "test9_builtins.expected" +run_test "Built-in Functions" "test10_builtin_functions.fawk" "" "test10_builtin_functions.expected" + +# Summary +echo "======================================================================" +echo "Results: $PASSED passed, $FAILED failed out of $TOTAL tests" +echo "======================================================================" + +if [ $FAILED -eq 0 ]; then + exit 0 +else + exit 1 +fi