src/check.bend source
src/check.bend on the hub · documented module
# shake/check: the ways a Cli can contradict itself, internal (main.bend# exports `check`). `parse` does not call it: a program checks its spec once,# and every guarantee of SPEC.md's parse rows holds for a spec `check` passes.import Baseimport ./cli.bend as C# one way a spec contradicts itself, at the command path where it doestype SpecErr is Data: RestNotLast{path: List<&2, String>, name: String} SameName{path: List<&2, String>, name: String} SameShort{path: List<&2, String>, short: String} SameLong{path: List<&2, String>, long: String} SameSub{path: List<&2, String>, name: String} HelpSub{path: List<&2, String>} DefaultNotChoice{path: List<&2, String>, name: String, value: String} RequiredAfterOptional{path: List<&2, String>, name: String}# one report when the flag is set, else nonedef one(hit: Bool, err: SpecErr) -> List<&2, SpecErr>: match hit: case True{}: [err] case False{}: []# two lists of reports, one after the otherdef both(xs: List<&2, SpecErr>, ys: List<&2, SpecErr>) -> List<&2, SpecErr>: List.append(&2, SpecErr, xs, ys)# whether a spelling, when there is one, is among those already seendef spelled.seen(sp: Maybe<&2, String>, +seen: List<&2, String>) -> Bool: match sp: case None{}: False{} case Some{ss}: List.contains(~String, ~String.eq, seen, ss)# the spellings seen, with this one when there is onedef spelled.add(sp: Maybe<&2, String>, seen: List<&2, String>) -> List<&2, String>: match sp: case None{}: seen case Some{ss}: ss <> seen# a default outside a nonempty choices listdef args.default( dflt: Maybe<&2, String>, +cs: List<&2, String>, +path: List<&2, String>, +name: String) -> List<&2, SpecErr>: match dflt: case None{}: [] case Some{+vv}: one(Bool.and(Bool.not(List.is_empty(&2, String, cs)), Bool.not(C.allowed.ok(cs, vv))), DefaultNotChoice{path, name, vv})# each argument's name and spellings against the earlier ones', and its# default against its choicesdef args.dups( args: List<&2, C.Arg>, +path: List<&2, String>, +names: List<&2, String>, +shorts: List<&2, String>, +longs: List<&2, String>) -> List<&2, SpecErr>: match args: case Nil{}: [] case Con{hh, tt}: C.Arg{+name, +short, +long, _k, _h, _r, C.Fallback{_e, dflt}, +cs} = hh both(one(List.contains(~String, ~String.eq, names, name), SameName{path, name}), both(one(spelled.seen(short, shorts), SameShort{path, C.text.of(short, "")}), both(one(spelled.seen(long, longs), SameLong{path, C.text.of(long, "")}), both(args.default(dflt, cs, path, name), args.dups(tt, path, name <> names, spelled.add(short, shorts), spelled.add(long, longs))))))# the positionals in order: a rest before the last, and a required one after# an optional onedef pos.order( pos: List<&2, C.Arg>, +path: List<&2, String>, +opt_seen: Bool) -> List<&2, SpecErr>: match pos: case Nil{}: [] case Con{hh, +tt}: C.Arg{+name, _s, _l, kind, _h, +req, _d, _c} = hh both(one(Bool.and(C.rest.is.kind(kind), Bool.not(List.is_empty(&2, C.Arg, tt))), RestNotLast{path, name}), both(one(Bool.and(req, opt_seen), RequiredAfterOptional{path, name}), pos.order(tt, path, Bool.or(opt_seen, Bool.not(req)))))# each subcommand's name against the earlier ones', and against `help`def subs.dups( subs: List<&2, C.Sub>, +path: List<&2, String>, +names: List<&2, String>) -> List<&2, SpecErr>: match subs: case Nil{}: [] case Con{hh, tt}: C.Sub{+name, _a, _g, _c} = hh both(one(List.contains(~String, ~String.eq, names, name), SameSub{path, name}), both(one(String.eq(name, "help"), HelpSub{path}), subs.dups(tt, path, name <> names)))# one command's own reportsdef cmd( +path: List<&2, String>, +args: List<&2, C.Arg>, subs: List<&2, C.Sub>) -> List<&2, SpecErr>: both(args.dups(args, path, [], [], []), both(pos.order(C.pos_of(args), path, False{}), subs.dups(subs, path, [])))# a name in front of a reversed pathdef pushed(name: String, back: List<&2, String>) -> List<&2, String>: name <> back# every subcommand's reports, each under its own path, depth first. The path# is carried reversed, innermost name first, and read the right way round# once per commanddef forest(subs: List<&2, C.Sub>, +back: List<&2, String>) -> List<&2, SpecErr>: match subs: case Nil{}: [] case Con{hh, tt}: C.Sub{+name, _a, +args, +kids} = hh +here = pushed(name, back) both(cmd(List.reverse(&2, String, here), args, kids), both(forest(kids, here), forest(tt, back)))# every way the spec contradicts itself: the root's reports, then each# subcommand'sdef check(app: C.Cli) -> List<&2, SpecErr>: C.Cli{_n, _a, _v, +args, +subs} = app both(cmd([], args, subs), forest(subs, []))# where a report is: the root, or a subcommand's pathdef at.pick(root: Bool, +path: List<&2, String>) -> String: match root: case True{}: "spec: " case False{}: "spec, command '" ++ String.join(path, " ") ++ "': "# where a report isdef at(+path: List<&2, String>) -> String: at.pick(List.is_empty(&2, String, path), path)# a report as one linedef text(err: SpecErr) -> String: match err: case RestNotLast{path, name}: at(path) ++ "rest positional '" ++ name ++ "' is not the last positional" case SameName{path, name}: at(path) ++ "two arguments are named '" ++ name ++ "'" case SameShort{path, short}: at(path) ++ "two arguments are spelled '-" ++ short ++ "'" case SameLong{path, long}: at(path) ++ "two arguments are spelled '--" ++ long ++ "'" case SameSub{path, name}: at(path) ++ "two subcommands are named '" ++ name ++ "'" case HelpSub{path}: at(path) ++ "a subcommand is named 'help', which `parse` keeps for help" case DefaultNotChoice{path, name, value}: at(path) ++ "the default '" ++ value ++ "' of '" ++ name ++ "' is not one of its choices" case RequiredAfterOptional{path, name}: at(path) ++ "required positional '" ++ name ++ "' follows an optional one"