commit
This commit is contained in:
@@ -0,0 +1,436 @@
|
||||
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:
|
||||
//
|
||||
// <statement> ::= <ident> "=" <expr> | <expr>
|
||||
// <expr> ::= <term> { ("+" | "-") <term> }
|
||||
// <term> ::= <factor> { ("x" | "/") <factor> }
|
||||
// <factor> ::= <number> | <ident> | "(" <expr> ")" | "-" <factor>
|
||||
// <number> ::= <integer> | <float>
|
||||
// <integer> ::= digit digit? digit? (1-3 digits)
|
||||
// <float> ::= digit digit? "." digit digit? (1-2 . 1-2 digits)
|
||||
// <ident> ::= 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 "<result text>" or Error "<message>".
|
||||
member this.Evaluate (input: string) : Result<string, string> =
|
||||
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<float * float, string> =
|
||||
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)
|
||||
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<RootNamespace>Interpreter</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="Interpreter.fs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user