From COBOL to Kotlin. My First Experiment in Verifiable… | by Marco Graziano | Nov, 2025

Strategy: From Structure to Semantics

Large-scale modernization begins with a Structured Intermediate Representation (IR)This is not a compiler artifact, but a conceptual map of the meaning of the system,

layered ir

layer description Syntactic IR Parses COBOL into paragraphs, sections, data declarations, and copybooks structural ir Captures record layout, field types, and file I/O contracts Control-flow IR (CFG) graphs the flow of PERFORM, EVALUATEAnd IF branches impact ir Tracks reads/writes of files and variables Semantic IR Expresses the algebra of transformations – for example “equilibrium’ = balance – amount”

Each layer adds fidelity without losing context. Even if we stop halfway, the resulting model already serves as documentation, dependency graph, and modernization map.

The IR snippet below explains how the parser captures record, encodingAnd posting rules,

{
"program": "DAILYPOST",
"files": [
{
"name": "ACCT-FILE",
"record": "AccountRec",
"layout": [
{ "name": "ACCT_ID", "kind": "char", "len": 12, "offset": 0 },
{ "name": "CUST_ID", "kind": "char", "len": 12, "offset": 12 },
{ "name": "PRODUCT_CODE", "kind": "char", "len": 4, "offset": 24 },
{ "name": "ACCT_STATUS", "kind": "char", "len": 1, "offset": 28 },
{ "name": "CURR_BAL", "kind": "comp3", "digits": 13, "scale": 2, "offset": 29 },
{ "name": "OVERDRAFT_LIMIT", "kind": "comp3", "digits": 11, "scale": 2, "offset": 36 }
]
},
{
"name": "TXN-FILE",
"record": "TxnRec",
"layout": [
{ "name": "ACCT_ID", "kind": "char", "len": 12, "offset": 0 },
{ "name": "TXN_ID", "kind": "char", "len": 16, "offset": 12 },
{ "name": "TXN_CODE", "kind": "char", "len": 4, "offset": 28 },
{ "name": "TXN_AMT", "kind": "comp3","digits": 11, "scale": 2, "offset": 32 },
{ "name": "TXN_TS", "kind": "num", "len": 14, "offset": 37 }
]
}
],
"logic": [
{
"when": "yyyymmdd(TXN_TS) == TODAY",
"switch": "TXN_CODE",
"cases": {
"DEPO": [{ "op": "Add", "dst": "CURR_BAL", "src": "TXN_AMT" }],
"WDRW": [
{ "op": "Compute", "dst": "NEW_BAL", "expr": "CURR_BAL - TXN_AMT" },
{ "op": "Guard", "cond": "NEW_BAL >= -OVERDRAFT_LIMIT",
"then": [{ "op": "Move", "dst": "CURR_BAL", "src": "NEW_BAL" }],
"else": [{ "op": "Emit", "stream": "EXC-FILE", "rec": "TxnRec" }]
}
]
}
}
]
}

full ir is in parsing/out/ir.jsonNote that COMP-3 is clearly captured (digits, scale) so arithmetic in codegen is integer-safe.

The same meaning is compiled into one deterministic, testable Modern implementation in Kotlin.

// rewrite/kotlin/src/main/kotlin/banking/Posting.kt
package banking

data class AccountRec(
val acctId: String,
val custId: String,
val productCode: String,
val acctStatus: String,
var currBalCents: Long, // integer cents to mirror COMP-3
val overdraftLimitCents: Long
)

data class TxnRec(
val acctId: String,
val txnId: String,
val code: String, // "DEPO", "WDRW", ...
val amountCents: Long, // integer cents (no floats)
val tsYyyyMmDdHhMmSs: Long
)

fun runPosting(
accounts: MutableList,
txns: List,
todayYyyyMmDd: Long
): Pair, List> {

val (todayTxns, otherTxns) = txns.partition { it.tsYyyyMmDdHhMmSs / 1_000_000L == todayYyyyMmDd }
require(otherTxns.isEmpty()) { "Runner expects only today's txns (for golden-master parity)" }

val exceptions = mutableListOf()
val index = accounts.associateBy { it.acctId }.toMutableMap()

// Stable order to guarantee deterministic outputs
for (t in todayTxns.sortedWith(compareBy { it.acctId }.thenBy { it.txnId })) {
val a = index[t.acctId] ?: run { exceptions += t; continue }
when (t.code) {
"DEPO" -> a.currBalCents += t.amountCents
"WDRW" -> {
val newBal = a.currBalCents - t.amountCents
if (newBal >= -a.overdraftLimitCents) a.currBalCents = newBal else exceptions += t
}
else -> exceptions += t
}
}
return accounts.sortedBy { it.acctId } to exceptions
}

Role of formal model

⚙️Alloy

Alloy model static structure – relationships between records, constraints and invariants.

Example: “Each transaction belongs to exactly one account.”
has been captured in formal/alloy/banking.als And verified using alloy analyzer.

⏱ TLA+

TLA+ expresses temporary behavior – perfect for COBOL’s batch workflow.
This allows us to specify conditions such as: “After all transactions have been processed, each account balance must be updated exactly once.”

🧮 Smt / Z3

Z3 (satisfiability modulo principle) checks arithmetic and logical consistency. This is ideal for proving that rewritten arithmetic (for example, interest or overdraft rules) is equivalent not only with sample data, but also at the symbolic level.



Leave a Comment