PHoPol: COBOL Working-Storage Semantics in PHP
Preliminary
PHoPol: COBOL Working-Storage Semantics in PHP

Preliminary
Long time ago, I started my career as a COBOL developer working in a mainframe environment. Meanwhile, my biggest side-project was (and still is) about web-dev in PHP. I appreciate the power and flexibility of PHP, but i am always amazed at how much memory it takes to run : What should take Kilobytes needs Megabytes (It is not the only language in this case — think of Java).
So i was wondering, what if PHP could manage memory so to be as frugal as COBOL ? I have no skill in developing C extensions for PHP, but nowadays AI coding assistants are able to do that. So i dived into it with Claude Code (Sonnet 4.6) for a full week, and here is what we built (the rest of this article has also mainly been generated by Claude).
The problem
A lot of enterprise software that talks to mainframes, banks, or insurance systems has to deal with fixed-width binary records. COBOL programs think in flat memory buffers: a record is a byte array, fields are at known offsets with known lengths, packed-decimal encoding, REDEFINES overlays sharing the same bytes, OCCURS tables at fixed positions. COBOL lets you design the memory byte per byte, so when you grab a slice of it, you know exactly what data are in it. Meanwhile in PHP, data are spread across memory without you to know where. Gathering them in a contiguous place (e.g. to write them in a file) comes at a price.
PHoPol solves that by bringing COBOL WORKING-STORAGE SECTION semantics directly into PHP. You describe your data layout in a .phopol file that mirrors COBOL syntax closely, then access fields as if they were ordinary PHP object properties. All encoding and decoding happens in a C extension.
How it works
There are two pieces.
1. The .phopol file describes your data layout, one group per COBOL level-01. It is PHP-flavored syntax so your IDE can parse it, but it maps directly to COBOL constructs:
// wss.phopol file
namespace wss {
level(01) $WsEmployee {
string<6> $employeeId; // PIC X(6)
string<30> $employeeName; // PIC X(30)
string<1> $employeeGender; // PIC X(1)
string<1> $employeeStatus; // PIC X
sdecimal<9,2> packed $baseSalary; // PIC S9(9)V99 COMP-3
sdecimal<9,2> packed $bonus; // PIC S9(9)V99 COMP-3
sdecimal<9,2> packed $totalComp; // PIC S9(9)V99 COMP-3
int<4> binary $payGrade; // PIC S9(4) COMP
uint<8> $hireDate; // PIC 9(8)
when $employeeStatus == 'A' : bool $isActive;
when $employeeStatus == 'R' : bool $isRetired;
when $employeeStatus == 'T' : bool $isTerminated;
}
}
2. The loadSection function parses and registers that layout with the C extension in one call:
$levels = PHoPol\loadSection('/path/to/wss.phopol');
$WsEmployee = $levels['WsEmployee'];
After that, field access is usual property syntax. Encoding and decoding are invisible:
$WsEmployee->employeeName = 'John Smith';
$WsEmployee->baseSalary = 75000.00; // stored as BCD packed decimal
$WsEmployee->payGrade = 8; // stored as 4-byte binary int
echo $WsEmployee->baseSalary; // → 75000.0 (decoded from BCD automatically)
echo $WsEmployee->isActive; // → true (88-level condition name)
Writing to a binary file is a single fwrite() - same as COBOL's WRITE verb:
fwrite($fh, $WsEmployee); // writes raw buffer bytes
Reading a record back is attach():
$raw = fread($fh, $recSize);
$WsEmployee->attach($raw);
echo $WsEmployee->employeeName; // decoded from bytes
Translating COBOL WSS into PHoPol
- Numeric types
| COBOL | PHoPol |
|-----------------------|------------------------|
| `PIC X(10)` | `string<10>` |
| `PIC 9(5)` | `uint<5>` |
| `PIC S9(9) COMP-3` | `sdecimal<9,0> packed` |
| `PIC S9(7)V99 COMP-3` | `sdecimal<7,2> packed` |
| `PIC S9(9) COMP` | `int<9> binary` |
| `USAGE COMP-2` | `float64` |
- Edited picture masks
COBOL:
01 WS-EDITED-SAMPLES.
05 WS-AMT-EDITED PIC ZZZ,ZZ9.99.
05 WS-AMT-DOLLAR PIC $$$,$$$,$$9.99.
05 WS-AMT-CHEQUE PIC ***,**9.99.
05 WS-DATE-EDITED PIC 99/99/9999.
05 WS-AMT-CR PIC ZZZ,ZZ9.99CR.
PHoPol:
level(01) $WsEditedSamples {
edited<"ZZZ,ZZ9.99"> $amtEdited; // suppress leading zeros
edited<"$$$,$$$,$$9.99"> $amtDollar; // floating $
edited<"***,**9.99"> $amtCheque; // cheque protection
edited<"99/99/9999"> $dateEdited; // slash insertion
edited<"ZZZ,ZZ9.99CR"> $amtCr; // CR/DB suffix
}
Assign a number, read back the formatted string:
$WsEditedSamples->amtEdited = 1234.56; // → " 1,234.56" (leading zeros suppressed)
$WsEditedSamples->amtDollar = 1234.56; // → " $1,234.56" (floating $)
$WsEditedSamples->amtCheque = 1234.56; // → "**1,234.56" (asterisk fill)
$WsEditedSamples->dateEdited = 6152026; // → "06/15/2026"
$WsEditedSamples->amtCr = -42.00; // → " 42.00CR"
- Condition names (level 88)
COBOL:
01 WS-STATUS-CODE PIC X(2).
88 IS-OK VALUE 'OK'.
88 IS-ERROR VALUES 'ER' 'EX'.
88 IS-WARNING VALUE 8 THRU 99.
PHoPol:
level(01) $WsStatusCode {
string<2> $statusCode;
when $statusCode == 'OK' : bool $isOk;
when $statusCode == 'ER' | 'EX' : bool $isError;
when $statusCode in 8..99 : bool $isWarning;
}
Usage is identical to COBOL semantics — reading returns bool, assigning true writes the first listed value back to the parent field:
$status->isOk = true; // SET IS-OK TO TRUE equivalent
echo $status->isOk; // → true
- REDEFINES
COBOL:
01 WS-DATE-NUMERIC PIC 9(8).
01 WS-DATE-PARTS REDEFINES WS-DATE-NUMERIC.
05 WS-YYYY PIC 9(4).
05 WS-MM PIC 9(2).
05 WS-DD PIC 9(2).
PHoPol:
level(01) $WsDateNumeric {
uint<8> $date;
}
level(01) $WsDateParts redefines $WsDateNumeric {
uint<4> $yyyy;
uint<2> $mm;
uint<2> $dd;
}
Both objects share the same underlying byte buffer. Write through one, read through the other:
$WsDateNumeric->date = 20260615;
echo $WsDateParts->yyyy; // → 2026
echo $WsDateParts->mm; // → 6
echo $WsDateParts->dd; // → 15
$WsDateParts->yyyy = 1999;
$WsDateParts->dd = 30;
echo $WsDateNumeric->date; // → 19990630 (same bytes)
- OCCURS tables
COBOL:
01 WS-CALENDAR.
05 WS-MONTH OCCURS 12 TIMES INDEXED BY WS-MONTH-IDX.
10 WS-MONTH-NAME PIC X(9).
10 WS-MONTH-DAYS PIC 9(2).
10 WS-MONTH-TOTAL PIC S9(9)V99 COMP-3.
PHoPol:
level(01) $WsCalendar {
level(05) $month occurs(12, index: $monthIdx) {
string<9> $monthName;
uint<2> $monthDays;
sdecimal<9,2> packed $monthTotal;
}
}
Cell access is array-subscript syntax, 1-based like COBOL:
$WsCalendar->month[3]->monthName = 'March';
$WsCalendar->month[3]->monthDays = 31;
$WsCalendar->month[3]->monthTotal = 15250.75;
echo $WsCalendar->month[3]->monthName; // → 'March '
- OCCURS DEPENDING ON (variable-length tables) also works — set the control field before accessing rows, exactly as in COBOL:
$WsOrder->lineCount = 2; // DEPENDING ON field
$WsOrder->orderLine[1]->lineCode = 'WIDGET-A';
$WsOrder->orderLine[2]->lineCode = 'GADGET-B';
// $WsOrder->orderLine[3] → throws Error: OCCURS index out of range
Performance
Two benchmarks were run on Windows 11, PHP 8.3, php_phopol.dll (C extension), each one compares a pure PHP program with an equivalent one using PHoPol.
- Benchmark 1: fixed-width binary file I/O
50,000 records × 37 bytes in / 59 bytes out. Layout: string, zoned decimal, 4-byte binary int, packed-decimal amounts. Task: read input, compute gross/tax/net (COMP-3), write output.

