This commit is contained in:
2026-08-07 15:58:44 +01:00
commit f5eab85632
17 changed files with 870 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Interpreter", "Interpreter\Interpreter.fsproj", "{11111111-1111-1111-1111-111111111111}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GUI", "GUI\GUI.csproj", "{22222222-2222-2222-2222-222222222222}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{11111111-1111-1111-1111-111111111111}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{11111111-1111-1111-1111-111111111111}.Debug|Any CPU.Build.0 = Debug|Any CPU
{11111111-1111-1111-1111-111111111111}.Release|Any CPU.ActiveCfg = Release|Any CPU
{11111111-1111-1111-1111-111111111111}.Release|Any CPU.Build.0 = Release|Any CPU
{22222222-2222-2222-2222-222222222222}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{22222222-2222-2222-2222-222222222222}.Debug|Any CPU.Build.0 = Debug|Any CPU
{22222222-2222-2222-2222-222222222222}.Release|Any CPU.ActiveCfg = Release|Any CPU
{22222222-2222-2222-2222-222222222222}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+6
View File
@@ -0,0 +1,6 @@
<Application x:Class="InterpreterGui.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources />
</Application>
+8
View File
@@ -0,0 +1,8 @@
using System.Windows;
namespace InterpreterGui
{
public partial class App : Application
{
}
}
+15
View File
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<RootNamespace>InterpreterGui</RootNamespace>
<AssemblyName>InterpreterGui</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Interpreter\Interpreter.fsproj" />
</ItemGroup>
</Project>
+50
View File
@@ -0,0 +1,50 @@
<Window x:Class="InterpreterGui.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="CMP-7009A Interpreter + Maths Visualiser" Height="650" Width="800"
MinHeight="500" MinWidth="650">
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Enter an expression (e.g. 3 + 4 x 2), a variable assignment (e.g. a = 5), or a line equation (e.g. y = 2x + 1):"
TextWrapping="Wrap" Margin="0,0,0,6"/>
<Grid Grid.Row="1" Margin="0,0,0,6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="InputBox" Grid.Column="0" Height="28" VerticalContentAlignment="Center"
KeyDown="InputBox_KeyDown"/>
<Button x:Name="RunButton" Grid.Column="1" Content="Run" Width="80" Margin="6,0,0,0"
Click="RunButton_Click"/>
<Button x:Name="ClearButton" Grid.Column="2" Content="Clear Canvas" Width="100" Margin="6,0,0,0"
Click="ClearButton_Click"/>
</Grid>
<Grid Grid.Row="2" Margin="0,0,0,6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Output: " VerticalAlignment="Center" FontWeight="Bold"/>
<TextBox x:Name="OutputBox" Grid.Column="1" Height="28" IsReadOnly="True"
VerticalContentAlignment="Center" Background="#F2F2F2"/>
</Grid>
<Border Grid.Row="3" BorderBrush="Gray" BorderThickness="1">
<Canvas x:Name="DrawCanvas" Background="White" ClipToBounds="True"
SizeChanged="DrawCanvas_SizeChanged"/>
</Border>
<TextBlock Grid.Row="4" x:Name="StatusText" Margin="0,6,0,0" Foreground="Gray"
Text="Ready."/>
</Grid>
</Window>
+206
View File
@@ -0,0 +1,206 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using Interpreter;
using Microsoft.FSharp.Core;
namespace InterpreterGui
{
public partial class MainWindow : Window
{
// The F# interpreter engine. Persists variables across evaluations.
private readonly Engine _engine = new Engine();
// Pixels per one unit on the maths axes.
private const double Scale = 25.0;
public MainWindow()
{
InitializeComponent();
Loaded += (s, e) => DrawAxes();
}
private void InputBox_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
RunInput();
}
}
private void RunButton_Click(object sender, RoutedEventArgs e)
{
RunInput();
}
private void ClearButton_Click(object sender, RoutedEventArgs e)
{
DrawCanvas.Children.Clear();
DrawAxes();
StatusText.Text = "Canvas cleared.";
}
private void DrawCanvas_SizeChanged(object sender, SizeChangedEventArgs e)
{
// Redraw axes (and nothing else) whenever the window is resized.
DrawCanvas.Children.Clear();
DrawAxes();
}
/// <summary>
/// Decides whether the input is a line equation ("y = ax + b") or a
/// plain expression / assignment, and routes it accordingly.
/// </summary>
private void RunInput()
{
string text = InputBox.Text;
if (text == null)
{
return;
}
string trimmed = text.Trim();
if (trimmed.Length == 0)
{
return;
}
// Detect "y = ..." form (allowing for whitespace) to route to the
// line-drawing parser instead of the general expression evaluator.
string noLeadingWs = trimmed.TrimStart();
bool looksLikeLine =
noLeadingWs.Length > 0 &&
(noLeadingWs[0] == 'y' || noLeadingWs[0] == 'Y') &&
noLeadingWs.TrimStart('y', 'Y', ' ', '\t').StartsWith("=");
if (looksLikeLine)
{
FSharpResult<Tuple<double, double>, string> result = _engine.ParseLine(trimmed);
if (result.IsOk)
{
double a = result.ResultValue.Item1;
double b = result.ResultValue.Item2;
OutputBox.Text = "Line: y = " + a + "x + " + b;
StatusText.Text = "Drew line y = " + a + "x + " + b;
DrawLine(a, b);
}
else
{
OutputBox.Text = result.ErrorValue;
StatusText.Text = "Line parse failed.";
}
}
else
{
FSharpResult<string, string> result = _engine.Evaluate(trimmed);
if (result.IsOk)
{
OutputBox.Text = result.ResultValue;
StatusText.Text = "OK.";
}
else
{
OutputBox.Text = result.ErrorValue;
StatusText.Text = "Error.";
}
}
}
// ------------------------------------------------------------------
// Drawing (uses the WPF Canvas / Shapes API - explicitly permitted)
// ------------------------------------------------------------------
private double OriginX => DrawCanvas.ActualWidth / 2.0;
private double OriginY => DrawCanvas.ActualHeight / 2.0;
/// Converts a maths coordinate (x, y) into a canvas pixel point.
private Point ToCanvas(double x, double y)
{
return new Point(OriginX + x * Scale, OriginY - y * Scale);
}
private void DrawAxes()
{
double w = DrawCanvas.ActualWidth;
double h = DrawCanvas.ActualHeight;
if (w <= 0 || h <= 0)
{
return;
}
var xAxis = new Line
{
X1 = 0, Y1 = OriginY, X2 = w, Y2 = OriginY,
Stroke = Brushes.Black, StrokeThickness = 1.5
};
var yAxis = new Line
{
X1 = OriginX, Y1 = 0, X2 = OriginX, Y2 = h,
Stroke = Brushes.Black, StrokeThickness = 1.5
};
DrawCanvas.Children.Add(xAxis);
DrawCanvas.Children.Add(yAxis);
// Tick marks every unit, labelled every 5 units to avoid clutter.
int maxTicksX = (int)(w / (2 * Scale)) + 1;
for (int i = -maxTicksX; i <= maxTicksX; i++)
{
if (i == 0) continue;
double px = OriginX + i * Scale;
var tick = new Line { X1 = px, Y1 = OriginY - 4, X2 = px, Y2 = OriginY + 4, Stroke = Brushes.Gray, StrokeThickness = 1 };
DrawCanvas.Children.Add(tick);
if (i % 5 == 0)
{
var label = new TextBlock { Text = i.ToString(), FontSize = 10, Foreground = Brushes.Gray };
Canvas.SetLeft(label, px - 6);
Canvas.SetTop(label, OriginY + 6);
DrawCanvas.Children.Add(label);
}
}
int maxTicksY = (int)(h / (2 * Scale)) + 1;
for (int i = -maxTicksY; i <= maxTicksY; i++)
{
if (i == 0) continue;
double py = OriginY - i * Scale;
var tick = new Line { X1 = OriginX - 4, Y1 = py, X2 = OriginX + 4, Y2 = py, Stroke = Brushes.Gray, StrokeThickness = 1 };
DrawCanvas.Children.Add(tick);
if (i % 5 == 0)
{
var label = new TextBlock { Text = i.ToString(), FontSize = 10, Foreground = Brushes.Gray };
Canvas.SetLeft(label, OriginX + 6);
Canvas.SetTop(label, py - 6);
DrawCanvas.Children.Add(label);
}
}
}
/// Draws the line y = a*x + b across the full visible width of the canvas.
private void DrawLine(double a, double b)
{
double w = DrawCanvas.ActualWidth;
if (w <= 0)
{
return;
}
double xLeftMath = -OriginX / Scale;
double xRightMath = (w - OriginX) / Scale;
double yLeftMath = a * xLeftMath + b;
double yRightMath = a * xRightMath + b;
Point p1 = ToCanvas(xLeftMath, yLeftMath);
Point p2 = ToCanvas(xRightMath, yRightMath);
var line = new Line
{
X1 = p1.X, Y1 = p1.Y, X2 = p2.X, Y2 = p2.Y,
Stroke = Brushes.Blue, StrokeThickness = 2
};
DrawCanvas.Children.Add(line);
}
}
}
+436
View File
@@ -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)
+12
View File
@@ -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>
+113
View File
@@ -0,0 +1,113 @@
# CMP-7009A Reassessment 001 - Interpreter + Maths Visualiser
## How to open and run
1. Open **CMP7009A.sln** in Visual Studio 2022 (with the ".NET desktop
development" and "F# desktop language support" workloads installed).
2. Set **GUI** as the startup project (right-click GUI project -> "Set as
Startup Project").
3. Press F5 to build and run. The `GUI` project references `Interpreter`
directly (project reference), so both build together.
If you prefer the command line (with the .NET SDK installed):
```
dotnet build CMP7009A.sln
dotnet run --project GUI
```
## Project layout
```
CMP7009A.sln
Interpreter/
Interpreter.fsproj F# class library
Interpreter.fs Tokenizer, parser, evaluator, public Engine class
GUI/
GUI.csproj C# WPF application (references Interpreter)
App.xaml / App.xaml.cs
MainWindow.xaml Input box, output box, drawing canvas
MainWindow.xaml.cs Wires the UI to Engine; draws axes and lines
```
## Using the app
Type into the input box and press Enter or click Run:
- `3 + 4 x 2` -> expression evaluation (`x` is multiplication) -> `11`
- `a = 5` -> assignment, output shows `a = 5`; `a` is remembered
- `a x 2 + 1` -> uses the stored variable `a` -> `11`
- `y = 2x + 1` -> parsed as a line equation and drawn on the canvas
- `y = x + 5` -> coefficient defaults to 1
- `y = -2x - 3` -> negative coefficient/intercept supported
- `y = ax + b` (after `a` and `b` have been assigned) -> uses their values
## BNF grammar (for the report's Implementation section)
```
<statement> ::= <ident> "=" <expr> | <expr>
<expr> ::= <term> { ("+" | "-") <term> }
<term> ::= <factor> { ("x" | "/") <factor> }
<factor> ::= <number> | <ident> | "(" <expr> ")" | "-" <factor>
<number> ::= <integer> | <float>
<integer> ::= <digit> [<digit> [<digit>]] (1 to 3 digits)
<float> ::= <digit> [<digit>] "." <digit> [<digit>] (1-2 digits . 1-2 digits)
<ident> ::= <letter> [<alnum> [<alnum> [<alnum>]]] (max 4 characters)
<digit> ::= "0" | "1" | ... | "9"
<letter> ::= "a" | ... | "z" | "A" | ... | "Z"
<alnum> ::= <digit> | <letter>
```
Separate line-drawing syntax (parsed directly from raw characters in
`Engine.ParseLine`, since `x` is already the multiplication operator token
in the grammar above):
```
<line> ::= "y" "=" [ "-" ] [ <coefValue> ] ("x"|"X") ("+"|"-") <interceptValue>
<coefValue> ::= <number> | <ident>
<interceptValue> ::= <number> | <ident>
```
## Design notes (for the report)
- **No library parsing functions are used.** The tokenizer builds integer
and float values digit-by-digit using manual accumulator recursion
(see `buildIntValue` / `buildFracValue` in `Interpreter.fs`); it does not
call `Int32.Parse`, `Double.Parse`, `TryParse`, `String.Split`, or any
regular expression. Identifiers are built character-by-character with a
recursive `buildStr` helper instead of `Substring`.
- **No collection-library functions are used.** List reversal
(`reverseAcc`) and the variable environment lookup/update
(`lookupVar` / `updateEnv`) are hand-written recursive functions over a
plain association list, rather than `List.rev`, `Map`, or `Dictionary`.
- **The only library/framework usage is the WPF `Canvas` and shape classes**
(`Line`, `TextBlock`) in `MainWindow.xaml.cs`, which the assignment brief
explicitly permits.
- **Class diagram / sequence diagram**: the report should show
`MainWindow` -> `Engine.Evaluate` / `Engine.ParseLine` -> internal
`tokenize` -> `parseTokens` -> `evalExpr` pipeline for the class diagram,
and a sequence diagram for the line-drawing path:
`User -> MainWindow.RunInput -> Engine.ParseLine -> MainWindow.DrawLine -> Canvas`.
- **Line-drawing algorithm**: the line is drawn using two endpoint
coordinates computed analytically from `y = ax + b` at the left and right
edges of the visible canvas, converted from maths coordinates to pixel
coordinates via a fixed `Scale` (pixels per unit) and the canvas centre as
the origin, then rendered with WPF's built-in `Line` shape (this is the
permitted library use for the canvas itself - no custom rasterisation
algorithm like Bresenham is needed because WPF's vector `Line` element
handles anti-aliased rendering).
## Testing suggestions (for the report's Testing section)
Arithmetic:
- `1 + 2`, `10 - 4`, `6 x 7`, `20 / 4`, `(1 + 2) x 3`, `10 / (5 - 5)` (division by zero error)
- `999` (max 3-digit int, valid), `1000` (should error - exceeds 3 digits)
- `99.99` (max digits, valid), `100.5` (should error - integer part too long)
- `ab1` = `5` (valid 3-char identifier), `abcde` (should error - exceeds 4 chars)
Line drawing:
- `y = 2x + 1`, `y = -x + 3`, `y = 0.5x - 2`, `y = ax + b` after assigning `a` and `b`