commit
This commit is contained in:
@@ -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.
Binary file not shown.
Binary file not shown.
@@ -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>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace InterpreterGui
|
||||||
|
{
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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`
|
||||||
Reference in New Issue
Block a user