Output files were byte-identical. The dominant cost on the pure-PHP side is BCD encode/decode: every packed-decimal field requires a nibble loop in PHP. The C extension does this in a tight C loop with no per-nibble PHP overhead.
- Benchmark 2: in-memory OCCURS table
50,000 rows × 48 bytes (string, binary int, float64). Task: populate all rows, then compute amount/discount/net and aggregate.

PHoPol is 20% slower for pure in-memory traversal of simple numeric types — the property-handler overhead exists to support the full encoding pipeline (BCD, edited masks, condition names, REDEFINES). The tradeoff is memory: PHP arrays cost ~440 bytes per row in hash-map overhead. PHoPol’s C-side buffer is flat; PHP-side memory stays constant regardless of N.
- When PHoPol wins

What it covers
The full feature set maps COBOL WORKING-STORAGE 1:1:
- All numeric types: display (zoned decimal), COMP (binary), COMP-3 (packed decimal), COMP-5 (native), COMP-1/COMP-2 (float)
- Edited picture masks (
ZZZ,ZZ9.99,$$$,$$$,$$9.99,***,**9.99CR, ...) - DECIMAL POINT IS COMMA (European locale — swaps
.and,in edited masks) - VALUE clause with figurative constants (
SPACES,ZERO,HIGH_VALUES,LOW_VALUES,QUOTE) - REDEFINES (level-01, sub-group, inline field, anonymous group)
- OCCURS fixed, OCCURS DEPENDING ON, nested OCCURS, OCCURS with ASCENDING/DESCENDING KEY
- Condition names (88-levels) — single value, multiple values, range
- RENAMES (66-level aliases), FILLER, GLOBAL, EXTERNAL, standalone (77-level)
- JUSTIFIED RIGHT, BLANK WHEN ZERO, SYNCHRONIZED
And also a few COBOL runtime instructions (not mentioned before) : INITIALIZE, MOVE/ADD CORRESPONDING, SEARCH ALL, INSPECT TALLYING/REPLACING/CONVERTING.
Getting started
The extension requires PHP 8 and php_phopol.dll (Windows) / phopol.so (Linux). No Composer, no external dependencies.
// 1. Describe your layout in a .phopol file
// 2. Load it:
$levels = PHoPol\loadSection('/path/to/wss.phopol');
// 3. Use it:
$rec = $levels['MyRecord'];
$rec->attach(fread($fh, $recSize)); // read binary record
echo $rec->amount; // decoded automatically
fwrite($out, $rec); // write binary record
Conclusion
So (Arnaud speaking again), this version of PHoPol — which doesn’t even have a number, is just a beta. If it should be of any use to anyone, there are probably plenty of things to question. I am no C-coder and had no previous insight on how to build a PHP extension, so I trusted the coding assistant about most of the design patterns it chose. I challenged it as best as i could, resulting in a few significative optimizations, but an expert eye would definitively be usefull. I have no real need of PHoPol, and no means to test it on real life cases, so you are welcome to test it yourself. There are probably features of COBOL data division that are still missing. It would be interesting to have an AI generating a cobol2phopol migration tool, it seems achievable (perhaps not 100% for procedure division). A project for later.
Source: [github.com/a2rette/PHoPol]
PHoPol is an open-source project. Contributions welcome.
메타데이터
- post_id
- 786e1e5f3b74
- slug
- phopol-cobol-working-storage-semantics-in-php-786e1e5f3b74
- url
- https://medium.com/@a2rette/phopol-cobol-working-storage-semantics-in-php-786e1e5f3b74
- canonical_url
- https://medium.com/@a2rette/phopol-cobol-working-storage-semantics-in-php-786e1e5f3b74
- author_url
- https://medium.com/@a2rette
- status
- ok
- fetched_at
- 2026-06-20 20:29:01