summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRutger Broekhoff2026-01-08 02:16:11 +0100
committerRutger Broekhoff2026-01-08 02:16:11 +0100
commit40ed2624e13bc519bebe4332a217fd539b76e5f4 (patch)
tree30a2e3ef58bbb49633ecf703f6b606f523398116
parent76cc9ce576e830a3ee7615d0f617a7ce24316c44 (diff)
downloadrdcapsis-40ed2624e13bc519bebe4332a217fd539b76e5f4.tar.gz
rdcapsis-40ed2624e13bc519bebe4332a217fd539b76e5f4.zip
Type system crimes have been committed
-rw-r--r--lib/ledger.ml334
1 files changed, 194 insertions, 140 deletions
diff --git a/lib/ledger.ml b/lib/ledger.ml
index 058cc65..3e1d177 100644
--- a/lib/ledger.ml
+++ b/lib/ledger.ml
@@ -182,19 +182,7 @@ type scalar =
182 | Rate of { in_primary_commodity : Money.Amount.t; rate : Bigdecimal.t } 182 | Rate of { in_primary_commodity : Money.Amount.t; rate : Bigdecimal.t }
183[@@deriving equal, compare, sexp_of] *) 183[@@deriving equal, compare, sexp_of] *)
184 184
185module Account = struct 185module Gh = struct
186 (* The contents of an account of category 'a *)
187 type 'a core =
188 (* Comprises of subaccounts of its subcategories *)
189 | Node of 'a String.Map.t
190 (* Comprises of subaccounts of its own category *)
191 | Ind of 'a t String.Map.t
192 (* Has no subaccounts, has a balance in a certain commodity *)
193 | Leaf of Commodity_id.t * Money.Diff.t
194
195 and extra = { description : String.t }
196 and 'a t = extra * 'a core
197
198 (* The category of the five top-level categories *) 186 (* The category of the five top-level categories *)
199 type global 187 type global
200 188
@@ -207,92 +195,188 @@ module Account = struct
207 195
208 (* No subcategories *) 196 (* No subcategories *)
209 type final 197 type final
198end
210 199
211 (* Subaccounts under the five top-level categories *) 200module Account_structure0 (F : sig
201 type 'a t
202end) =
203struct
212 type 'a f = 204 type 'a f =
213 | Accounts_payable : final f t -> liability f 205 | Accounts_payable : Gh.final f F.t -> Gh.liability f
214 | Accounts_receivable : final f t -> asset f 206 | Accounts_receivable : Gh.final f F.t -> Gh.asset f
215 | Bank : final f t -> asset f 207 | Bank : Gh.final f F.t -> Gh.asset f
216 | Cash : final f t -> asset f 208 | Cash : Gh.final f F.t -> Gh.asset f
217 | Credit : final f t -> liability f 209 | Credit : Gh.final f F.t -> Gh.liability f
218 | Mutual_fund : final f t -> asset f 210 | Mutual_fund : Gh.final f F.t -> Gh.asset f
219 | Stock : final f t -> asset f 211 | Stock : Gh.final f F.t -> Gh.asset f
220 212
221 module Ft_mapper = struct 213 type t0 =
222 type nonrec 'b t = { car : 'a. 'a f t -> ('b * 'a f t) option } 214 | Asset of Gh.asset f F.t
223 215 | Equity of Gh.equity f F.t
224 let map (type b c) (f : c t) : b f -> (c * b f) option = 216 | Expense of Gh.expense f F.t
217 | Income of Gh.income f F.t
218 | Liability of Gh.liability f F.t
219end
220
221module Account_type = struct
222 type 'a elem = Leaf | Node of 'a
223
224 include Account_structure0 (struct
225 type 'a t = 'a elem
226 end)
227end
228
229module Account_structure (F : sig
230 type 'a t
231end) =
232struct
233 include Account_structure0 (F)
234
235 module Mapper = struct
236 type nonrec 'b t = {
237 car :
238 'a.
239 'a f F.t ->
240 ('a Account_type.f Account_type.elem -> Account_type.t0) ->
241 ('b * 'a f F.t) option;
242 }
243
244 let map (type b c) (f : c t) (mkt : b Account_type.f -> Account_type.t0) :
245 b f -> (c * b f) option =
225 let open Option.Let_syntax in 246 let open Option.Let_syntax in
226 function 247 function
227 | Accounts_payable v -> 248 | Accounts_payable v ->
228 let%map c, v' = f.car v in 249 let%map c, v' =
250 f.car v (fun el -> mkt (Account_type.Accounts_payable el))
251 in
229 (c, Accounts_payable v') 252 (c, Accounts_payable v')
230 | Accounts_receivable v -> 253 | Accounts_receivable v ->
231 let%map c, v' = f.car v in 254 let%map c, v' =
255 f.car v (fun el -> mkt (Account_type.Accounts_receivable el))
256 in
232 (c, Accounts_receivable v') 257 (c, Accounts_receivable v')
233 | Bank v -> 258 | Bank v ->
234 let%map c, v' = f.car v in 259 let%map c, v' = f.car v (fun el -> mkt (Account_type.Bank el)) in
235 (c, Bank v') 260 (c, Bank v')
236 | Cash v -> 261 | Cash v ->
237 let%map c, v' = f.car v in 262 let%map c, v' = f.car v (fun el -> mkt (Account_type.Cash el)) in
238 (c, Cash v') 263 (c, Cash v')
239 | Credit v -> 264 | Credit v ->
240 let%map c, v' = f.car v in 265 let%map c, v' = f.car v (fun el -> mkt (Account_type.Credit el)) in
241 (c, Credit v') 266 (c, Credit v')
242 | Mutual_fund v -> 267 | Mutual_fund v ->
243 let%map c, v' = f.car v in 268 let%map c, v' =
269 f.car v (fun el -> mkt (Account_type.Mutual_fund el))
270 in
244 (c, Mutual_fund v') 271 (c, Mutual_fund v')
245 | Stock v -> 272 | Stock v ->
246 let%map c, v' = f.car v in 273 let%map c, v' = f.car v (fun el -> mkt (Account_type.Stock el)) in
247 (c, Stock v') 274 (c, Stock v')
248 end 275 end
276end
249 277
250 module Top_level (F : sig 278module Typed_account_path = struct
251 type 'a t 279 type 'a elem = Leaf | Node of string * 'a | Ind of string * 'a elem
252 end) =
253 struct
254 type t =
255 | Asset of asset F.t
256 | Equity of equity F.t
257 | Expense of expense F.t
258 | Income of income F.t
259 | Liability of liability F.t
260 end
261 280
262 module Top_level_type = Top_level (struct 281 include Account_structure (struct
263 type nonrec 'a t = unit 282 type 'a t = 'a elem
264 end) 283 end)
284end
265 285
266 (* I swear the bullshit stops here *) 286module Account_path = struct
267 module F0 = struct 287 type t = string list [@@deriving compare, sexp]
268 include Top_level (struct 288
269 type nonrec 'a t = 'a f t 289 module Map = Map.Make (struct
270 end) 290 type nonrec t = t [@@deriving compare, sexp]
271 291 end)
272 let type_ : t -> Top_level_type.t = function 292end
273 | Asset _ -> Asset () 293
274 | Equity _ -> Equity () 294module Account_hierarchy = struct
275 | Expense _ -> Expense () 295 (* The contents of an account of category 'a *)
276 | Income _ -> Income () 296 type 'a core =
277 | Liability _ -> Liability () 297 (* Comprises of subaccounts of its subcategories *)
278 end 298 | Node of 'a String.Map.t
299 (* Comprises of subaccounts of its own category *)
300 | Ind of 'a account String.Map.t
301 (* Has no subaccounts, has a balance in a certain commodity *)
302 | Leaf of Commodity_id.t * Money.Diff.t
303
304 and extra = { description : String.t }
305 and 'a account = extra * 'a core
306
307 include Account_structure (struct
308 type 'a t = 'a account
309 end)
279 310
280 (* All accounts *) 311 (* All accounts *)
281 type world = F0.t String.Map.t 312 type world = t0 String.Map.t
282 313
283 module Path = struct 314 let rec alter_aux (subaid : Account_path.t)
284 type t = string list [@@deriving compare, sexp] 315 (f :
316 Account_type.t0 -> Commodity_id.t -> Money.Diff.t -> 'a * Money.Diff.t)
317 : 'a Mapper.t =
318 {
319 car =
320 (fun in_acc mkt ->
321 let open Option.Let_syntax in
322 match (subaid, in_acc) with
323 | [], (extra, Leaf (acc_comm, acc_bal)) ->
324 let x, acc_bal' = f (mkt Account_type.Leaf) acc_comm acc_bal in
325 Some (x, (extra, Leaf (acc_comm, acc_bal')))
326 | [], _ -> None
327 | subaid0 :: subaid, (extra, Node subaccs) ->
328 let open Option.Let_syntax in
329 let%bind subacc = Map.find subaccs subaid0 in
330 let%map x, subacc' =
331 Mapper.map (alter_aux subaid f) (fun k -> mkt (Node k)) subacc
332 in
333 (x, (extra, Node (Map.set subaccs ~key:subaid0 ~data:subacc')))
334 | subaid0 :: subaid, (extra, Ind subaccs) ->
335 let open Option.Let_syntax in
336 let%bind subacc = Map.find subaccs subaid0 in
337 let%map x, subacc' = (alter_aux subaid f).car subacc mkt in
338 (x, (extra, Ind (Map.set subaccs ~key:subaid0 ~data:subacc')))
339 | _ :: _, (_, Leaf _) -> None);
340 }
285 341
286 module Map = Map.Make (struct 342 let alter (aid : Account_path.t)
287 type nonrec t = t [@@deriving compare, sexp] 343 (f :
288 end) 344 Account_type.t0 -> Commodity_id.t -> Money.Diff.t -> 'a * Money.Diff.t)
289 end 345 (w : world) : ('a * world) option =
346 match aid with
347 | [] -> None
348 | aid0 :: subaid -> (
349 let open Option.Let_syntax in
350 match%bind Map.find w aid0 with
351 | Asset acc ->
352 let%map x, acc' = (alter_aux subaid f).car acc (fun k -> Asset k) in
353 (x, Map.set w ~key:aid0 ~data:(Asset acc'))
354 | Expense acc ->
355 let%map x, acc' =
356 (alter_aux subaid f).car acc (fun k -> Expense k)
357 in
358 (x, Map.set w ~key:aid0 ~data:(Expense acc'))
359 | Income acc ->
360 let%map x, acc' =
361 (alter_aux subaid f).car acc (fun k -> Income k)
362 in
363 (x, Map.set w ~key:aid0 ~data:(Income acc'))
364 | Liability acc ->
365 let%map x, acc' =
366 (alter_aux subaid f).car acc (fun k -> Liability k)
367 in
368 (x, Map.set w ~key:aid0 ~data:(Liability acc'))
369 | Equity acc ->
370 let%map x, acc' =
371 (alter_aux subaid f).car acc (fun k -> Equity k)
372 in
373 (x, Map.set w ~key:aid0 ~data:(Equity acc')))
290 374
291 let world_inst : world = 375 let world_inst : world =
292 String.Map.of_alist_exn 376 String.Map.of_alist_exn
293 [ 377 [
294 ( "Assets", 378 ( "Assets",
295 F0.Asset 379 Asset
296 ( { description = "assets" }, 380 ( { description = "assets" },
297 Ind 381 Ind
298 (String.Map.of_alist_exn 382 (String.Map.of_alist_exn
@@ -323,7 +407,7 @@ module Account = struct
323end 407end
324 408
325type bal_assert = { 409type bal_assert = {
326 account : Account.Path.t; 410 account : Account_path.t;
327 labels : Labels.t; 411 labels : Labels.t;
328 bal : Money.Diff.t; 412 bal : Money.Diff.t;
329} 413}
@@ -340,7 +424,7 @@ module Tx : sig
340 (* Private because we only want to allow constructing balanced transactions. *) 424 (* Private because we only want to allow constructing balanced transactions. *)
341 type t = private { 425 type t = private {
342 cleared : Date.t option; 426 cleared : Date.t option;
343 entries : entry Account.Path.Map.t; 427 entries : entry Account_path.Map.t;
344 labels : Labels.t; 428 labels : Labels.t;
345 } 429 }
346 430
@@ -348,7 +432,7 @@ module Tx : sig
348 432
349 val make : 433 val make :
350 cleared:Date.t option -> 434 cleared:Date.t option ->
351 entries:entry Account.Path.Map.t -> 435 entries:entry Account_path.Map.t ->
352 labels:Labels.t -> 436 labels:Labels.t ->
353 (t, error) result 437 (t, error) result
354 438
@@ -364,7 +448,7 @@ end = struct
364 448
365 type t = { 449 type t = {
366 cleared : Date.t option; 450 cleared : Date.t option;
367 entries : entry Account.Path.Map.t; 451 entries : entry Account_path.Map.t;
368 labels : Labels.t; 452 labels : Labels.t;
369 } 453 }
370 [@@deriving sexp_of] 454 [@@deriving sexp_of]
@@ -394,87 +478,57 @@ type item =
394type t = item list [@@deriving sexp_of] 478type t = item list [@@deriving sexp_of]
395 479
396module World = struct 480module World = struct
397 type t = Account.world 481 type t = Account_hierarchy.world
398 482
399 let empty : t = String.Map.empty 483 let empty : t = String.Map.empty
400 484
401 let update_bal_fn = (f :
402 Account.Top_level_type.t ->
403 Commodity_id.t ->
404 Money.Diff.t ->
405 ('a * Money.Diff.t) option)
406
407 (* Stretching the type system a little :) *)
408 let rec update_bal_aux ttype subaid
409 (f :
410 Account.Top_level_type.t ->
411 Commodity_id.t ->
412 Money.Diff.t ->
413 ('a * Money.Diff.t) option) : 'a Account.Ft_mapper.t =
414 {
415 car =
416 (fun in_acc ->
417 let open Option.Let_syntax in
418 match (subaid, in_acc) with
419 | [], (extra, Account.Leaf (acc_comm, acc_bal)) ->
420 let%bind x, acc_bal' = f ttype acc_comm acc_bal in
421 Some (x, (extra, Account.Leaf (acc_comm, acc_bal')))
422 | [], _ -> None
423 | subaid0 :: subaid, (extra, Node subaccs) ->
424 let open Option.Let_syntax in
425 let%bind subacc = Map.find subaccs subaid0 in
426 let%map x, subacc' =
427 Account.Ft_mapper.map (update_bal_aux ttype subaid f) subacc
428 in
429 ( x,
430 ( extra,
431 Account.Node (Map.set subaccs ~key:subaid0 ~data:subacc') ) )
432 | subaid0 :: subaid, (extra, Ind subaccs) ->
433 let open Option.Let_syntax in
434 let%bind subacc = Map.find subaccs subaid0 in
435 let%map x, subacc' = (update_bal_aux ttype subaid f).car subacc in
436 ( x,
437 (extra, Account.Ind (Map.set subaccs ~key:subaid0 ~data:subacc'))
438 )
439 | _ :: _, (_, Leaf _) -> None);
440 }
441
442 (** Update the balance (debit/credit ([dc])) of account [aid] [by_amount] 485 (** Update the balance (debit/credit ([dc])) of account [aid] [by_amount]
443 (commodity: [in_comm]) in [world], giving the updated world and the pre 486 (commodity: [in_comm]) in [world], giving the updated world and the pre
444 and post balances for [aid] iff the account exists in [world]. *) 487 and post balances for [aid] iff the account exists in [world]. *)
445 let update_bal aid dc by_amount in_comm (world : t) : 488 let update_bal aid dc by_amount in_comm (world : t) :
446 ((Money.Diff.t * Money.Diff.t) * t) option = 489 (Money.Diff.t * Money.Diff.t * t) option =
447 match aid with 490 let open Option.Let_syntax in
448 | [] -> None 491 let%bind mres, world' =
449 | aid0 :: subaid -> ( 492 Account_hierarchy.alter aid
450 let open Option.Let_syntax in 493 (fun acc_type acc_comm acc_bal ->
451 match%bind Map.find world aid0 with 494 if not ([%equal: Commodity_id.t] acc_comm in_comm) then (None, acc_bal)
452 | Asset acc -> 495 else
453 let diff = Money.Diff.of_amount by_amount dc ~on_debit:`Incr in 496 match acc_type with
454 let%map bals, acc' = (update_bal_aux subaid diff in_comm).car acc in 497 | Asset _ ->
455 (bals, Map.set world ~key:aid0 ~data:(Asset acc')) 498 let acc_bal' =
456 | Expense acc -> 499 Money.Diff.(acc_bal + of_amount by_amount dc ~on_debit:`Incr)
457 let diff = Money.Diff.of_amount by_amount dc ~on_debit:`Incr in 500 in
458 let%map bals, acc' = (update_bal_aux subaid diff in_comm).car acc in 501 (Some (acc_bal, acc_bal'), acc_bal')
459 (bals, Map.set world ~key:aid0 ~data:(Expense acc')) 502 | Expense _ ->
460 | Income acc -> 503 let acc_bal' =
461 let diff = Money.Diff.of_amount by_amount dc ~on_debit:`Decr in 504 Money.Diff.(acc_bal + of_amount by_amount dc ~on_debit:`Incr)
462 let%map bals, acc' = (update_bal_aux subaid diff in_comm).car acc in 505 in
463 (bals, Map.set world ~key:aid0 ~data:(Income acc')) 506 (Some (acc_bal, acc_bal'), acc_bal')
464 | Liability acc -> 507 | Income _ ->
465 let diff = Money.Diff.of_amount by_amount dc ~on_debit:`Decr in 508 let acc_bal' =
466 let%map bals, acc' = (update_bal_aux subaid diff in_comm).car acc in 509 Money.Diff.(acc_bal + of_amount by_amount dc ~on_debit:`Decr)
467 (bals, Map.set world ~key:aid0 ~data:(Liability acc')) 510 in
468 | Equity acc -> 511 (Some (acc_bal, acc_bal'), acc_bal')
469 let diff = Money.Diff.of_amount by_amount dc ~on_debit:`Decr in 512 | Liability _ ->
470 let%map bals, acc' = (update_bal_aux subaid diff in_comm).car acc in 513 let acc_bal' =
471 (bals, Map.set world ~key:aid0 ~data:(Equity acc'))) 514 Money.Diff.(acc_bal + of_amount by_amount dc ~on_debit:`Decr)
515 in
516 (Some (acc_bal, acc_bal'), acc_bal')
517 | Equity _ ->
518 let acc_bal' =
519 Money.Diff.(acc_bal + of_amount by_amount dc ~on_debit:`Decr)
520 in
521 (Some (acc_bal, acc_bal'), acc_bal'))
522 world
523 in
524 let%map pre_bal, post_bal = mres in
525 (pre_bal, post_bal, world')
472 526
473 let apply_tx (tx : Tx.t) world : t option = 527 let apply_tx (tx : Tx.t) world : t option =
474 Map.fold_option tx.entries ~init:world 528 Map.fold_option tx.entries ~init:world
475 ~f:(fun ~key:aid ~(data : Tx.entry) world -> 529 ~f:(fun ~key:aid ~(data : Tx.entry) world ->
476 let open Option.Let_syntax in 530 let open Option.Let_syntax in
477 let%bind (_old_bal, new_bal), world = 531 let%bind _old_bal, new_bal, world =
478 update_bal aid data.dc data.amount data.commodity world 532 update_bal aid data.dc data.amount data.commodity world
479 in 533 in
480 match data.assertion with 534 match data.assertion with