Metadata-Version: 2.4
Name: psei
Version: 0.1.0
Summary: Minimal Cambridge-style pseudocode interpreter
Author: Luke Tang
License: MIT
Project-URL: Homepage, https://github.com/luke-tangh/psei
Project-URL: Repository, https://github.com/luke-tangh/psei
Project-URL: Issues, https://github.com/luke-tangh/psei/issues
Keywords: pseudocode,interpreter,education,computer-science,cambridge
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Education
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Education
Classifier: Topic :: Software Development :: Interpreters
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Dynamic: license-file

# psei

`psei` is a lightweight interpreter for Cambridge International AS & A Level Computer Science 9618-style pseudocode.

It implements a practical subset of Cambridge-style pseudocode and can be used for:

- running `.pseudo` files locally
- experimenting with pseudocode in a REPL
- executing pseudocode from Python tests or applications
- building teaching examples
- checking common runtime and type errors

> `psei` is not an official Cambridge tool. It is also not a full exam-format validator. Its goal is to provide a useful, testable interpreter for a Cambridge-style pseudocode subset.

## Quick start

Install as a CLI tool:

```bash
pipx install psei
```

Or install with pip:

```bash
python -m pip install psei
```

Create `hello.pseudo`:

```text
OUTPUT "Hello"
```

Run it:

```bash
pseudo run hello.pseudo
```

---

## Contents

