Skip to content

Repository files navigation

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
  • checking source against the Cambridge 2027-2029 pseudocode guide

psei is not an official Cambridge tool. Its compliance profile checks plain-text source rules but cannot validate presentation details such as font choice. Its goal is to provide a useful, testable interpreter and checker for a Cambridge-style pseudocode subset.

Quick start

Install as a CLI tool:

pipx install psei

Or install with pip:

python -m pip install psei

Create hello.pseudo:

OUTPUT "Hello"

Run it:

pseudo run hello.pseudo

Contents


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

git clone <repo-url>
cd psei

2. Install the project environment

Install uv, then create and synchronize the project environment from the committed lock file:

uv sync --locked

uv creates .venv automatically and installs the project plus the default dev dependency group. There is no need to activate the environment; prefix project commands with uv run.

Python requirement:

Python 3.10 through 3.14

Command-line usage

The package provides two equivalent console commands:

pseudo
psei

Run a pseudocode file

pseudo run path/to/program.pseudo

Example:

pseudo run examples/passing/declare_assign_output.pseudo

You can also run it as a Python module:

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

Run with strict mode

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

Example:

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

Analyze without executing

Run the static semantic analyzer on a file:

pseudo analyze path/to/program.pseudo

Use --strict to require declarations before assignment, or --recommendations to report reads before explicit initialization:

pseudo analyze path/to/program.pseudo --strict --recommendations

Machine-readable output is available for editors and CI:

pseudo analyze path/to/program.pseudo --format json

Check Cambridge 2027 compliance

Check syntax and presentation without executing the program:

pseudo check path/to/program.pseudo

The default profile covers the Cambridge pseudocode guide for examinations in 2027, 2028 and 2029:

pseudo check path/to/program.pseudo --profile cambridge-2027

Use JSON output for editors, CI or other tools:

pseudo check path/to/program.pseudo --format json

Examination line numbers are detected automatically. Override detection when needed:

pseudo check path/to/program.pseudo --line-numbers present
pseudo check path/to/program.pseudo --line-numbers absent

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

pseudo check exits with status code 1 when it reports any error or warning. A compliant file exits with status code 0.

pseudo analyze exits with status code 1 for semantic errors. Warnings are reported but keep the exit status at 0.


REPL usage

Start the REPL:

pseudo repl

Or:

python -m psei repl

Start the REPL in strict mode:

pseudo repl --strict

Available REPL commands:

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

Example session:

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

from psei import run_source

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

run_source(source)

Output:

1

Capture OUTPUT

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

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

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

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.

Analyze source without executing it

from psei import analyze_file, analyze_source

report = analyze_source("""
DECLARE Count : INTEGER
Count ← "one"
""")

assert not report.valid
assert report.diagnostics[0].code == "SEM004"

file_report = analyze_file(
    "path/to/program.pseudo",
    strict=True,
    recommendations=True,
)

analyze_program() is also available when an application already has a parsed Program AST. Reports and diagnostics provide to_dict() for JSON-oriented integrations.


Pseudocode examples

Declaration, assignment and output

DECLARE Counter : INTEGER

Counter ← 0
Counter ← Counter + 1

OUTPUT Counter

Output:

1

Arrays and loops

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:

Total=20

IF statement

DECLARE Score : INTEGER

Score ← 75

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

Output:

Pass

CASE statement

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:

Distinction

WHILE loop

DECLARE Number : INTEGER

Number ← 27

WHILE Number > 9
   Number ← Number - 9
ENDWHILE

OUTPUT Number

Output:

9

REPEAT ... UNTIL loop

DECLARE Number : INTEGER

Number ← 0

REPEAT
   Number ← Number + 1
UNTIL Number = 3

OUTPUT Number

Output:

3

Strict mode

Strict mode is a runtime guardrail. Use pseudo check for Cambridge style and exam-format validation without executing the program.

Enable strict mode from the command line:

