This guide covers everything you need to start contributing to PostForge. Each section gives a quick summary with links to the detailed guides.
Clone and install:
git clone https://github.com/AndyCappDev/postforge.git
cd postforge
./install.shThe install script checks for Python 3.13+ and Cairo, creates a virtual
environment, installs dependencies, and sets up the pf command. Verify the
installation:
pf samples/tiger.ps # Opens a Qt window with the rendered tigerIf you prefer not to install the system-wide pf command, you can use the
launcher script or module directly:
./postforge.sh samples/tiger.ps # Launcher script (activates venv)
python -m postforge samples/tiger.ps # After pip install -e .| Directory | Purpose |
|---|---|
postforge/core/ |
PostScript execution infrastructure (types, tokenizer, error handling, color spaces) |
postforge/operators/ |
PostScript language operators organized by functional area |
postforge/devices/ |
Output devices (PNG, PDF, SVG, TIFF, Qt) |
postforge/utils/ |
System utilities (memory analysis, profiling) |
postforge/resources/ |
PostScript resource files (fonts, encodings, initialization scripts, device configs) |
unit_tests/ |
PostScript-based test suite |
See Architecture Overview for a full description of the execution engine, type system, memory model, and rendering pipeline.
- Relative imports within the
postforge/package:from ..core import types as ps - PEP 8 ordering: standard library, third-party, then local imports
- All imports at the top of the file — never import inside functions
Operator functions always receive the context first:
def operator_name(ctxt, ostack):
...
def operator_with_estack(ctxt, e_stack, ostack):
...All Python files use from __future__ import annotations and follow Python 3.13
style. Since future annotations are lazy strings, they have zero runtime cost and
no forward-reference issues.
Rules:
| Rule | Example |
|---|---|
from __future__ import annotations first code line |
After copyright header |
| Lowercase builtins | list[str], dict[str, Any], tuple[int, ...] |
| Union syntax | X | None not Optional[X] |
Minimal typing imports |
Only Any, Callable, ClassVar, TYPE_CHECKING |
__copy__ return type |
def __copy__(self) -> MyClass: |
| Operator signature | def op_name(ctxt: ps.Context, ostack: ps.Stack) -> None: |
| All functions annotated | Public and private, parameters and return types |
Before:
from typing import Dict, List, Optional, Union
def process(items: List[str], config: Optional[Dict[str, int]] = None) -> Union[str, None]:
...After:
from __future__ import annotations
from typing import Any
def process(items: list[str], config: dict[str, int] | None = None) -> str | None:
...Cython .pyx files are excluded (different typing semantics).
PostForge implements PostScript Level 3, which is a strict superset of Level 2. All changes must preserve backward compatibility with Level 2 programs.
For the full set of conventions see the Architecture Overview.
The workflow in brief:
-
Consult the PLRM — Read the operator's entry in the PostScript Language Reference Manual (Second Edition first, then Third Edition for updates). Identify all error conditions and the exact stack effect.
-
Implement — Add the function to the appropriate file in
postforge/operators/. Follow the validate-before-pop pattern:def operator_name(ctxt, ostack): # 1. Check stack depth if len(ostack) < 1: return ps_error.e(ctxt, ps_error.STACKUNDERFLOW, operator_name.__name__) # 2. Validate types (peek with negative indexing) if ostack[-1].TYPE not in ps.NUMERIC_TYPES: return ps_error.e(ctxt, ps_error.TYPECHECK, operator_name.__name__) # 3. Only after all validation passes — pop and execute val = ostack.pop() ...
-
Register — Add a tuple to the
opslist inpostforge/operators/dict.py:("operator_name", ps.Operator, ps_module.function_name),
-
Write tests — See the next section.
See Operator Implementation Standards for the full guide covering error handling, type group constants, naming conventions, and the complete checklist.
Tests are written in PostScript using a custom framework. The assert
procedure (defined in unit_tests/unittest.ps) compares the operand stack
after executing an operator or procedure against expected values.
% Format: operand(s) /operator|{} [expected_results] assert
(hello) /length [5] assert % Simple test
5 {dup mul} [25] assert % Procedure testWhen an operator fails validation, it must leave all original operands on the stack. The error name is pushed on top:
/add [/stackunderflow] assert % No operands
(string) 5 /get [(string) 5 /rangecheck] assert % Operands preserved- Normal operation and boundary values
- Type variations (Int vs Real, Array vs PackedArray)
- Every error condition listed in the PLRM
- Access control violations where applicable
A well-tested operator typically has 10-20+ assertions.
pf unit_tests/ps_tests.ps # All tests
pf unit_tests/string_tests.ps # Specific test fileSee Testing Guide for the full framework documentation including visual regression testing.
An output device consists of two parts:
- PostScript resource file (
postforge/resources/OutputDevice/<name>.ps) — a page device dictionary that configures page size, resolution, and rendering mode - Python module (
postforge/devices/<name>/) — implementsshowpage(ctxt, pd)to render the display list
See Adding an Output Device for the complete walkthrough with templates, display list element reference, and advanced patterns.
-
Branch from master — Use a descriptive branch name (e.g.,
add-charpath-operator,fix-arc-winding) -
Keep commits focused — Separate test commits from code changes so reviewers can see what moved. One logical change per commit.
-
Commit messages — Use imperative mood and a concise summary line. Add detail in the body when the "why" isn't obvious from the diff.
Add charpath operator with strokepath support Implements charpath per PLRM Section 8.2. Handles both fill and stroke variants via the bool parameter. -
Run the full test suite before submitting:
pf unit_tests/ps_tests.ps
-
Open a PR against master with a description that summarizes the change and links to any relevant PLRM sections or issues.
Reviewers will check for:
- PLRM compliance — Operator behavior matches the specification
- Complete error handling — Every error condition from the PLRM is covered
- Validate-before-pop — No stack corruption on error paths
- Test coverage — Normal operation, error conditions, and boundary cases
- No regressions — Existing PostScript programs still work
The test suite is the project's safety net. These rules protect it:
- PRs that modify existing assertions require explicit justification for every changed expected value
- Test count must never decrease — removing or weakening assertions is a red flag that must be explained in the PR description
- New operators must ship with tests — not "will add tests later"
- Separate test commits from code commits so reviewers can see exactly what changed
- Run the test suite locally before approving a PR