123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299(* Claude Code
*
* Copyright (C) 2026 Yoann Padioleau
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public License
* (LGPL) as published by the Free Software Foundation; either version
* 2 of the License, or (at your option) any later version.
*)(* See Sheet.mli *)(*****************************************************************************)(* Types *)(*****************************************************************************)moduleCells=Map.Make(structtypet=Formula.cellletcompare=compareend)typevalue=Numberoffloat|Textofstring|Errorofstring|Emptytypet={(* what was typed, kept as typed: a formula that does not parse has
to stay on the screen to be corrected *)raws:stringCells.t;contents:Formula.contentCells.t;values:valueCells.t;(* the graph, both ways round: what a cell reads, and who reads it.
The second is the one that matters -- a change walks *forwards*
to everything downstream of it *)reads:Formula.celllistCells.t;read_by:Formula.celllistCells.t;last_recalculated:int;}letempty={raws=Cells.empty;contents=Cells.empty;values=Cells.empty;reads=Cells.empty;read_by=Cells.empty;last_recalculated=0;}(*****************************************************************************)(* Reading a sheet *)(*****************************************************************************)letrawtc=matchCells.find_optct.rawswithSomes->s|None->""letvaluetc=matchCells.find_optct.valueswithSomev->v|None->Emptyletcellst=Cells.bindingst.raws|>List.mapfstletrecalculatedt=t.last_recalculatedletshow=function|Empty->""|Texts->s|Errorwhy->"#"^why|Numberf->ifFloat.is_integerf&&Float.absf<1e15thenPrintf.sprintf"%.0f"felsePrintf.sprintf"%g"f(*****************************************************************************)(* Computing one cell, given the values of the cells it reads *)(*****************************************************************************)(* [Error] here is this module's own (a cell in error), and [Result]'s
* is the answer of a computation that could not be done -- hence the
* qualified constructors below, which is the price of a good name *)letnumber_of:value->(float,string)result=function|Numberf->Result.Okf|Empty->Result.Ok0.(* an empty cell counts as nothing, as everywhere *)|Text_->Result.Error"text"|Errorwhy->Result.Errorwhy(* the numbers an argument stands for: one for a cell, many for a
* range -- and a range's text and empty cells are skipped, which is
* what makes SUM over a column with a heading do what you meant *)letrecnumberst(e:Formula.expr):(floatlist,string)result=matchewith|Formula.Range((c1,r1),(c2,r2))->letout=ref[]andbad=refNoneinforcol=minc1c2tomaxc1c2doforrow=minr1r2tomaxr1r2domatchvaluet(col,row)with|Numberf->out:=f::!out|Empty|Text_->()|Errorwhy->if!bad=Nonethenbad:=Somewhydonedone;(match!badwithSomewhy->Result.Errorwhy|None->Result.Ok(List.rev!out))|e->(matchnumber_of(evalte)with|Result.Okf->Result.Ok[f]|Result.Errorwhy->Result.Errorwhy)andevalt(e:Formula.expr):value=letarithfab=match(number_of(evalta),number_of(evaltb))with|Result.Okx,Result.Oky->fxy|Result.Errorwhy,_|_,Result.Errorwhy->Errorwhyinmatchewith|Formula.Numberf->Numberf|Formula.Refc->valuetc|Formula.Range_->Error"range"|Formula.Unary('-',e)->(matchnumber_of(evalte)with|Result.Okf->Number(-.f)|Result.Errorwhy->Errorwhy)|Formula.Unary(_,e)->evalte|Formula.Binop('+',a,b)->arith(funxy->Number(x+.y))ab|Formula.Binop('-',a,b)->arith(funxy->Number(x-.y))ab|Formula.Binop('*',a,b)->arith(funxy->Number(x*.y))ab|Formula.Binop('/',a,b)->arith(funxy->ify=0.thenError"div0"elseNumber(x/.y))ab|Formula.Binop(c,_,_)->Error(Printf.sprintf"op%c"c)|Formula.Call(name,args)->(letgathered:(floatlist,string)result=List.fold_left(fun(acc:(floatlist,string)result)arg->match(acc,numberstarg)with|Result.Errorwhy,_->Result.Errorwhy|_,Result.Errorwhy->Result.Errorwhy|Result.Okall,Result.Oksome->Result.Ok(all@some))(Result.Ok[])argsinmatchgatheredwith|Result.Errorwhy->Errorwhy|Result.Okns->(letsum=List.fold_left(+.)0.nsinmatch(name,ns)with|"SUM",_->Numbersum|"COUNT",_->Number(float_of_int(List.lengthns))|"PRODUCT",_->Number(List.fold_left(*.)1.ns)|"AVERAGE",[]->Error"empty"|"AVERAGE",_->Number(sum/.float_of_int(List.lengthns))|"MIN",[]|"MAX",[]->Error"empty"|"MIN",n::rest->Number(List.fold_leftminnrest)|"MAX",n::rest->Number(List.fold_leftmaxnrest)|_->Error("fn"^name)))letvalue_of_contentt=function|Formula.Blank->Empty|Formula.Valuef->Numberf|Formula.Texts->Texts|Formula.Invalidwhy->Errorwhy|Formula.Formulae->evalte(*****************************************************************************)(* What a change reaches, and in what order *)(*****************************************************************************)letreaderstc=matchCells.find_optct.read_bywithSomel->l|None->[](* every cell downstream of [start], itself included: the change walks
* forwards through the graph *)letdownstreamtstart=letseen=ref[]inletrecgoc=ifnot(List.memc!seen)thenbeginseen:=c::!seen;List.itergo(readerstc)endingostart;!seen(* Kahn's algorithm (1962), over that part of the graph only: take a
* cell that waits for nothing, compute it, and cross it off the
* lists of those waiting for it. What is left when nothing can be
* taken is a cycle. *)letrecomputetdirty=letwaiting_forc=matchCells.find_optct.readswith|None->[]|Somel->List.filter(fund->List.memddirty)linletpending=ref(List.map(func->(c,waiting_forc))dirty)inlett=reftinletdone_=ref0inletrecrounds()=letready,blocked=List.partition(fun(_,waits)->waits=[])!pendinginifready<>[]thenbeginList.iter(fun(c,_)->letv=matchCells.find_optc!t.contentswith|Somecontent->value_of_content!tcontent|None->Emptyint:={!twithvalues=Cells.addcv!t.values};incrdone_)ready;letcomputed=List.mapfstreadyinpending:=List.map(fun(c,waits)->(c,List.filter(fund->not(List.memdcomputed))waits))blocked;rounds()endinrounds();(* whatever is still waiting is waiting on itself, round some loop *)lett=List.fold_left(funt(c,_)->{twithvalues=Cells.addc(Error"cycle")t.values})!t!pendingin{twithlast_recalculated=!done_+List.length!pending}(*****************************************************************************)(* Typing into a cell *)(*****************************************************************************)letstorectextt=letcontent=Formula.content_oftextinletold_reads=matchCells.find_optct.readswithSomel->l|None->[]inletnew_reads=matchcontentwithFormula.Formulae->Formula.refse|_->[]in(* the reverse edges, taken out where they were and put in where
they are now: the graph is only ever as right as this step *)letread_by=List.fold_left(funmd->letothers=List.filter(funx->x<>c)(matchCells.find_optdmwithSomel->l|None->[])inCells.adddothersm)t.read_byold_readsinletread_by=List.fold_left(funmd->letothers=matchCells.find_optdmwithSomel->l|None->[]inCells.addd(c::others)m)read_bynew_readsin{twithraws=(ifString.trimtext=""thenCells.removect.rawselseCells.addctextt.raws);contents=Cells.addccontentt.contents;reads=Cells.addcnew_readst.reads;read_by;}(* typing into a cell, and then everything that depends on it *)letsetctextt=lett=storectexttinrecomputet(downstreamtc)(*****************************************************************************)(* The way it was done in 1979 *)(*****************************************************************************)typeorder=Rows|Columns(* One pass, in the order the cells are laid out -- and nothing about
* what reads what. Compare with [recompute] above, which is the same
* loop with the graph in it. *)letrecalculateordert=letcs=cellstinletordered=List.sort(fun(c1,r1)(c2,r2)->matchorderwith|Rows->compare(r1,c1)(r2,c2)|Columns->compare(c1,r1)(c2,r2))csinlett=List.fold_left(funtc->matchCells.find_optct.contentswith|Somecontent->{twithvalues=Cells.addc(value_of_contenttcontent)t.values}|None->t)torderedin{twithlast_recalculated=List.lengthordered}(*****************************************************************************)(* Saving *)(*****************************************************************************)letto_stringt=cellst|>List.map(func->Printf.sprintf"%s\t%s"(Formula.name_of_cellc)(rawtc))|>String.concat"\n"letof_strings=String.split_on_char'\n's|>List.fold_left(funtline->matchString.index_optline'\t'with|None->t|Somei->(letname=String.subline0iinlettext=String.subline(i+1)(String.lengthline-i-1)inmatchFormula.cell_of_name(String.trimname)with|Somec->setctextt|None->t))empty