pseudo run program.pseudo --strict

Enable strict mode from 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

Static semantic analysis

The static analyzer parses and checks a complete program without executing statements, requesting input or opening pseudocode files. It resolves case-insensitive symbols and user-defined types before checking:

  • declarations, duplicate names and unknown identifiers or types
  • assignment, condition, operator and return-value types
  • array indices, record fields, object properties and pointer dereferences
  • procedure, function, method and constructor usage
  • argument count, BYVAL compatibility and writable BYREF lvalues
  • class inheritance, duplicate or private members and constructor form
  • whether every function control-flow path returns
  • statements that follow an unconditional return

Normal analysis mirrors non-strict execution by allowing an assignment to introduce an inferred variable. strict=True rejects that allowance. recommendations=True additionally emits SEM003 when a declared variable is read before an explicit assignment; this is advisory and is not a flow-sensitive definite-assignment proof.

Semantic diagnostics use these stable codes:

Code Severity Meaning
SEM001 error Undefined identifier
SEM002 error Duplicate declaration
SEM003 warning Read before explicit initialization
SEM004 error Incompatible assignment or constant mutation
SEM005 error Invalid condition or operator operand
SEM006 error Unknown or incorrectly used callable
SEM007 error Wrong argument count
SEM008 error Incompatible BYVAL argument
SEM009 error Invalid BYREF argument or address-of operand
SEM010 error Invalid member, index or pointer access
SEM011 error Function may finish without returning
SEM012 error RETURN outside a function
SEM013 error Incompatible return value
SEM014 warning Unreachable statement
SEM015 error Unknown or invalid type relationship

Warnings do not make a SemanticReport invalid. The analyzer remains conservative about value-dependent behavior: array bounds, division by zero, file state, pointer initialization and similar facts are still checked at runtime.


Cambridge 2027 compliance checking

The cambridge-2027 profile checks source code against the Cambridge International AS & A Level Computer Science 9618 pseudocode guide for examinations in 2027, 2028 and 2029.

The detailed Cambridge 2027-2029 conformance matrix marks each behavior as formal syntax, a recommendation, a compatibility allowance or a documented psei extension.

It performs static checking only. It parses the source but never executes it, reads pseudocode input or opens pseudocode files.

The profile currently checks:

  • upper-case Cambridge keywords and standard function names
  • three-space structural indentation and tab usage
  • the Cambridge assignment operator
  • ASCII identifier characters
  • consistent case-insensitive identifier spelling
  • lexer and parser compatibility with the formal guide syntax
  • examination line numbers, including optional preprocessing before parsing
  • uses of documented psei operations that are not defined by the guide
  • normal-mode allowances for blank OUTPUT, expression CASE selectors and arrays with more than two dimensions
  • the guide's page 19 CALL Beep inconsistency against the formal CALL Beep() grammar in section 8.1
  • semantic diagnostics for names, types, calls, members and return paths

Diagnostics have stable codes, error or warning severity, and one of four categories:

Category Meaning
formal An explicit Cambridge syntax or presentation rule
recommendation Guidance described as usual or good practice
compatibility Syntax accepted for normal psei compatibility but not the formal Cambridge format
extension A documented psei feature outside the guide
Code Category Meaning
C2027-A001 compatibility Non-Cambridge assignment operator
C2027-C001 compatibility Procedure call missing formal parentheses
C2027-I001 recommendation Tab used for indentation
C2027-I002 recommendation Structural indentation differs from three-space nesting
C2027-ID001 formal Inconsistent case-insensitive identifier spelling
C2027-ID002 formal Non-ASCII identifier character
C2027-K001 formal Keyword or standard function name is not upper-case
C2027-L001 formal Other lexical error
C2027-N001 formal Line numbers do not increase
C2027-P001 formal Parser error against the formal syntax
C2027-X001 extension Documented psei operation outside the guide
C2027-X002 compatibility OUTPUT has no value
C2027-X003 compatibility CASE OF uses an expression instead of an identifier
C2027-X004 compatibility Array declaration has more than two dimensions

