Is it be possible to write a macro like sscanf!"{String}: {Nat} - {Nat}" that would return an Option (String x Nat x Nat) the tuple type being the same as whatever was in the format string? Rust has a crate for this which I use a lot and I would like to have it in lean.
Yeah, you should be able to write such a macro without too much work. You could use interpStr to parse these curly brace strings in a general way, but you'd have to figure out how to encode parsing options somehow.
importLeanopenLeanElabdeclare_syntax_catparsecTermdeclare_syntax_catparsecSpecsyntax(term)?(" : "parsecSpec)?:parsecTermsyntax(name:=parsecStr)"parsec!"interpolatedStr(parsecTerm):termsyntax"r16":parsecSpec/-- Represents the data after the `:` in a `parsec!` string,along with other incidental configuration. -/structureParsecSpecwhere/-- If not `none`, parse without including this character. This comes from the character immediately after the `}`. -/untilChar?:OptionCharradix:Nat:=10instance:QuoteCharwherequotec:=letdigs:=Nat.toDigits16c.toNatletdigs:=(List.replicate(4-digs.length)'0')++digsletn:=String.mkdigsSyntax.mkLitcharLitKinds!"'\\u{n}'"instance:QuoteParsecSpecwherequotespec:=Unhygienic.run`(ParsecSpec.mk$(quotespec.untilChar?)$(quotespec.radix))/-- Predicate for the character not being the one in `untilChar?`. -/defParsecSpec.notUntilChar(spec:ParsecSpec)(c:Char):Bool:=spec.untilChar?|>.map(·!=c)|>.getDtrue/-- Class for associating parsers to types. -/classParsecParse(α:Type)whereparse:ParsecSpec→Lean.Parsecαinstance:ParsecParseNatwhereparsespec:=do-- TODO: Should handle `spec.radix` in here.letchars←Parsec.many1Chars(Parsec.satisfyfunc=>c.isDigit&&spec.notUntilCharc)returnchars.toNat!instance:ParsecParseStringwhereparsespec:=Parsec.manyChars(Parsec.satisfyspec.notUntilChar)/-- Modify the `spec` given the contents of `specStx?`. -/defprocessParsecSpec(specStx?:Option(TSyntax`parsecSpec))(spec:ParsecSpec):MacroMParsecSpec:=matchspecStx?with|somespecStx=>matchspecStxwith|`(parsecSpec|r16)=>return{specwithradix:=16}|_=>Macro.throwUnsupported|none=>returnspecdefprocessParsecTerm(term:TSyntax`parsecTerm)(untilChar?:OptionChar):MacroM(Term×ParsecSpec):=domatchtermwith|`(parsecTerm|$[$ty?]?$[:$spec?]?)=>letspec←processParsecSpecspec?{untilChar?}letty←ifletsomety:=ty?thenpuretyelse`(_)return(ty,spec)|_=>Macro.throwUnsupporteddefmkTuple(xs:ArrayTerm):MacroMTerm:=matchxswith|#[]=>`(())|#[x]=>returnx|_=>`(($(xs[0]!),$(xs[1:]),*))defexpandChunks(chunks:ArraySyntax):MacroMTerm:=withFreshMacroScopedo-- Array of binder/parser pairsletmutparsers:List(OptionTerm×Term):=[]letmutres:ListTerm:=[]letmutuntilChar?:OptionChar:=noneforjin[0:chunks.size]do-- Reverse order for `untilChar?` handlingleti:=chunks.size-1-jletelem:=chunks[i]!matchelem.isInterpolatedStrLit?with|none=>letn:=mkIdentFromelem(←MonadQuotation.addMacroScope(Name.appendIndexAfter`ni))let(ty,spec)←processParsecTerm⟨elem⟩untilChar?parsers:=(n,←`((ParsecParse.parse$(quotespec):Parsec$ty)))::parsersres:=n::resuntilChar?:=none|somestr=>ifstr.utf8ByteSize>0thenparsers:=(none,←`(Parsec.skipString$(quotestr)))::parsersuntilChar?:=str.get?⟨0⟩letres'←`(pure$(←mkTupleres.toArray))parsers.foldrM(init:=res')fun(binder?,parser)t=>doifletsomebinder:=binder?then`($parser>>=fun$binder=>$t)else`($parser*>$t)macro_rules|`(parsec!$str)=>dolett←expandChunksstr.raw.getArgsreturnt#checkparsec!"{String}: {Nat} - {Nat}"/-do let n_1 ← ParsecParse.parse { untilChar? := some (Char.ofNat 58) } SeqRight.seqRight (Parsec.skipString ": ") fun x ↦ do let n_3 ← ParsecParse.parse { untilChar? := some (Char.ofNat 32) } SeqRight.seqRight (Parsec.skipString " - ") fun x ↦ do let n_5 ← ParsecParse.parse { untilChar? := none } pure (n_1, n_3, n_5) : Parsec (String × Nat × Nat)-/#eval(parsec!"{String}: {Nat} - {Nat}").run"entry1: 1 - 2"/-Except.ok ("entry1", 1, 2)-/-- This works too:#eval(parsec!"{}: {} - {}":Parsec(String×Nat×Nat)).run"entry1: 1 - 2"
This looks great, I wasn't expecting a working prototype within an hour! I am still working through the macro docs so it'll take me a bit to understand all of this.