123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215(* 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 Talk.mli *)(* the seeded numbers: Lehmer's generator, drawn exactly as
Playground.random_int draws them, so that a seed gives the same game
in a test and on the Playground *)typeseed=Lehmer.tletinitial_seed(n:int):seed=Lehmer.scramblenletrandom_int(lo:int)(hi:int)(seed:seed):int*seed=letseed=Lehmer.nextseedin(lo+int_of_float(float_of_int(hi-lo+1)*.Lehmer.to_unitseed),seed)(*****************************************************************************)(* Programs *)(*****************************************************************************)typestatus=Exited|Interruptedtype'atalk=|Doneof'a|Printofstring*'atalk|Read_lineof(string->'atalk)|Read_keyof(string->'atalk)|Randomofint*(int->'atalk)|Spawnofunittalk*(status->'atalk)|Stepof(unit->'atalk)letreturnx=Donexletprints=Print(s,Done())letread_line=Read_line(funl->Donel)letread_key=Read_key(funk->Donek)letrandomn=Random(n,funi->Donei)letspawnchild=Spawn(child,funst->Donest)letstep=Step(fun()->Done())(* the rest of the program glued after the end of [m]: the requests
stay the same, and where [m] was Done, [f] takes over *)letrec(let*)(m:'atalk)(f:'a->'btalk):'btalk=matchmwith|Donex->fx|Print(s,k)->Print(s,(let*)kf)|Read_linek->Read_line(funl->(let*)(kl)f)|Read_keyk->Read_key(funkey->(let*)(kkey)f)|Random(n,k)->Random(n,funi->(let*)(ki)f)|Spawn(child,k)->Spawn(child,funst->(let*)(kst)f)(* [f] applied only when the step is taken: what follows is built as
late as that *)|Stepk->Step(fun()->(let*)(k())f)letask(question:string):stringtalk=let*()=printquestioninread_line(*****************************************************************************)(* Running without a screen *)(*****************************************************************************)letrun?(seed=1)(program:'atalk)(answers:stringlist):string=letout=Buffer.create256inletsteps=ref0in(* the answers and the seed left, if the program reached its end: a
spawned child's end is where its parent carries on *)letrecgo:'b.'btalk->stringlist->seed->(stringlist*seed)option=funpanswersseed->matchp,answerswith|Done_,_->Some(answers,seed)|Print(s,k),_->Buffer.add_stringouts;gokanswersseed|Random(n,k),_->leti,seed=random_int0(n-1)seedingo(ki)answersseed(* the answer echoed, as the paper would show it *)|Read_linek,a::rest->Buffer.add_stringout(a^"\n");go(ka)restseed|Read_keyk,a::rest->go(ka)restseed|(Read_line_|Read_key_),[]->None|Spawn(child,k),_->(matchgochildanswersseedwith|Some(answers,seed)->go(kExited)answersseed|None->None)(* a million steps: a program that doesn't end *)|Stepk,_->incrsteps;if!steps>1_000_000thenNoneelsego(k())answersseedinignore(goprogramanswers(initial_seedseed));Buffer.contentsout(*****************************************************************************)(* The machine *)(*****************************************************************************)typemachine={program:unittalk;(* the parents waiting for their spawned child, the innermost first *)parents:(status->unittalk)list;vt:Vt.t;tty:Line_discipline.t;(* bytes printed but not yet on the screen, at a baud rate *)(* newest first, joined when it goes to the screen: a string appended
to at each print would be copied whole each time, and a BASIC loop
prints thousands of lines a frame *)outbox:stringlist;(* characters a second, None: at once *)cps:floatoption;budget:float;seed:seed;(* the steps taken since the last frame *)steps:int;}(* bytes put in the outbox, and the outbox's bytes in order *)letadd(s:string)(outbox:stringlist):stringlist=ifs=""thenoutboxelses::outboxletpending(m:machine):string=String.concat""(List.revm.outbox)(* everything in the outbox on the screen, unless a baud rate
rations it (then [tick] does it) *)letflush(m:machine):machine=matchm.cpswith|None->{mwithvt=Vt.feedm.vt(pendingm);outbox=[]}|Some_->m(* the steps a frame may take: a program computing at length (BASIC's
10 GOTO 10) runs this much a frame, then the screen is drawn and the
keyboard read, Control-C included *)letsteps_per_frame=20_000(* at a baud rate, the bytes waiting for the line: past this, a program
printing in a loop waits for them, as write(2) blocks on a tty whose
output buffer is full *)letoutput_buffer=256letfull(m:machine):bool=m.cps<>None&&List.fold_left(funns->n+String.lengths)0m.outbox>=output_buffer(* the program run until it reads, ends, or has taken its steps for
this frame: its prints into the outbox, its random numbers drawn,
the tty set to the mode its read needs *)letrecadvance(m:machine):machine=matchm.programwith|Stepkwhenm.steps<steps_per_frame&¬(fullm)->advance{mwithprogram=k();steps=m.steps+1}|Step_->flush{mwithtty=Line_discipline.set_modem.ttyCooked}|Print(s,k)->advance{mwithprogram=k;outbox=add(Line_discipline.outputs)m.outbox}|Random(n,k)->leti,seed=random_int0(n-1)m.seedinadvance{mwithprogram=ki;seed}|Spawn(child,k)->advance{mwithprogram=child;parents=k::m.parents}|Done()whenm.parents<>[]->exit_childmExited|Read_key_->flush{mwithtty=Line_discipline.set_modem.ttyRaw}|Read_line_|Done()->flush{mwithtty=Line_discipline.set_modem.ttyCooked}(* the innermost program over: its parent carries on, told how *)andexit_child(m:machine)(st:status):machine=matchm.parentswith|k::parents->advance{mwithprogram=kst;parents}|[]->advance{mwithprogram=Done()}letstart?baud~(seed:int)~(rows:int)~(cols:int)(program:unittalk):machine=advance{program;parents=[];vt=Vt.create~rows~cols;tty=Line_discipline.create();outbox=[];(* a character is 10 bits on the line (a start bit, 8, a stop
bit); the Model 33 had two stop bits, so its 110 baud were
exactly 10 characters a second *)cps=Option.map(funb->float_of_intb/.ifb=110then11.else10.)baud;budget=0.;seed=initial_seedseed;steps=0;}(* a key at a time, since a line or a key given to the program can
change the tty's mode for the next one *)letinput(m:machine)(bytes:string):machine=letone(m:machine)(key:string):machine=lettty,echo,events=Line_discipline.inputm.ttykeyinletm={mwithtty;outbox=addechom.outbox}inList.fold_left(fun(m:machine)(ev:Line_discipline.event)->matchev,m.programwith|Linel,Read_linek->advance{mwithprogram=kl}|Keys,Read_keyk->advance{mwithprogram=ks}|(Interrupt|End_of_file),(Read_line_|Read_key_|Step_)->exit_childmInterrupted(* typed ahead of a question, or after the end: dropped *)|_->m)meventsinflush(List.fold_leftonem(Line_discipline.split_keysbytes))lettick(m:machine)(dt:float):machine=(* a new frame: the steps counted again, a computing program resumed *)letm=matchm.programwithStep_->advance{mwithsteps=0}|_->{mwithsteps=0}inmatchm.cpswith|None->m|Some_whenm.outbox=[]->{mwithbudget=0.}|Somecps->letbudget=m.budget+.(dt*.cps)inlettext=pendingminletn=min(int_of_floatbudget)(String.lengthtext)inletrest=String.subtextn(String.lengthtext-n)in{mwithvt=Vt.feedm.vt(String.subtext0n);outbox=addrest[];budget=budget-.float_of_intn}letscreen(m:machine)=m.vtletreading(m:machine)=m.outbox=[]&&matchm.programwithRead_line_|Read_key_->true|_->falseletfinished(m:machine)=m.outbox=[]&&matchm.programwithDone()->true|_->false