Warnings are compliance failures but do not imply that normal, non-strict execution would fail. For example, normal execution accepts lower-case keywords while the compliance profile reports them. Text diagnostics include the category in square brackets, and JSON diagnostics expose it as the category field.

Use the checker from Python:

from psei import check_file, check_source

report = check_source("""
DECLARE Count : INTEGER
Count ← 1
OUTPUT Count
""")

assert report.compliant

file_report = check_file("program.pseudo")

for diagnostic in file_report.diagnostics:
    print(diagnostic.format("program.pseudo"))

line_numbers can be set to "auto", "present" or "absent" in the Python API. The default is "auto". Semantic findings retain their SEM### codes and use the formal compliance category.


Resource limits

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

Default limits:

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:

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:

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:

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

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:

First
Second

Random file example

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:

Johnson:6

Random files can store:

  • scalar values
  • arrays
  • records
  • sets

Random files cannot store:

  • object instances
  • pointer values

Local random files use the deterministic, versioned psei-random-v1 JSON format. Persisted addresses, types, record fields, array elements and scalar values are validated when a file is opened; malformed data raises PseudoRuntimeError. Non-finite REAL values cannot be persisted.


User-defined types

Enumerated types

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

DECLARE ThisSeason : Season

ThisSeason ← Summer

OUTPUT ThisSeason

Output:

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

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:

Johnson,Leroy,6

Record assignment

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

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:

6
7

Arrays of records

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:

Ali:12
Mei:11

Pointer types

TYPE TIntPointer = ^INTEGER

DECLARE X : INTEGER
DECLARE P : TIntPointer

X ← 10
P ← ^X

OUTPUT P^

P^ ← 20

OUTPUT X

Output:

10
20

Set types

TYPE LetterSet = SET OF CHAR

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

OUTPUT Vowels

Output:

{A, E, I, O, U}

DEFINE creates a constant set. Use DECLARE when the set needs to be changed:

DECLARE Selected : LetterSet

Selected ← Vowels
CALL SETADD(Selected, 'Y')
CALL SETREMOVE(Selected, 'A')

Set assignment uses copy semantics. The operations below return a new set and do not change either operand:

Combined ← UNION(SetA, SetB)
Shared ← INTERSECTION(SetA, SetB)
OnlyA ← DIFFERENCE(SetA, SetB)
EitherButNotBoth ← SYMMETRICDIFFERENCE(SetA, SetB)

Set query functions:

Function Result
CONTAINS(SetValue, Element) Whether the element belongs to the set
CARDINALITY(SetValue) Number of distinct elements
ISEMPTY(SetValue) Whether the set is empty
ISSUBSET(SetA, SetB) Whether every element of SetA is in SetB
ISPROPERSUBSET(SetA, SetB) Whether SetA is a strict subset of SetB
ISSUPERSET(SetA, SetB) Whether SetA contains every element of SetB
ISPROPERSUPERSET(SetA, SetB) Whether SetA is a strict superset of SetB
ISDISJOINT(SetA, SetB) Whether the sets have no elements in common

Set mutation procedures:

Procedure Effect
CALL SETADD(SetValue, Element) Adds an element; existing elements are unchanged
CALL SETREMOVE(SetValue, Element) Removes an element, or raises an error if absent
CALL SETDISCARD(SetValue, Element) Removes an element if present
CALL SETCLEAR(SetValue) Removes all elements

The Cambridge pseudocode guide does not define standard notation for these operations. These named functions and procedures are a documented psei extension.


Procedures and functions

Procedure without parameters

PROCEDURE Hello()
   OUTPUT "Hello"
ENDPROCEDURE

CALL Hello()

Output:

Hello

Procedure with parameters

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.

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