- [Features](#features)
- [Installation](#installation)
- [Command-line usage](#command-line-usage)
- [REPL usage](#repl-usage)
- [Python API usage](#python-api-usage)
- [Pseudocode examples](#pseudocode-examples)
- [Strict mode](#strict-mode)
- [Resource limits](#resource-limits)
- [File handling](#file-handling)
- [User-defined types](#user-defined-types)
- [Procedures and functions](#procedures-and-functions)
- [Object-oriented pseudocode](#object-oriented-pseudocode)
- [Built-in functions](#built-in-functions)
- [Errors](#errors)
- [Development](#development)
- [Project structure](#project-structure)
- [Current limitations](#current-limitations)

---

## Features

### Basic language features

Supported:

- `DECLARE`
- `CONSTANT`
- assignment using `←`
- `INPUT`
- `OUTPUT`
- comments using `//`

### Basic data types

Supported data types:

- `INTEGER`
- `REAL`
- `CHAR`
- `STRING`
- `BOOLEAN`
- `DATE`

### Expressions and operators

Arithmetic operators:

- `+`
- `-`
- `*`
- `/`
- `DIV`
- `MOD`

Comparison operators:

- `=`
- `<>`
- `<`
- `<=`
- `>`
- `>=`

Logic operators:

- `AND`
- `OR`
- `NOT`

String concatenation:

- `&`

`AND` and `OR` use short-circuit evaluation.

### Selection and iteration

Supported control structures:

- `IF ... THEN ... ELSE ... ENDIF`
- `CASE OF ... OTHERWISE ... ENDCASE`
- `FOR ... TO ... STEP ... NEXT`
- `WHILE ... ENDWHILE`
- `REPEAT ... UNTIL`

### Arrays

Supported:

- one-dimensional arrays
- two-dimensional arrays
- explicit lower and upper bounds
- bounds checking
- whole-array assignment with copy semantics

### User-defined types

Supported:

- enumerated types
- pointer types
- set types
- record types
- class/object types

### Procedures and functions

Supported:

- `PROCEDURE`
- `FUNCTION`
- `CALL`
- `RETURN`
- `BYVAL`
- `BYREF`

### File handling

Supported text file operations:

- `OPENFILE ... FOR READ`
- `OPENFILE ... FOR WRITE`
- `OPENFILE ... FOR APPEND`
- `READFILE`
- `WRITEFILE`
- `CLOSEFILE`
- `EOF(...)`

Supported random file operations:

- `OPENFILE ... FOR RANDOM`
- `SEEK`
- `GETRECORD`
- `PUTRECORD`

### Object-oriented subset

Supported:

- `CLASS ... ENDCLASS`
- `PUBLIC`
- `PRIVATE`
- `INHERITS`
- `SUPER`
- constructors using `PROCEDURE NEW(...)`
- object creation using `NEW ClassName(...)`
- method calls using `Object.Method(...)`

---

## Installation

### 1. Clone the repository

```bash
git clone <repo-url>
cd psei
```

### 2. Create a virtual environment

Linux / macOS:

```bash
python -m venv .venv
source .venv/bin/activate
```

Windows PowerShell:

```powershell
python -m venv .venv
.venv\Scripts\Activate.ps1
```

### 3. Install the package

For normal use:

```bash
python -m pip install --upgrade pip
python -m pip install -e .
```

For development:

```bash
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
```

Python requirement:

```text
Python >= 3.10
```

---

## Command-line usage

The package provides two equivalent console commands:

```bash
pseudo
psei
```

### Run a pseudocode file

```bash
pseudo run path/to/program.pseudo
```

Example:

```bash
pseudo run examples/passing/declare_assign_output.pseudo
```

You can also run it as a Python module:

```bash
python -m psei run examples/passing/declare_assign_output.pseudo
```

### Run with strict mode

```bash
pseudo run path/to/program.pseudo --strict
```

Example:

```bash
pseudo run examples/errors/strict_ascii_assignment.pseudo --strict
```

### CLI error behavior

If a program produces a lexical, parse or runtime error:

- the error message is written to `stderr`
- the process exits with status code `1`

---

## REPL usage

Start the REPL:

```bash
pseudo repl
```

Or:

```bash
python -m psei repl
```

Start the REPL in strict mode:

```bash
pseudo repl --strict
```

Available REPL commands:

```text
:help     show help
:vars     show variables in the current runtime
:reset    reset the runtime
:quit     exit
:exit     exit
```

Example session:

```text
pseudo> DECLARE X : INTEGER
pseudo> X ← 10
pseudo> OUTPUT X + 5
15
pseudo> :vars
X : INTEGER = 10
pseudo> :quit
```

For multi-line constructs such as `IF`, `WHILE`, `PROCEDURE`, `FUNCTION` and `CLASS`, the REPL waits until the block is complete.

---

## Python API usage

`psei` can also be used as a Python library.

### Run source code from a string

```python
from psei import run_source

source = """
DECLARE Counter : INTEGER
Counter ← 0
Counter ← Counter + 1
OUTPUT Counter
"""

run_source(source)
```

Output:

```text
1
```

### Capture `OUTPUT`

By default, `OUTPUT` uses Python's `print`. To capture output in tests, create a custom `Runtime`.

```python
from psei import Runtime, run_source

output = []

runtime = Runtime(output_writer=output.append)

run_source("""
OUTPUT "Hello"
OUTPUT "World"
""", runtime)

assert output == ["Hello", "World"]
```

### Provide `INPUT`

```python
from psei import Runtime, run_source

inputs = iter(["41"])
output = []

runtime = Runtime(
    input_provider=lambda: next(inputs),
    output_writer=output.append,
)

run_source("""
DECLARE X : INTEGER

INPUT X
OUTPUT X + 1
""", runtime)

assert output == ["42"]
```

### Run a file

```python
from psei import run_file

run_file("path/to/program.pseudo")
```

`run_file()` uses a local file system rooted at the directory containing the pseudocode file.

---

## Pseudocode examples

### Declaration, assignment and output

```text
DECLARE Counter : INTEGER

Counter ← 0
Counter ← Counter + 1

OUTPUT Counter
```

Output:

```text
1
```

---

### Arrays and loops

```text
DECLARE Values : ARRAY[1:4] OF INTEGER
DECLARE I : INTEGER
DECLARE Total : INTEGER

Total ← 0

FOR I ← 1 TO 4
   Values[I] ← I * 2
   Total ← Total + Values[I]
NEXT I

OUTPUT "Total=", Total
```

Output:

```text
Total=20
```

---

### `IF` statement

```text
DECLARE Score : INTEGER

Score ← 75

IF Score >= 50 THEN
   OUTPUT "Pass"
ELSE
   OUTPUT "Fail"
ENDIF
```

Output:

```text
Pass
```

---

### `CASE` statement

```text
DECLARE Mark : INTEGER

Mark ← 75

CASE OF Mark
   0 TO 49 : OUTPUT "Fail"
   50 TO 69 : OUTPUT "Pass"
   70 TO 100 : OUTPUT "Distinction"
   OTHERWISE : OUTPUT "Invalid"
ENDCASE
```

Output:

```text
Distinction
```

---

### `WHILE` loop

```text
DECLARE Number : INTEGER

Number ← 27

WHILE Number > 9
   Number ← Number - 9
ENDWHILE

OUTPUT Number
```

Output:

```text
9
```

---

### `REPEAT ... UNTIL` loop

```text
DECLARE Number : INTEGER

Number ← 0

REPEAT
   Number ← Number + 1
UNTIL Number = 3

OUTPUT Number
```

Output:

```text
3
```

---

## Strict mode

Strict mode is a limited Cambridge-style guardrail. It is not a complete style or exam-format validator.

Enable strict mode from the command line:

```bash
pseudo run program.pseudo --strict
```

Enable strict mode from Python:

```python
from psei import Runtime, run_source

runtime = Runtime(strict=True)

run_source("""
DECLARE X : INTEGER
X ← 1
""", runtime, strict=True)
```

Strict mode currently enforces:

- assignment must use `←`
- ASCII assignment `<-` is rejected
- variables must be declared before assignment
- identifiers may contain only ASCII letters, digits and `_`
- identifiers must start with an ASCII letter

Non-strict mode currently allows:

- assignment using either `←` or `<-`
- assignment to undeclared variables, creating them with inferred types
- non-ASCII alphabetic characters in identifiers

Both modes still perform core runtime checks, including:

- assignment type checks
- constant immutability
- array bounds checks
- unknown type checks
- record field checks
- enumerated type checks
- file mode checks
- division by zero checks
- Boolean condition checks
- procedure/function arity checks
- function return type checks
- `BYREF` lvalue and type checks

---

## Resource limits

`Runtime` applies conservative execution limits by default to protect the interpreter from runaway programs.

Default limits:

```python
Runtime(
    max_steps=1_000_000,
    max_array_elements=1_000_000,
    max_call_depth=1_000,
    max_output_chars=1_000_000,
)
```

| Option | Purpose |
|---|---|
| `max_steps` | Limits executed statements and loop iterations |
| `max_array_elements` | Limits the number of elements in a single array |
| `max_call_depth` | Limits procedure, function and method call depth |
| `max_output_chars` | Limits the total number of output characters |

Example:

```python
from psei import Runtime, run_source
from psei.errors import PseudoRuntimeError

runtime = Runtime(max_steps=1000)

try:
    run_source("""
WHILE TRUE
ENDWHILE
""", runtime)
except PseudoRuntimeError as error:
    print(error)
```

To disable a specific limit, pass `None`:

```python
runtime = Runtime(max_steps=None)
```

> These limits are not a full security sandbox. If you run untrusted code in production, also use process-level timeouts, memory limits, containers or operating-system sandboxing.

---

## File handling

### File handling with `run_source()`

`run_source()` uses an in-memory file system by default.

This means:

- no real files are created
- execution is deterministic
- tests and REPL usage are easier to manage

Example:

```text
DECLARE Line : STRING

OPENFILE "Log.txt" FOR WRITE
WRITEFILE "Log.txt", "Hello"
CLOSEFILE "Log.txt"

OPENFILE "Log.txt" FOR READ
READFILE "Log.txt", Line
OUTPUT Line
CLOSEFILE "Log.txt"
```

---

### File handling with `run_file()`

`run_file()` uses a local file system.

Important behavior:

- relative paths are resolved beside the pseudocode source file
- absolute paths are rejected
- paths escaping the program directory are rejected
- text files are read and written as UTF-8
- random files are persisted as JSON

---

### Text file example

```text
DECLARE LineOfText : STRING

OPENFILE "FileA.txt" FOR WRITE
WRITEFILE "FileA.txt", "First"
WRITEFILE "FileA.txt", "Second"
CLOSEFILE "FileA.txt"

OPENFILE "FileA.txt" FOR READ

WHILE NOT EOF("FileA.txt")
   READFILE "FileA.txt", LineOfText
   OUTPUT LineOfText
ENDWHILE

CLOSEFILE "FileA.txt"
```

Output:

```text
First
Second
```

---

### Random file example

```text
TYPE StudentRecord
   DECLARE LastName : STRING
   DECLARE YearGroup : INTEGER
ENDTYPE

DECLARE Pupil : StudentRecord
DECLARE Loaded : StudentRecord

Pupil.LastName ← "Johnson"
Pupil.YearGroup ← 6

OPENFILE "StudentFile.Dat" FOR RANDOM

SEEK "StudentFile.Dat", 10
PUTRECORD "StudentFile.Dat", Pupil

SEEK "StudentFile.Dat", 10
GETRECORD "StudentFile.Dat", Loaded

CLOSEFILE "StudentFile.Dat"

OUTPUT Loaded.LastName, ":", Loaded.YearGroup
```

Output:

```text
Johnson:6
```

Random files can store:

- scalar values
- arrays
- records
- sets

Random files cannot store:

- object instances
- pointer values

---

## User-defined types

### Enumerated types

```text
TYPE Season = (Spring, Summer, Autumn, Winter)

DECLARE ThisSeason : Season

ThisSeason ← Summer

OUTPUT ThisSeason
```

Output:

```text
Summer
```

Enumerated values are case-insensitive.

If a variable has the same name as an enumerated value, the variable shadows the enumerated value.

---

### Record types

```text
TYPE StudentRecord
   DECLARE LastName : STRING
   DECLARE FirstName : STRING
   DECLARE YearGroup : INTEGER
ENDTYPE

DECLARE Pupil : StudentRecord

Pupil.LastName ← "Johnson"
Pupil.FirstName ← "Leroy"
Pupil.YearGroup ← 6

OUTPUT Pupil.LastName, ",", Pupil.FirstName, ",", Pupil.YearGroup
```

Output:

```text
Johnson,Leroy,6
```

---

### Record assignment

Record assignment uses copy semantics. Assigning one record to another does not alias their fields.

```text
TYPE StudentRecord
   DECLARE LastName : STRING
   DECLARE YearGroup : INTEGER
ENDTYPE

DECLARE Pupil1 : StudentRecord
DECLARE Pupil2 : StudentRecord

Pupil1.LastName ← "Johnson"
Pupil1.YearGroup ← 6

Pupil2 ← Pupil1

Pupil1.YearGroup ← 7

OUTPUT Pupil2.YearGroup
OUTPUT Pupil1.YearGroup
```

Output:

```text
6
7
```

---

### Arrays of records

```text
TYPE StudentRecord
   DECLARE Name : STRING
   DECLARE YearGroup : INTEGER
ENDTYPE

DECLARE Form : ARRAY[1:2] OF StudentRecord

Form[1].Name ← "Ali"
Form[1].YearGroup ← 12

Form[2].Name ← "Mei"
Form[2].YearGroup ← 11

OUTPUT Form[1].Name, ":", Form[1].YearGroup
OUTPUT Form[2].Name, ":", Form[2].YearGroup
```

Output:

```text
Ali:12
Mei:11
```

---

### Pointer types

```text
TYPE TIntPointer = ^INTEGER

DECLARE X : INTEGER
DECLARE P : TIntPointer

X ← 10
P ← ^X

OUTPUT P^

P^ ← 20

OUTPUT X
```

Output:

```text
10
20
```

---

### Set types

```text
TYPE LetterSet = SET OF CHAR

DEFINE Vowels ('A','E','I','O','U') : LetterSet

OUTPUT Vowels
```

Sets currently support declaration, definition, assignment and placeholder output. A full set operation library is not implemented yet.

---

## Procedures and functions

### Procedure without parameters

```text
PROCEDURE Hello()
   OUTPUT "Hello"
ENDPROCEDURE

CALL Hello()
```

Output:

```text
Hello
```

---

### Procedure with parameters

```text
PROCEDURE Square(Size : INTEGER)
   FOR Side ← 1 TO 4
      OUTPUT "Side length=", Size
   NEXT Side
ENDPROCEDURE

CALL Square(100)
```

---

### `BYVAL`

Parameters are passed by value by default.

```text
PROCEDURE AddOne(X : INTEGER)
   X ← X + 1
ENDPROCEDURE

DECLARE A : INTEGER

A ← 5

CALL AddOne(A)

OUTPUT A
```

Output:

```text
5
```

---

### `BYREF`

`BYREF` parameters modify the caller's variable, array element, record field, object property or pointer dereference.

```text
PROCEDURE AddOne(BYREF X : INTEGER)
   X ← X + 1
ENDPROCEDURE

DECLARE A : INTEGER

A ← 5

CALL AddOne(A)

OUTPUT A
```

Output:

```text
6
```

The current passing mode continues across comma-separated parameters until another `BYVAL` or `BYREF` keyword appears.

```text
PROCEDURE Swap(BYREF X : INTEGER, Y : INTEGER)
   DECLARE Temp : INTEGER

   Temp ← X
   X ← Y
   Y ← Temp
ENDPROCEDURE
```

In the example above, both `X` and `Y` are passed by reference.

To reset the mode explicitly:

```text
PROCEDURE Test(BYREF X : INTEGER, BYVAL Y : INTEGER)
   X ← 10
   Y ← 20
ENDPROCEDURE
```

---

### Functions

```text
FUNCTION Max(Number1 : INTEGER, Number2 : INTEGER) RETURNS INTEGER
   IF Number1 > Number2 THEN
      RETURN Number1
   ELSE
      RETURN Number2
   ENDIF
ENDFUNCTION

OUTPUT "Maximum=", Max(10, 20)
```

Output:

```text
Maximum=20
```

Function calls must be used as part of an expression.

Valid:

```text
OUTPUT Max(10, 20)
X ← Max(10, 20)
```

Invalid:

```text
Max(10, 20)
```

Function parameters cannot be passed `BYREF`.

---

## Object-oriented pseudocode

### Basic class

```text
CLASS Player
   PRIVATE Attempts : INTEGER

   Attempts ← 3

   PUBLIC PROCEDURE SetAttempts(Number : INTEGER)
      Attempts ← Number
   ENDPROCEDURE

   PUBLIC FUNCTION GetAttempts() RETURNS INTEGER
      RETURN Attempts
   ENDFUNCTION
ENDCLASS

DECLARE P : Player

P ← NEW Player()

OUTPUT P.GetAttempts()

P.SetAttempts(5)

OUTPUT P.GetAttempts()
```

Output:

```text
3
5
```

---

### Constructors

Constructors are procedures named `NEW`.

```text
CLASS Pet
   PRIVATE Name : STRING

   PUBLIC PROCEDURE NEW(GivenName : STRING)
      Name ← GivenName
   ENDPROCEDURE

   PUBLIC FUNCTION GetName() RETURNS STRING
      RETURN Name
   ENDFUNCTION
ENDCLASS

MyPet ← NEW Pet("Kitty")

OUTPUT MyPet.GetName()
```

Output:

```text
Kitty
```

---

### Inheritance and `SUPER`

```text
CLASS Pet
   PRIVATE Name : STRING

   PUBLIC PROCEDURE NEW(GivenName : STRING)
      Name ← GivenName
   ENDPROCEDURE

   PUBLIC FUNCTION GetName() RETURNS STRING
      RETURN Name
   ENDFUNCTION
ENDCLASS

CLASS Cat INHERITS Pet
   PRIVATE Breed : STRING

   PUBLIC PROCEDURE NEW(GivenName : STRING, GivenBreed : STRING)
      SUPER.NEW(GivenName)
      Breed ← GivenBreed
   ENDPROCEDURE

   PUBLIC FUNCTION GetBreed() RETURNS STRING
      RETURN Breed
   ENDFUNCTION
ENDCLASS

MyCat ← NEW Cat("Kitty", "Shorthaired")

OUTPUT MyCat.GetName()
OUTPUT MyCat.GetBreed()
```

Output:

```text
Kitty
Shorthaired
```

---

### `PUBLIC` and `PRIVATE`

`PUBLIC` members can be accessed from outside the object.

`PRIVATE` members can only be accessed from methods or initializers of the class that declares them.

Example:

```text
CLASS Account
   PRIVATE Balance : INTEGER

   PUBLIC PROCEDURE NEW(StartBalance : INTEGER)
      Balance ← StartBalance
   ENDPROCEDURE

   PUBLIC FUNCTION GetBalance() RETURNS INTEGER
      RETURN Balance
   ENDFUNCTION
ENDCLASS

A ← NEW Account(100)

OUTPUT A.GetBalance()
```

Output:

```text
100
```

This external access raises a runtime error:

```text
OUTPUT A.Balance
```

---

## Built-in functions

Supported built-in functions:

| Function | Description |
|---|---|
| `RIGHT(ThisString, x)` | Returns the rightmost `x` characters |
| `MID(ThisString, x, y)` | Returns a substring of length `y` starting at one-based position `x` |
| `LENGTH(ThisString)` | Returns the length of a string |
| `LCASE(ThisChar)` | Converts ASCII uppercase letters to lowercase; other characters are unchanged |
| `UCASE(ThisChar)` | Converts ASCII lowercase letters to uppercase; other characters are unchanged |
| `INT(x)` | Returns the integer part of a number |
| `RAND(x)` | Returns a random `REAL` in the range `[0, x)` |
| `EOF(file)` | Returns whether an open text file has reached end-of-file |

Example:

```text
OUTPUT RIGHT("ABCDEFGH", 3)
OUTPUT MID("ABCDEFGH", 2, 3)
OUTPUT LENGTH("Happy Days")
OUTPUT UCASE('h')
OUTPUT LCASE('W')
OUTPUT INT(27.5415)
```

Output:

```text
FGH
BCD
10
H
w
27
```

---

## Errors

Error classes are available from `psei.errors`:

```python
from psei.errors import (
    PseudoError,
    LexError,
    ParseError,
    IncompleteInput,
    PseudoRuntimeError,
)
```

| Error type | Meaning |
|---|---|
| `LexError` | Lexical error, such as an invalid character or malformed literal |
| `ParseError` | Syntax error |
| `IncompleteInput` | Used by the REPL when a block is incomplete |
| `PseudoRuntimeError` | Runtime error, such as type mismatch, division by zero or array bounds error |

Example:

```python
from psei import run_source
from psei.errors import PseudoError

try:
    run_source("""
DECLARE X : INTEGER
X ← "not an integer"
""")
except PseudoError as error:
    print(error)
```

---

## Development

Install development dependencies:

```bash
python -m pip install -e ".[dev]"
```

Run tests:

```bash
python -m pytest -q
```

The repository includes example programs:

```text
examples/passing/
examples/errors/
```

`examples/passing/` contains programs that should run successfully.

Each passing example has a matching `.out` file containing expected output.

`examples/errors/` contains programs that should raise errors.

`examples/errors/manifest.json` records the expected error type for each error example.

---

## Project structure

```text
psei/
├── examples/
│   ├── passing/
│   └── errors/
├── src/
│   └── psei/
│       ├── lexer.py
│       ├── parser.py
│       ├── ast_nodes.py
│       ├── interpreter.py
│       ├── runner.py
│       ├── cli.py
│       ├── repl.py
│       ├── runtime/
│       │   ├── core.py
│       │   ├── environment.py
│       │   ├── files.py
│       │   ├── oop.py
│       │   ├── serialization.py
│       │   ├── types.py
│       │   └── values.py
│       ├── tokens.py
│       └── values.py
├── tests/
├── pyproject.toml
└── README.md
```

Main modules:

| File or directory | Purpose |
|---|---|
| `lexer.py` | Lexical analysis |
| `parser.py` | Parsing and AST construction |
| `ast_nodes.py` | AST node definitions |
| `interpreter.py` | AST execution |
| `runtime/core.py` | Runtime object, scopes and limits |
| `runtime/environment.py` | Variables, constants and references |
| `runtime/types.py` | Type system, coercion and cloning |
| `runtime/files.py` | Text and random file abstractions |
| `runtime/oop.py` | Class and object runtime structures |
| `runner.py` | `run_source()` and `run_file()` |
| `cli.py` | Command-line entry point |
| `repl.py` | Interactive REPL |

---

## Current limitations

`psei` implements a practical Cambridge-style pseudocode subset. It is not a complete programming language implementation or a full Cambridge exam-format checker.

Not fully implemented:

- the full ADT library mentioned by the Cambridge syllabus, including:
  - stack
  - queue
  - linked list
  - dictionary
  - binary tree
- full style validation, such as:
  - checking that keywords are uppercase
  - checking indentation
  - checking mixed-case identifier style
- a complete set operation library
- full compiler-style static analysis
- process-level sandboxing

If you execute untrusted code, consider using:

- subprocess timeouts
- operating-system memory limits
- containers
- API-level request limits
- process isolation

---

## Minimal example

Create `hello.pseudo`:

```text
DECLARE Name : STRING

Name ← "Cambridge pseudocode"

OUTPUT "Hello, ", Name
```

Run it:

```bash
pseudo run hello.pseudo
```

Output:

```text
Hello, Cambridge pseudocode
```
