123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439(* 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 Basic_parse.mli *)(*****************************************************************************)(* Types *)(*****************************************************************************)typeop=Add|Sub|Mul|Div|Pow|Eq|Ne|Lt|Le|Gt|Ge|And|Ortypeexpr=|Numoffloat|Strofstring|Varofstring|Indexofstring*exprlist|Callofstring*exprlist|Fnofstring*expr|Negofexpr|Notofexpr|Binofop*expr*exprtypeitem=Exprofexpr|Tabofexpr|Spcofexprtypesep=Semi|Comma|Newlinetypelvalue=Scalarofstring|Elemofstring*exprlisttypedatum=D_numoffloat|D_strofstringtypestmt=|Printof(item*sep)list|Inputofstringoption*lvaluelist|Letoflvalue*expr|Ifofexpr|Gotoofexpr|Gosubofexpr|Onofexpr*bool*intlist|Return|Forofstring*expr*expr*exproption|Nextofstringlist|Dimof(string*exprlist)list|Dataofdatumlist|Readoflvaluelist|Restore|Defofstring*string*expr|End|Stop|Rem|List|Run|New|Bye|Fp|Int|Catalog|Loadofstring|Saveofstring|Run_fileofstringtypeline=Numberedofint*stmtlistoption|Directofstmtlistletfunctions=["INT";"ABS";"SGN";"SQR";"RND";"SIN";"COS";"TAN";"ATN";"EXP";"LOG";"LEN";"VAL";"ASC";"CHR$";"STR$";"LEFT$";"RIGHT$";"MID$"](* the words the cruncher finds anywhere, even inside a name *)letkeywords=["PRINT";"INPUT";"LET";"IF";"THEN";"GOTO";"GOSUB";"ON";"RETURN";"FOR";"TO";"STEP";"NEXT";"DIM";"DATA";"READ";"RESTORE";"DEF";"FN";"END";"STOP";"REM";"AND";"OR";"NOT";"TAB(";"SPC("]@functionsletis_string(name:string):bool=name<>""&&name.[String.lengthname-1]='$'(*****************************************************************************)(* Characters *)(*****************************************************************************)letcapitals(s:string):string=letinside=reffalseinString.map(func->ifc='"'theninside:=not!inside;if!insidethencelseChar.uppercase_asciic)s(* the line being read, and where *)typep={s:string;mutablei:int}exceptionSyntaxofstringletfail(msg:string)=raise(Syntaxmsg)letskip(p:p):unit=whilep.i<String.lengthp.s&&p.s.[p.i]=' 'dop.i<-p.i+1done(* the next character that isn't a space, not consumed *)letpeek(p:p):charoption=skipp;ifp.i<String.lengthp.sthenSomep.s.[p.i]elseNoneletat_end(p:p):bool=peekp=None(* the end of a statement: the line's, or a ":" *)letat_stmt_end(p:p):bool=matchpeekpwithNone|Some':'->true|_->falseletaccept(p:p)(c:char):bool=ifpeekp=Somecthenbeginp.i<-p.i+1;trueendelsefalseletexpect(p:p)(c:char):unit=ifnot(acceptpc)thenfail(Printf.sprintf"%c EXPECTED"c)(* a keyword, spaces allowed between its letters as in 1976 (G O T O
is GOTO); nothing consumed if it isn't there *)letkeyword(p:p)(k:string):bool=letstart=p.iinletok=String.for_all(func->acceptpc)kinifnotokthenp.i<-start;ok(* whether a keyword starts here, nothing consumed *)letlooking_at_keyword(p:p):bool=List.exists(funk->letstart=p.iinletfound=keywordpkinp.i<-start;found)keywordsletis_digitc=c>='0'&&c<='9'letis_letterc=c>='A'&&c<='Z'(*****************************************************************************)(* Numbers and names *)(*****************************************************************************)(* digits, a point, more digits, an exponent: 12, 3.5, .25, 1E6, 2.5E-3 *)letnumber(p:p):float=skipp;letstart=p.iinletdigits()=whilep.i<String.lengthp.s&&is_digitp.s.[p.i]dop.i<-p.i+1doneindigits();ifp.i<String.lengthp.s&&p.s.[p.i]='.'thenbeginp.i<-p.i+1;digits()end;ifp.i<String.lengthp.s&&p.s.[p.i]='E'thenbeginletbefore=p.iinp.i<-p.i+1;ifp.i<String.lengthp.s&&(p.s.[p.i]='+'||p.s.[p.i]='-')thenp.i<-p.i+1;letdigits_start=p.iindigits();(* an E not followed by digits isn't an exponent *)ifp.i=digits_startthenp.i<-beforeend;matchfloat_of_string_opt(String.subp.sstart(p.i-start))with|Somefwhenp.i>start->f|_->fail"NUMBER EXPECTED"letline_number(p:p):int=letf=numberpinifFloat.is_integerf&&f>=0.&&f<=63999.thenint_of_floatfelsefail"BAD LINE NUMBER"(* a letter, then letters and digits until a keyword starts (the
cruncher's reading), then a $ for a string's *)letname(p:p):string=matchpeekpwith|Somecwhenis_letterc&¬(looking_at_keywordp)->letstart=p.iinp.i<-p.i+1;whilep.i<String.lengthp.s&&(is_letterp.s.[p.i]||is_digitp.s.[p.i])&¬(looking_at_keywordp)dop.i<-p.i+1done;ifp.i<String.lengthp.s&&p.s.[p.i]='$'thenp.i<-p.i+1;String.subp.sstart(p.i-start)|_->fail"VARIABLE EXPECTED"letstring_lit(p:p):string=expectp'"';matchString.index_from_optp.sp.i'"'with|Somej->lets=String.subp.sp.i(j-p.i)inp.i<-j+1;s|None->(* Microsoft allowed a string left open at the end of the line *)lets=String.subp.sp.i(String.lengthp.s-p.i)inp.i<-String.lengthp.s;s(*****************************************************************************)(* Expressions *)(*****************************************************************************)(* from the loosest to the tightest: OR, AND, NOT, comparisons, + -,
* /, unary -, ^ *)letrecexpr(p:p):expr=letrecmoreacc=ifkeywordp"OR"thenmore(Bin(Or,acc,conjp))elseaccinmore(conjp)andconj(p:p):expr=letrecmoreacc=ifkeywordp"AND"thenmore(Bin(And,acc,negationp))elseaccinmore(negationp)andnegation(p:p):expr=ifkeywordp"NOT"thenNot(negationp)elsecomparisonpandcomparison(p:p):expr=letrelop()=ifacceptp'<'thenSome(ifacceptp'>'thenNeelseifacceptp'='thenLeelseLt)elseifacceptp'>'thenSome(ifacceptp'='thenGeelseifacceptp'<'thenNeelseGt)elseifacceptp'='thenSome(ifacceptp'<'thenLeelseifacceptp'>'thenGeelseEq)elseNoneinletrecmoreacc=matchrelop()withSomeop->more(Bin(op,acc,sump))|None->accinmore(sump)andsum(p:p):expr=letrecmoreacc=ifacceptp'+'thenmore(Bin(Add,acc,productp))elseifacceptp'-'thenmore(Bin(Sub,acc,productp))elseaccinmore(productp)andproduct(p:p):expr=letrecmoreacc=ifacceptp'*'thenmore(Bin(Mul,acc,unaryp))elseifacceptp'/'thenmore(Bin(Div,acc,unaryp))elseaccinmore(unaryp)(* -2^2 is -4: the power binds tighter than the sign *)andunary(p:p):expr=ifacceptp'-'thenNeg(unaryp)elseifacceptp'+'thenunarypelsepowerpandpower(p:p):expr=letrecmoreacc=ifacceptp'^'thenmore(Bin(Pow,acc,ifacceptp'-'thenNeg(atomp)elseatomp))elseaccinmore(atomp)andargs(p:p):exprlist=expectp'(';letrecrestacc=ifacceptp','thenrest(exprp::acc)elseList.revaccinletfirst=exprpinletl=rest[first]inexpectp')';landatom(p:p):expr=matchpeekpwith|Some'('->p.i<-p.i+1;lete=exprpinexpectp')';e|Some'"'->Str(string_litp)|Somecwhenis_digitc||c='.'->Num(numberp)|Some_whenkeywordp"FN"->letf=namepin(matchargspwith[e]->Fn(f,e)|_->fail"ONE ARGUMENT EXPECTED")|Some_->(matchList.find_opt(funf->keywordpf)functionswith|Somef->Call(f,argsp)|None->letv=namepinifpeekp=Some'('thenIndex(v,argsp)elseVarv)|None->fail"EXPRESSION EXPECTED"(*****************************************************************************)(* Statements *)(*****************************************************************************)letlvalue(p:p):lvalue=letv=namepinifpeekp=Some'('thenElem(v,argsp)elseScalarvletreccomma_list(f:p->'a)(p:p):'alist=letx=fpinifacceptp','thenx::comma_listfpelse[x](* PRINT's items and separators; an item right after another (PRINT
"X"A) is joined as by ";" *)letrecprint_items(p:p):(item*sep)list=ifat_stmt_endpthen[]elseifacceptp','then(Expr(Str""),Comma)::print_itemspelseifacceptp';'thenprint_itemspelseletitem=ifkeywordp"TAB("thenbeginlete=exprpinexpectp')';Tabeendelseifkeywordp"SPC("thenbeginlete=exprpinexpectp')';SpceendelseExpr(exprp)inifacceptp','then(item,Comma)::print_itemspelseifacceptp';'then(item,Semi)::print_itemspelseifat_stmt_endpthen[(item,Newline)]else(item,Semi)::print_itemsp(* a DATA item: a number, a quoted string, or anything up to the next
comma, trimmed *)letdatum(p:p):datum=ifpeekp=Some'"'thenD_str(string_litp)elsebeginletstart=p.iinwhilep.i<String.lengthp.s&&p.s.[p.i]<>','&&p.s.[p.i]<>':'dop.i<-p.i+1done;lets=String.trim(String.subp.sstart(p.i-start))inmatchfloat_of_string_optswithSomefwhens<>""->D_numf|_->D_strsend(* a statement, and for IF what THEN starts: the list is what the line
goes on with *)letrecstatement(p:p):stmtlist=(* the commands, alone on their line *)letcommandkc=ifkeywordpk&&at_endpthenSomecelseNoneinletcommands=[("LIST",List);("RUN",Run);("NEW",New);("BYE",Bye);("FP",Fp);("INT",Int)]inletstart=p.iin(* and the disk's, followed by a file's name *)letfilekc=p.i<-start;ifkeywordpk&¬(at_endp)thenbeginletname=String.trim(String.subp.sp.i(String.lengthp.s-p.i))inp.i<-String.lengthp.s;Some(cname)endelseNoneinletfiles=[("LOAD",funn->Loadn);("SAVE",funn->Saven);("RUN",funn->Run_filen)]inmatchmatchList.find_map(fun(k,c)->p.i<-start;commandkc)(("CATALOG",Catalog)::commands)with|Somec->Somec|None->List.find_map(fun(k,c)->filekc)fileswith|Somec->[c]|None->p.i<-start;ifkeywordp"PRINT"then[Print(print_itemsp)]elseifacceptp'?'then[Print(print_itemsp)]elseifkeywordp"INPUT"thenbeginletprompt=ifpeekp=Some'"'thenbeginlets=string_litpinifnot(acceptp';'||acceptp',')thenfail"; EXPECTED";SomesendelseNonein[Input(prompt,comma_listlvaluep)]endelseifkeywordp"IF"thenbeginletc=exprpinifkeywordp"THEN"thenmatchpeekpwithSomedwhenis_digitd->[Ifc;Goto(Num(float_of_int(line_numberp)))]|_->Ifc::statementpelseifkeywordp"GOTO"then[Ifc;Goto(Num(float_of_int(line_numberp)))]elsefail"THEN EXPECTED"endelseifkeywordp"GOTO"then[Goto(exprp)]elseifkeywordp"GOSUB"then[Gosub(exprp)]elseifkeywordp"ON"thenbeginlete=exprpinletsub=ifkeywordp"GOSUB"thentrueelseifkeywordp"GOTO"thenfalseelsefail"GOTO EXPECTED"in[On(e,sub,comma_listline_numberp)]endelseifkeywordp"RETURN"then[Return]elseifkeywordp"FOR"thenbeginletv=namepinexpectp'=';leta=exprpinifnot(keywordp"TO")thenfail"TO EXPECTED";letb=exprpinletstep=ifkeywordp"STEP"thenSome(exprp)elseNonein[For(v,a,b,step)]endelseifkeywordp"NEXT"then[Next(ifat_stmt_endpthen[]elsecomma_listnamep)]elseifkeywordp"DIM"then[Dim(comma_list(funp->letv=namepin(v,argsp))p)]elseifkeywordp"DATA"then[Data(comma_listdatump)]elseifkeywordp"READ"then[Read(comma_listlvaluep)]elseifkeywordp"RESTORE"then[Restore]elseifkeywordp"DEF"thenbeginifnot(keywordp"FN")thenfail"FN EXPECTED";letf=namepinexpectp'(';letx=namepinexpectp')';expectp'=';[Def(f,x,exprp)]endelseifkeywordp"END"then[End]elseifkeywordp"STOP"then[Stop]elseifkeywordp"REM"thenbeginp.i<-String.lengthp.s;[Rem]endelsebegin(* the LET left out, as every BASIC after Dartmouth's allowed *)ignore(keywordp"LET");letv=lvaluepinexpectp'=';[Let(v,exprp)]end(* statements separated by ":" *)letrecstatements(p:p):stmtlist=letfirst=statementpinifacceptp':'thenfirst@statementspelseifat_endpthenfirstelsefail"SYNTAX"(*****************************************************************************)(* Lines *)(*****************************************************************************)letparse_line(line:string):(line,string)result=letp={s=capitalsline;i=0}intrymatchpeekpwith|Somecwhenis_digitc->letn=line_numberpinOk(ifat_endpthenNumbered(n,None)elseNumbered(n,Some(statementsp)))|_->Ok(Direct(statementsp))withSyntaxmsg->Errormsg