DECLARE A : INTEGER

A ← 5

CALL AddOne(A)

OUTPUT A

Output:

5

BYREF

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

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

DECLARE A : INTEGER

A ← 5

CALL AddOne(A)

OUTPUT A

Output:

6

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

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:

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

Functions

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:

Maximum=20

Function calls must be used as part of an expression.

Valid:

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

Invalid:

Max(10, 20)

Function parameters cannot be passed BYREF.


Object-oriented pseudocode

Basic class

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:

3
5

Constructors

Constructors are procedures named NEW.

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:

Kitty

Inheritance and SUPER

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:

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:

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:

100

This external access raises a runtime error:

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
UNION(SetA, SetB) Returns the union of two sets
INTERSECTION(SetA, SetB) Returns the intersection of two sets
DIFFERENCE(SetA, SetB) Returns the elements in SetA but not SetB
SYMMETRICDIFFERENCE(SetA, SetB) Returns elements in exactly one operand
CONTAINS(SetValue, Element) Tests set membership
CARDINALITY(SetValue) Returns the number of elements
ISEMPTY(SetValue) Tests whether a set is empty
ISSUBSET(SetA, SetB) Tests subset inclusion
ISPROPERSUBSET(SetA, SetB) Tests strict subset inclusion
ISSUPERSET(SetA, SetB) Tests superset inclusion
ISPROPERSUPERSET(SetA, SetB) Tests strict superset inclusion
ISDISJOINT(SetA, SetB) Tests whether two sets are disjoint

Example:

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

Output:

FGH
BCD
10
H
w
27

Errors

Error classes are available from psei.errors:

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:

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

Create or update the locked development environment:

uv sync --locked

Run tests:

uv run --locked pytest -q

Run the coverage baseline used by CI:

uv run --locked pytest --cov=psei --cov-branch --cov-report=term-missing \
  --cov-fail-under=80

Run the CLI from the project environment:

uv run --locked pseudo run examples/passing/declare_assign_output.pseudo

The committed .python-version selects Python 3.14 for local development, while the package and CI remain compatible with Python 3.10 through 3.14.

In the Codex workspace sandbox, the default user cache may be read-only. Use a temporary writable cache and copy mode when synchronizing or running commands:

UV_CACHE_DIR=/tmp/psei-uv-cache UV_LINK_MODE=copy uv sync --locked
UV_CACHE_DIR=/tmp/psei-uv-cache UV_LINK_MODE=copy uv run --locked pytest -q

The first synchronization may also require sandbox network approval to download locked packages from PyPI.

The repository includes example programs:

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

psei/
├── examples/
│   ├── passing/
│   └── errors/
├── src/
│   └── psei/
│       ├── lexer.py
│       ├── parser.py
│       ├── ast_nodes.py
│       ├── analyzer.py
│       ├── compliance.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
analyzer.py Non-executing static semantic analysis
compliance.py Cambridge compliance profiles and diagnostics
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 and a Cambridge 2027-2029 source compliance profile. It is not an official Cambridge tool or a complete programming language implementation.

Not fully implemented:

  • the full ADT library mentioned by the Cambridge syllabus, including:
    • stack
    • queue
    • linked list
    • dictionary
    • binary tree
  • presentation checks that cannot be inferred reliably from plain text, such as font choice and the alignment of wrapped continuation lines
  • a prescriptive camelCase/PascalCase identifier-name checker; the current profile checks ASCII characters and consistent case-insensitive spelling
  • flow-sensitive definite-assignment and interprocedural data-flow 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:

DECLARE Name : STRING

Name ← "Cambridge pseudocode"

OUTPUT "Hello, ", Name

Run it:

pseudo run hello.pseudo

Output:

Hello, Cambridge pseudocode

About

Minimal Cambridge-style pseudocode interpreter. This project implements a practical subset of Cambridge International AS & A Level Computer Science 9618-style pseudocode.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages