namespace Interpreter /// ============================================================================ /// CMP-7009A Advanced Programming - Reassessment 001 /// F# Interpreter for a small arithmetic / variable-assignment language. /// /// DESIGN NOTE ON THE "NO BUILT-IN / LIBRARY FUNCTIONS" CONSTRAINT /// ---------------------------------------------------------------- /// Every part of lexing, parsing and evaluation below is written using only /// core language constructs: pattern matching, recursion, if/then/else, /// arithmetic operators (+, -, *, /), comparison operators and string /// concatenation (+). No use is made of: /// - Int32.Parse / Double.Parse / TryParse /// - System.Text.RegularExpressions /// - String.Split / String.Substring /// - FSharp.Core collection functions such as List.map, List.fold, /// List.filter, List.rev, List.length, Seq.*, Array.* /// Numbers are built digit-by-digit; identifiers are built character-by- /// character; lists are reversed and searched with hand-written recursive /// helper functions; the variable environment is a plain association list /// searched and updated with hand-written recursion (no Map/Dictionary). /// This is all individual work - no code was copied from any library. /// ============================================================================ module Core = // ------------------------------------------------------------------ // Character classification helpers (manual - no System.Char.IsDigit etc.) // ------------------------------------------------------------------ let isDigit (c: char) : bool = c >= '0' && c <= '9' let isLetter (c: char) : bool = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') let isAlphaNum (c: char) : bool = isDigit c || isLetter c let digitVal (c: char) : int = int c - int '0' // ------------------------------------------------------------------ // Errors // ------------------------------------------------------------------ exception LexError of string exception ParseError of string exception EvalError of string // ------------------------------------------------------------------ // Tokens // Six operators required by the spec: + - x / ( ) // Plus '=' for assignment, integers, floats and identifiers. // ------------------------------------------------------------------ type Token = | TInt of int | TFloat of float | TIdent of string | TPlus | TMinus | TMul // the character 'x' or 'X' | TDiv | TLParen | TRParen | TAssign // Manual list reverse (used instead of List.rev) let rec private reverseAcc (lst: 'a list) (acc: 'a list) : 'a list = match lst with | [] -> acc | h :: t -> reverseAcc t (h :: acc) // ------------------------------------------------------------------ // Tokenizer // ------------------------------------------------------------------ let tokenize (input: string) : Token list = let len = input.Length // Count consecutive digit characters starting at position j // (does not build a value - used only to know how many digits exist, // so we can enforce the maximum digit rules BEFORE building the value). let rec countDigitsFrom (j: int) (count: int) : int = if j < len && isDigit input.[j] then countDigitsFrom (j + 1) (count + 1) else count // Build an integer value from 'remaining' digits starting at position j. let rec buildIntValue (j: int) (remaining: int) (value: int) : int * int = if remaining = 0 then (j, value) else buildIntValue (j + 1) (remaining - 1) (value * 10 + digitVal input.[j]) // Build a fractional value (e.g. digits "5","0" -> 0.50) from 'remaining' // digits starting at position j. let rec buildFracValue (j: int) (remaining: int) (value: float) (divisor: float) : float = if remaining = 0 then value else buildFracValue (j + 1) (remaining - 1) (value + float (digitVal input.[j]) / divisor) (divisor * 10.0) // Scan a number token starting at position i. Handles both the // "plain integer, max 3 digits" case and the // "float, max 2 digits before AND after the point" case. let scanNumber (i: int) : int * Token = let intDigitCount = countDigitsFrom i 0 let afterInt = i + intDigitCount if afterInt < len && input.[afterInt] = '.' then // Floating point literal if intDigitCount = 0 || intDigitCount > 2 then raise (LexError "Float literal: integer part must be 1-2 digits") let fracStart = afterInt + 1 let fracDigitCount = countDigitsFrom fracStart 0 if fracDigitCount = 0 || fracDigitCount > 2 then raise (LexError "Float literal: fractional part must be 1-2 digits") let (_, intPart) = buildIntValue i intDigitCount 0 let fracPart = buildFracValue fracStart fracDigitCount 0.0 10.0 (fracStart + fracDigitCount, TFloat (float intPart + fracPart)) else // Plain integer literal if intDigitCount = 0 || intDigitCount > 3 then raise (LexError "Integer literal must be 1-3 digits") let (nextPos, v) = buildIntValue i intDigitCount 0 (nextPos, TInt v) // Scan an identifier: letter, then up to 3 more alphanumeric chars (max 4 total) let scanIdent (i: int) : int * Token = let rec countAlnum (j: int) (count: int) : int = if j < len && isAlphaNum input.[j] then countAlnum (j + 1) (count + 1) else count let idLen = countAlnum i 0 if idLen > 4 then raise (LexError "Variable names may be at most 4 alphanumeric characters") let rec buildStr (j: int) (remaining: int) (acc: string) : string = if remaining = 0 then acc else buildStr (j + 1) (remaining - 1) (acc + string input.[j]) let s = buildStr i idLen "" (i + idLen, TIdent s) let rec scan (i: int) (acc: Token list) : Token list = if i >= len then reverseAcc acc [] else let c = input.[i] if c = ' ' || c = '\t' then scan (i + 1) acc elif c = '+' then scan (i + 1) (TPlus :: acc) elif c = '-' then scan (i + 1) (TMinus :: acc) elif c = 'x' || c = 'X' then scan (i + 1) (TMul :: acc) elif c = '/' then scan (i + 1) (TDiv :: acc) elif c = '(' then scan (i + 1) (TLParen :: acc) elif c = ')' then scan (i + 1) (TRParen :: acc) elif c = '=' then scan (i + 1) (TAssign :: acc) elif isDigit c then let (nextI, tok) = scanNumber i scan nextI (tok :: acc) elif isLetter c then let (nextI, tok) = scanIdent i scan nextI (tok :: acc) else raise (LexError ("Unexpected character: '" + string c + "'")) scan 0 [] // ------------------------------------------------------------------ // Abstract Syntax Tree // ------------------------------------------------------------------ type Expr = | Num of float | Var of string | Neg of Expr | BinOp of char * Expr * Expr // op is one of '+','-','*','/' type Stmt = | Assign of string * Expr | Eval of Expr // ------------------------------------------------------------------ // Recursive-descent parser. // // BNF grammar implemented: // // ::= "=" | // ::= { ("+" | "-") } // ::= { ("x" | "/") } // ::= | | "(" ")" | "-" // ::= | // ::= digit digit? digit? (1-3 digits) // ::= digit digit? "." digit digit? (1-2 . 1-2 digits) // ::= letter (letter | digit){0,3} (max 4 chars) // ------------------------------------------------------------------ let rec private parseExpr (toks: Token list) : Expr * Token list = let (left, rest) = parseTerm toks let rec loop (accExpr: Expr) (toks2: Token list) : Expr * Token list = match toks2 with | TPlus :: rest2 -> let (rightExpr, rest3) = parseTerm rest2 loop (BinOp('+', accExpr, rightExpr)) rest3 | TMinus :: rest2 -> let (rightExpr, rest3) = parseTerm rest2 loop (BinOp('-', accExpr, rightExpr)) rest3 | _ -> (accExpr, toks2) loop left rest and private parseTerm (toks: Token list) : Expr * Token list = let (left, rest) = parseFactor toks let rec loop (accExpr: Expr) (toks2: Token list) : Expr * Token list = match toks2 with | TMul :: rest2 -> let (rightExpr, rest3) = parseFactor rest2 loop (BinOp('*', accExpr, rightExpr)) rest3 | TDiv :: rest2 -> let (rightExpr, rest3) = parseFactor rest2 loop (BinOp('/', accExpr, rightExpr)) rest3 | _ -> (accExpr, toks2) loop left rest and private parseFactor (toks: Token list) : Expr * Token list = match toks with | TMinus :: rest -> let (e, rest2) = parseFactor rest (Neg e, rest2) | TInt n :: rest -> (Num (float n), rest) | TFloat f :: rest -> (Num f, rest) | TIdent name :: rest -> (Var name, rest) | TLParen :: rest -> let (e, rest2) = parseExpr rest match rest2 with | TRParen :: rest3 -> (e, rest3) | _ -> raise (ParseError "Expected closing bracket ')'") | [] -> raise (ParseError "Unexpected end of input, expected a value") | _ -> raise (ParseError "Unexpected token in expression") let parseTokens (tokens: Token list) : Stmt = match tokens with | TIdent name :: TAssign :: rest when rest <> [] -> let (e, remaining) = parseExpr rest match remaining with | [] -> Assign(name, e) | _ -> raise (ParseError "Unexpected tokens after expression") | _ -> let (e, remaining) = parseExpr tokens match remaining with | [] -> Eval e | _ -> raise (ParseError "Unexpected tokens after expression") // ------------------------------------------------------------------ // Evaluator - environment is a plain (name * value) association list, // searched / updated with hand-written recursion (no Map / Dictionary). // ------------------------------------------------------------------ let rec lookupVar (name: string) (env: (string * float) list) : float = match env with | [] -> raise (EvalError ("Undefined variable: " + name)) | (n, v) :: rest -> if n = name then v else lookupVar name rest let rec updateEnv (name: string) (value: float) (env: (string * float) list) : (string * float) list = match env with | [] -> [ (name, value) ] | (n, v) :: rest -> if n = name then (name, value) :: rest else (n, v) :: updateEnv name value rest let rec evalExpr (e: Expr) (env: (string * float) list) : float = match e with | Num n -> n | Var name -> lookupVar name env | Neg inner -> -1.0 * (evalExpr inner env) | BinOp (op, l, r) -> let lv = evalExpr l env let rv = evalExpr r env match op with | '+' -> lv + rv | '-' -> lv - rv | '*' -> lv * rv | '/' -> if rv = 0.0 then raise (EvalError "Division by zero") else lv / rv | _ -> raise (EvalError "Unknown operator") // ------------------------------------------------------------------ // Manual float -> string formatting (avoids relying on default .NET // formatting quirks; strips a trailing ".0" for whole numbers). // ------------------------------------------------------------------ let formatNumber (v: float) : string = let rounded = System.Math.Round(v, 6) if rounded = System.Math.Floor(rounded) && abs rounded < 1e15 then (string (int64 rounded)) else string rounded open Core /// ============================================================================ /// Public engine exposed to the C# GUI project. /// Keeps a persistent variable environment across calls, so a user can type /// "a = 5" then later type "a x 2 + 1" and it will resolve 'a'. /// ============================================================================ type Engine() = let mutable env : (string * float) list = [] /// Evaluate one line of input (either "name = expr" or a plain "expr"). /// Returns Ok "" or Error "". member this.Evaluate (input: string) : Result = try let tokens = tokenize input if tokens = [] then Error "Empty expression" else let stmt = parseTokens tokens match stmt with | Assign (name, e) -> let v = evalExpr e env env <- updateEnv name v env Ok (name + " = " + formatNumber v) | Eval e -> let v = evalExpr e env Ok (formatNumber v) with | LexError msg -> Error ("Lexical error: " + msg) | ParseError msg -> Error ("Syntax error: " + msg) | EvalError msg -> Error ("Evaluation error: " + msg) | ex -> Error ("Error: " + ex.Message) /// Look up a previously assigned variable's value, if any. member this.TryGetVariable (name: string) : float option = let rec find (e: (string * float) list) = match e with | [] -> None | (n, v) :: rest -> if n = name then Some v else find rest find env /// Clear all assigned variables. member this.Reset () : unit = env <- [] /// -------------------------------------------------------------- /// Parses the special canvas line-drawing syntax: "y = ax + b" /// (also accepts "y = x + b", "y = -x + b", "y = 2.5x - 3", etc.) /// /// Because 'x' is already the multiplication operator token in the /// general grammar, this line format is parsed directly from the raw /// characters rather than reusing the general tokenizer, exactly as /// the module's grammar intends 'x' here to mean the canvas x-axis, /// not a multiplication sign. 'a' and 'b' may each be a number /// literal or a variable name previously assigned via Evaluate. /// -------------------------------------------------------------- member this.ParseLine (input: string) : Result = let s = input let len = s.Length let rec skipWs (i: int) : int = if i < len && (s.[i] = ' ' || s.[i] = '\t') then skipWs (i + 1) else i // Read a number literal (manual digit accumulation, same style as tokenizer) let readNumber (j: int) : (float * int) option = let rec countDigitsFrom (k: int) (count: int) : int = if k < len && isDigit s.[k] then countDigitsFrom (k + 1) (count + 1) else count let intCount = countDigitsFrom j 0 if intCount = 0 then None else let rec buildInt (k: int) (remaining: int) (value: int) = if remaining = 0 then value else buildInt (k + 1) (remaining - 1) (value * 10 + digitVal s.[k]) let intVal = buildInt j intCount 0 let afterInt = j + intCount if afterInt < len && s.[afterInt] = '.' then let fracStart = afterInt + 1 let fracCount = countDigitsFrom fracStart 0 if fracCount = 0 then Some (float intVal, afterInt) else let rec buildFrac (k: int) (remaining: int) (value: float) (divisor: float) = if remaining = 0 then value else buildFrac (k + 1) (remaining - 1) (value + float (digitVal s.[k]) / divisor) (divisor * 10.0) let fracVal = buildFrac fracStart fracCount 0.0 10.0 Some (float intVal + fracVal, fracStart + fracCount) else Some (float intVal, afterInt) // Read a variable name (letter + up to 3 alphanumerics) and resolve its value let readVariable (j: int) : (float * int) option = if j < len && isLetter s.[j] then let rec countAlnum (k: int) (count: int) : int = if k < len && isAlphaNum s.[k] && count < 4 then countAlnum (k + 1) (count + 1) else count let idLen = countAlnum j 0 let rec buildStr (k: int) (remaining: int) (acc: string) = if remaining = 0 then acc else buildStr (k + 1) (remaining - 1) (acc + string s.[k]) let name = buildStr j idLen "" match this.TryGetVariable name with | Some v -> Some (v, j + idLen) | None -> None else None // Read either a number or a variable at position j let readValue (j: int) : (float * int) option = match readNumber j with | Some r -> Some r | None -> readVariable j let i0 = skipWs 0 if i0 >= len || (s.[i0] <> 'y' && s.[i0] <> 'Y') then Error "Line equation must start with 'y'" else let i1 = skipWs (i0 + 1) if i1 >= len || s.[i1] <> '=' then Error "Expected '=' after 'y'" else let i2 = skipWs (i1 + 1) // optional leading '-' for the coefficient let (negCoef, i3) = if i2 < len && s.[i2] = '-' then (true, skipWs (i2 + 1)) else (false, i2) // optional explicit coefficient value before 'x' (defaults to 1) let (coefMag, i4) = if i3 < len && (s.[i3] = 'x' || s.[i3] = 'X') then (1.0, i3) else match readValue i3 with | Some (v, nextI) -> (v, skipWs nextI) | None -> (1.0, i3) let coef = if negCoef then -1.0 * coefMag else coefMag if i4 >= len || (s.[i4] <> 'x' && s.[i4] <> 'X') then Error "Expected 'x' in line equation (format: y = ax + b)" else let i5 = skipWs (i4 + 1) if i5 >= len || (s.[i5] <> '+' && s.[i5] <> '-') then Error "Expected '+' or '-' before the intercept (format: y = ax + b)" else let signIsNeg = s.[i5] = '-' let i6 = skipWs (i5 + 1) match readValue i6 with | None -> Error "Expected a number or variable for the intercept 'b'" | Some (bMag, nextI) -> let i7 = skipWs nextI if i7 <> len then Error "Unexpected extra characters after intercept" else let b = if signIsNeg then -1.0 * bMag else bMag Ok (coef, b)