123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400(* 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 Js_parse.mli *)openJs_asttypeerror={line:int;message:string}exceptionErroroferror(* the tokens, and where the parser is in them *)typet={tokens:Js_lexer.tokenarray;mutablepos:int}(*****************************************************************************)(* Tokens *)(*****************************************************************************)letpeek(p:t):Js_lexer.token=p.tokens.(minp.pos(Array.lengthp.tokens-1))letpeek_at(p:t)(k:int):Js_lexer.token=p.tokens.(min(p.pos+k)(Array.lengthp.tokens-1))letadvance(p:t):Js_lexer.token=lett=peekpinp.pos<-p.pos+1;tletdescribe(k:Js_lexer.kind):string=matchkwith|Keywordw|Namew|Punctw->Printf.sprintf"'%s'"w|Numberf->Js_ast.number_to_stringf|Strings->Printf.sprintf"%S"s|Regex(r,f)->Printf.sprintf"/%s/%s"rf|Eof->"the end"letfail(p:t)(message:string)=raise(Error{line=(peekp).line;message})letunexpected(p:t)(what:string)=failp(Printf.sprintf"expected %s, not %s"what(describe(peekp).kind))letis_punct(p:t)(s:string):bool=(peekp).kind=Punctsletis_keyword(p:t)(s:string):bool=(peekp).kind=Keywordsletexpect(p:t)(s:string):unit=ifis_punctpsthenignore(advancep)elseunexpectedp(Printf.sprintf"'%s'"s)letname(p:t):string=match(advancep).kindwith|Namex->x|_->p.pos<-p.pos-1;unexpectedp"a name"(*****************************************************************************)(* Expressions: Pratt *)(*****************************************************************************)(* an infix operator's binding power, and whether it is right-associative *)letinfix(op:string):(int*bool)option=matchopwith|"="|"+="|"-="|"*="|"/="|"%="->Some(1,true)|"?"->Some(2,true)|"||"->Some(3,false)|"&&"->Some(4,false)|"==="|"!=="|"=="|"!="->Some(5,false)|"<"|">"|"<="|">="->Some(6,false)|"+"|"-"->Some(7,false)|"*"|"/"|"%"->Some(8,false)|_->Noneletprefix_power=9letpostfix_power=10(* what an assignment or ++ may change *)lettarget(p:t)(e:expr):expr=matchewithName_|Member_|Index_->e|_->failp"that cannot be assigned to"(* the index of the ")" matching the "(" at [p.pos], if any *)letclosing(p:t):intoption=letrecgoidepth=matchp.tokens.(i).kindwith|Eof->None|Punct("("|"["|"{")->go(i+1)(depth+1)|Punct(")"|"]"|"}")->ifdepth=1thenSomeielsego(i+1)(depth-1)|_->go(i+1)depthingop.pos0(* an arrow ahead: "x =>", or "( ... ) =>" *)letarrow_ahead(p:t):bool=match((peekp).kind,(peek_atp1).kind)with|Name_,Punct"=>"->true|Punct"(",_->(matchclosingpwithSomei->p.tokens.(i+1).kind=Punct"=>"|None->false)|_->falseletrecexpression(p:t)(min:int):expr=letleft=prefixpinlooppminleft(* the operators binding at least as tight as [min], each taking the
* left side so far *)andloop(p:t)(min:int)(left:expr):expr=lett=peekpinmatcht.kindwith|Punct"."whenpostfix_power>=min->ignore(advancep);(* a keyword is a property name too: o.default, e.catch *)letx=match(advancep).kindwithNamex|Keywordx->x|_->p.pos<-p.pos-1;unexpectedp"a property name"inlooppmin(Member(left,x))|Punct"["whenpostfix_power>=min->ignore(advancep);leti=expressionp0inexpectp"]";looppmin(Index(left,i))|Punct"("whenpostfix_power>=min->ignore(advancep);letargs=argumentspinlooppmin(Call(left,args))(* x++, but not x on one line and ++y on the next: [no LineTerminator here] *)(* x instanceof F: as tight as < *)|Keyword"instanceof"when6>=min->ignore(advancep);looppmin(Binary("instanceof",left,expressionp7))|Punct(("++"|"--")asop)whenpostfix_power>=min&¬t.newline_before->ignore(advancep);looppmin(Update(op,false,targetpleft))|Punctop->(matchinfixopwith|Some(power,right)whenpower>=min->ignore(advancep);letnext=ifrightthenpowerelsepower+1inlete=matchopwith|"?"->(* the middle as if in parentheses, then the rest *)leta=expressionp0inexpectp":";Conditional(left,a,expressionpnext)|"="|"+="|"-="|"*="|"/="|"%="->Assign(op,targetpleft,expressionpnext)|"&&"|"||"->Logical(op,left,expressionpnext)|_->Binary(op,left,expressionpnext)inlooppmine|_->left)|_->left(* what can start an expression *)andprefix(p:t):expr=ifarrow_aheadpthenarrowpelselett=advancepinmatcht.kindwith|Numberf->Numberf|Strings->Strings|Namex->Namex|Keyword"true"->Booltrue|Keyword"false"->Boolfalse|Keyword"null"->Null|Keyword"this"->This|Keyword"typeof"->Unary("typeof",expressionpprefix_power)|Keyword"function"->Function(funcp~arrow:false)|Punct(("-"|"+"|"!")asop)->Unary(op,expressionpprefix_power)|Punct(("++"|"--")asop)->Update(op,true,targetp(expressionpprefix_power))|Punct"("->lete=expressionp0inexpectp")";e|Punct"["->letrecelementsacc=ifis_punctp"]"thenList.revaccelselete=expressionp1inifis_punctp","then(ignore(advancep);elements(e::acc))elseList.rev(e::acc)inletes=elements[]inexpectp"]";Arrayes|Punct"{"->letrecmembersacc=ifis_punctp"}"thenList.revaccelseletkey=match(advancep).kindwith|Namek|Keywordk|Stringk->k|Numberf->Js_ast.number_to_stringf|_->p.pos<-p.pos-1;unexpectedp"a property name"inexpectp":";letv=expressionp1inifis_punctp","then(ignore(advancep);members((key,v)::acc))elseList.rev((key,v)::acc)inletkvs=members[]inexpectp"}";Objectkvs|Regex(r,f)->Regex(r,f)(* new F(a), new F: F a name and its members, not a call *)|Keyword"new"->letrecmemberse=match(peekp).kindwith|Punct"."->(ignore(advancep);match(advancep).kindwithNamex|Keywordx->members(Member(e,x))|_->p.pos<-p.pos-1;unexpectedp"a property name")|Punct"["->ignore(advancep);leti=expressionp0inexpectp"]";members(Index(e,i))|_->einletcallee=members(prefixp)inletargs=ifis_punctp"("then(ignore(advancep);argumentsp)else[]inNew(callee,args)|Keyword("class"ask)->p.pos<-p.pos-1;failp(Printf.sprintf"%s is not supported here (an exercise: prototypes and new are)"k)|_->p.pos<-p.pos-1;unexpectedp"an expression"(* f(a, b): what is after the "(" *)andarguments(p:t):exprlist=letrecgoacc=ifis_punctp")"then(ignore(advancep);List.revacc)elselete=expressionp1inifis_punctp","then(ignore(advancep);go(e::acc))else(expectp")";List.rev(e::acc))ingo[](* "(a, b)": the parameters *)andparams(p:t):stringlist=expectp"(";letrecgoacc=ifis_punctp")"then(ignore(advancep);List.revacc)elseletx=namepinifis_punctp","then(ignore(advancep);go(x::acc))else(expectp")";List.rev(x::acc))ingo[](* x => ..., (a, b) => ...: a body in braces, or an expression returned *)andarrow(p:t):expr=letps=ifis_punctp"("thenparamspelse[namep]inletline=(peekp).lineinexpectp"=>";letbody=ifis_punctp"{"thenblock_bodypelse[{line;stmt=Return(Some(expressionp1))}]inFunction{name=None;params=ps;body;arrow=true}(* function name? (params) { body }: what is after the keyword *)andfunc(p:t)~(arrow:bool):func=letname=match(peekp).kindwithNamex->ignore(advancep);Somex|_->Noneinletps=paramspin{name;params=ps;body=block_bodyp;arrow}(*****************************************************************************)(* Statements: recursive descent *)(*****************************************************************************)(* "{ statements }" *)andblock_body(p:t):stmtlist=expectp"{";letrecgoacc=ifis_punctp"}"then(ignore(advancep);List.revacc)elsego(statementp::acc)ingo[](* a statement's end: ";", or before "}", the end, or a new line *)andend_statement(p:t):unit=lett=peekpinifis_punctp";"thenignore(advancep)elseifis_punctp"}"||t.kind=Eof||t.newline_beforethen()elseunexpectedp"';' or a new line"andlet_kind(k:string):let_kind=matchkwith"const"->Const_kind|"var"->Var_kind|_->Let_kind(* let a = 1, b: after the keyword *)anddeclarations(p:t):(string*exproption)list=letrecgoacc=letx=namepinletinit=ifis_punctp"="then(ignore(advancep);Some(expressionp1))elseNoneinifis_punctp","then(ignore(advancep);go((x,init)::acc))elseList.rev((x,init)::acc)ingo[]andstatement(p:t):stmt=lett=peekpinletline=t.lineinletsstmt={line;stmt}inmatcht.kindwith|Punct";"->ignore(advancep);sEmpty|Punct"{"->s(Block(block_bodyp))|Keyword(("let"|"const"|"var")ask)->ignore(advancep);letds=declarationspinend_statementp;s(Let(let_kindk,ds))|Keyword"function"->ignore(advancep);letf=funcp~arrow:falseiniff.name=Nonethenfailp"a function declaration needs a name";s(Function_declf)|Keyword"return"->ignore(advancep);(* return, then a new line: returns nothing *)letnext=peekpinletvalue=ifis_punctp";"||is_punctp"}"||next.kind=Eof||next.newline_beforethenNoneelseSome(expressionp0)inend_statementp;s(Returnvalue)|Keyword"if"->ignore(advancep);expectp"(";letc=expressionp0inexpectp")";leta=statementpinletb=ifis_keywordp"else"then(ignore(advancep);Some(statementp))elseNoneins(If(c,a,b))|Keyword"while"->ignore(advancep);expectp"(";letc=expressionp0inexpectp")";s(While(c,statementp))|Keyword"for"->ignore(advancep);expectp"(";s(for_restp)|Keyword"break"->ignore(advancep);end_statementp;sBreak|Keyword"continue"->ignore(advancep);end_statementp;sContinue|Keyword"throw"->ignore(advancep);lete=expressionp0inend_statementp;s(Throwe)|Keyword"try"->ignore(advancep);letbody=block_bodypinifnot(is_keywordp"catch")thenunexpectedp"catch (finally: an exercise)";ignore(advancep);expectp"(";letx=namepinexpectp")";lethandler=block_bodypinifis_keywordp"finally"thenfailp"finally is not supported here (an exercise)";s(Try(body,x,handler))|Keyword(("class"|"switch"|"do"|"delete"|"in"|"instanceof"|"void")ask)->failp(Printf.sprintf"'%s' is not supported here (plan_tiny_firefox.md: what is left out)"k)|_->lete=expressionp0inend_statementp;s(Expre)(* for (let x of xs) body, or for (init; test; update) body: what is
* after the "(" *)andfor_rest(p:t):statement=match((peekp).kind,(peek_atp1).kind,(peek_atp2).kind)with|Keyword(("let"|"const"|"var")ask),Namex,Name"of"->p.pos<-p.pos+3;letxs=expressionp0inexpectp")";For_of(let_kindk,x,xs,statementp)|_->letline=(peekp).lineinletinit=match(peekp).kindwith|Punct";"->None|Keyword(("let"|"const"|"var")ask)->ignore(advancep);Some{line;stmt=Let(let_kindk,declarationsp)}|_->Some{line;stmt=Expr(expressionp0)}inexpectp";";lettest=ifis_punctp";"thenNoneelseSome(expressionp0)inexpectp";";letupdate=ifis_punctp")"thenNoneelseSome(expressionp0)inexpectp")";For(init,test,update,statementp)(*****************************************************************************)(* Entry points *)(*****************************************************************************)letwith_tokens(text:string)(f:t->'a):('a,error)result=matchJs_lexer.tokenizetextwith|exceptionJs_lexer.Error(line,message)->Error{line;message}|tokens->(letp={tokens=Array.of_listtokens;pos=0}inmatchfpwithx->Okx|exceptionErrore->Errore)letparse(text:string):(program,error)result=with_tokenstext(funp->letrecgoacc=if(peekp).kind=EofthenList.revaccelsego(statementp::acc)ingo[])letparse_expression(text:string):(expr,error)result=with_tokenstext(funp->lete=expressionp0inif(peekp).kind<>Eofthenunexpectedp"the end";e)