ZyX_I avatar ZyX_I committed 3e2c28e

@/fwc: Added ability to compile simple checks, added tests
@/fwc/compiler:
Fixed function that obtains argument list length limits
Added fwc_compile feature
Added {var} compilation
Added required arguments' checks and pipes ({expr} and {var} ones)
@/fwc/parser:
Added single-quoted strings where there are already double-quoted ones
Fixed getchvar() if variable is at the end of the string
getfunc() now supports comma-separated lists
function argument is either a dot or a list of {var}s now

Comments (0)

Files changed (6)

plugin/frawor/fwc/compiler.vim

 endif
 execute frawor#Setup('0.0', {'@/fwc/parser': '0.0'}, 1)
 let s:constructor={}
-"▶1 add        :: &self
-" Adds an element to current context
-function s:constructor.add(item)
-    call add(self.l, a:item)
+"▶1 Messages
+let s:_messages={
+            \'tooshort': 'Argument list is too short: '.
+            \            'expected at least %u, but got %u',
+            \'toolong' : 'Argument list is too long: '.
+            \            'expected at most %u, but got %u',
+            \'invlen'  : 'Invalid arguments length: expected %u, but got %u',
+        \}
+call extend(s:_messages, map({
+            \'funcfail': 'custom function returned 0',
+            \'exprfail': 'custom expression returned 0',
+        \}, '"Error while processing check %u for %s: ".v:val'))
+"▶1 string         :: String → String
+function s:F.string(str)
+    return substitute(substitute(substitute(string(a:str),
+                \"\n",     '''."\\n".''', 'g'),
+                \"^''\\.", "",            ""),
+                \"\\.''$", "",            "")
+endfunction
+"▶1 add            :: item, ... + self → self + self
+function s:constructor.add(...)
+    let self.l+=a:000
     return self
 endfunction
-"▶1 conclose   :: &self
-" Closes current context
-function s:constructor.conclose()
+"▶1 up             :: &self
+function s:constructor.up()
     call remove(self.stack, -1)
     let self.l=self.stack[-1]
     return self
 endfunction
-"▶1 addcon     :: ()[, conelement1[, ...]] + self → self + self
-" Adds new context with given elements
-function s:constructor.addcon(...)
-    let con=copy(a:000)
-    call self.add(con)
-    call add(self.stack, con)
-    let self.l=con
+"▶1 deeper         :: ()[, conelement1[, ...]] + self → self + self
+function s:constructor.deeper(...)
+    if type(get(self.l, -1))==type([])
+        let self.l[-1]+=a:000
+        call add(self.stack, self.l[-1])
+    else
+        let con=copy(a:000)
+        call self.add(con)
+        call add(self.stack, con)
+    endif
+    let self.l=self.stack[-1]
     return self
 endfunction
-"▶1 cleanup      :: list::[_, {arg}*] → + list
+"▶1 close          :: &self
+let s:cmdends={
+            \    'if': 'endif',
+            \'elseif': 'endif',
+            \'else'  : 'endif',
+            \'try'    : 'endtry',
+            \'catch'  : 'endtry',
+            \'finally': 'endtry',
+            \'function': 'endfunction',
+            \     'for': 'endfor',
+            \   'while': 'endwhile',
+            \'augroup': 'augroup END',
+            \  'redir': 'redir END',
+        \}
+function s:constructor.close()
+    let command=self.l[-2][:(stridx(self.l[-2], ' ')-1)]
+    if has_key(s:cmdends, command)
+        return self.add(s:cmdends[command])
+    else
+        return self
+    endif
+endfunction
+"▶1 nextcond       :: [condition] + self → self + self
+function s:constructor.nextcond(...)
+    if type(self.l[-1])==type([])
+        let prevline=self.l[-2]
+    else
+        let prevline=self.l[-1]
+    endif
+    let command=prevline[:(stridx(prevline, ' ')-1)]
+    if command is 'if' || command is 'elseif'
+        if a:0
+            return self.add('elseif '.a:1).deeper()
+        else
+            return self.add('else').deeper()
+        endif
+    else
+        if a:0
+            return self.add('if '.a:1).deeper()
+        else
+            return self
+        endif
+    endif
+endfunction
+"▶1 addthrow       :: msg::String, msgarg, ... + self → self + self
+function s:constructor.addthrow(msg, ...)
+    let args=map(copy(a:000), '((v:val is "@#@")?(string(self.argstr())):'.
+                \                               '(v:val))')
+    return self.add('call add(@$@messages, ['.string(a:msg).', '.
+                \                             join(args, ', ').'])',
+                \   'throw "CHECKFAILED"')
+endfunction
+"▶1 cleanup        :: list::[_, {arg}*] → + list
 function s:F.cleanup(list)
     if len(a:list)>1
         let args=remove(a:list, 1, -1)
                 if get(get(arg, 1, []), 0, "") is 'matcher'
                     let sq[arg[0].'matcher']=remove(arg, 1)[1:]
                 endif
-                call extend(sq[arg[0]], map(arg[1:], 'v:val[1:]'))
+                call extend(sq[arg[0]], filter(map(arg[1:], 'v:val[1:]'),
+                            \                  '!empty(v:val)'))
             elseif arg[0] is 'next'
-                call add(sq[arg[0]], arg[1:][0][1:])
+                if len(arg[1:][0])>1
+                    call add(sq[arg[0]], arg[1:][0][1:])
+                endif
             elseif arg[0] is 'optional'
-                call add(sq[arg[0]], arg)
+                if len(arg)>1
+                    call add(sq[arg[0]], arg)
+                endif
             else
-                call add(sq[arg[0]], arg[1:])
+                if len(arg)>1
+                    call add(sq[arg[0]], arg[1:])
+                endif
             endif
         endfor
         call filter(sq, '!empty(v:val)')
         for key in filter(['actions', 'optional'], 'has_key(sq, v:val)')
             call map(copy(sq[key]), 's:F.cleanup(v:val)')
-            call filter(sq[key], 'len(v:val)>1')
+            call filter(sq[key], '!empty(v:val)')
             if empty(sq[key])
                 call remove(sq, key)
             endif
         endif
     endif
 endfunction
-"▶1 getlenrange  :: adescr → (minlen, maxlen)
+"▶1 getlenrange    :: adescr → (minlen, maxlen) + adescr
 function s:F.getlenrange(adescr)
     let minimum=0
     let maximum=0
     endif
     "▶2 “actions” key
     if has_key(a:adescr, 'actions')
+        let minset=0
         let amin=0
         let amax=0
         let has0=0
             endif
             if len(action)>1
                 let [namin, namax]=s:F.getlenrange(action[1])
-                if namin>amin
+                if namin<amin || !minset
+                    let minset=1
                     let amin=namin
                 endif
                 if namax==-1
             let minimum+=((prefopts.opt)?
                         \   (0):
                         \   ((prefopts.list)?
-                        \       (0):
-                        \       (prefopts.argnum)))
+                        \       (2):
+                        \       (1+prefopts.argnum)))
         endfor
     endif
     "▶2 “next” key
         let maximum=-1
     endif
     "▲2
+    let a:adescr.minimum=minimum
+    let a:adescr.maximum=maximum
     return [minimum, maximum]
 endfunction
-"▶1 lencheck  :: &self
-"▶1 string
-function s:F.string(string)
+"▶1 lencheck       :: minlen, maxlen + self → self + self
+function s:constructor.lencheck(minimum, maximum)
+    if a:maximum==a:minimum
+        call self.nextcond('@$@largs isnot '.a:maximum)
+                    \.addthrow('invlen', a:minimum, '@$@largs')
+                    \.up()
+    else
+        if a:minimum>0
+            call self.nextcond('@$@largs<'.a:minimum)
+                        \.addthrow('tooshort', a:minimum, '@$@largs')
+                        \.up()
+        endif
+        if a:maximum!=-1
+            call self.nextcond('@$@largs>'.a:maximum)
+                        \.addthrow('toolong',  a:maximum, '@$@largs')
+                        \.up()
+        endif
+    endif
+    return self.close()
+endfunction
+"▶1 substostr      :: [subscript] → String
+function s:F.substostr(subscripts)
+    let r=""
+    for subs in a:subscripts
+        let tsubs=type(subs)
+        if tsubs==type("")
+            if subs=~#'^\w\+$'
+                let r.='.'.subs
+            else
+                let r.='['.s:F.string(subs).']'
+            endif
+        elseif tsubs==type(0)
+            let r.='['.subs.']'
+        elseif tsubs==type([])
+            let r.='['.join(subs, ':').']'
+        endif
+        unlet subs
+    endfor
+    return r
+endfunction
+"▶1 getfunc        :: funccontext[, addarg, ...] + self → String + self
+function s:constructor.getfunc(func, ...)
+    let r=self.getvar(a:func[1]).'('
+    let args=[]
+    let added000=0
+    for arg in a:func[2:]
+        if a:0 && arg[0] is 'this'
+            let args+=a:000
+            let added000=1
+        else
+            call add(args, self.getvar(arg))
+        endif
+    endfor
+    if a:0 && !added000
+        let args+=a:000
+    endif
+    let r.=join(args, ', ').')'
+    return r
+endfunction
+"▶1 getvar         :: varcontext + self → String + self
+function s:constructor.getvar(var)
+    if a:var[0] is 'plugvar'
+        return '@%@.p.'.a:var[1].s:F.substostr(a:var[2:])
+    elseif a:var[0] is 'expr'
+        return substitute(a:var[1], '\V@.@', self.argstr(), 'g')
+    elseif a:var[0] is 'string'
+        return s:F.string(a:var[1])
+    elseif a:var[0] is 'argument'
+        return self.argstr(self.nargs[-1]+a:var[1]).s:F.substostr(a:var[2:])
+    elseif a:var[0] is 'cur'
+        return self.argstr().s:F.substostr(self.subs[:-1-a:var[1]]+a:var[2:])
+    elseif a:var[0] is 'list'
+        let r='['
+        for item in a:var[1:]
+            let titem=type(item)
+            if titem==type('')
+                let r.=s:F.string(item)
+            elseif titem==type([])
+                if item[0] is 'expr'
+                    let r.=item[1]
+                elseif item[0] is 'func'
+                    let r.=self.getfunc(item)
+                else
+                    let r.=self.getvar(item)
+                endif
+            endif
+            let r.=', '
+            unlet item
+        endfor
+        let r.=']'
+        return r
+    elseif a:var[0] is 'evaluate'
+        return eval(substitute(self.getvar(a:var[1]), '@%@', 'self.vars', 'g'))
+    elseif a:var[0] is 'func'
+        return self.getfunc(a:var)
+    elseif a:var[0] is 'this'
+        return self.argstr()
+    endif
+endfunction
+"▶1 argstr         :: [narg] + self → String
+function s:constructor.argstr(...)
+    return '@@@['.get(a:000, 0, self.nargs[-1]).']'.s:F.substostr(self.subs)
+endfunction
+"▶1 compilemsg     :: msgcontext, _ + self → self + self
+function s:constructor.compilemsg(msg, idx)
+    let msg=[a:msg[1]]
+    for msgarg in a:msg[2:]
+        if msgarg[0] is 'curval'
+            call add(msg, self.argstr())
+        elseif msgarg[0] is 'curarg'
+            call add(msg, nargs[-1])
+        else
+            call add(msg, substitute(s:constructor.getvar(msgarg), '@#@',
+                        \            escape(self.argstr(), '\&~'), 'g'))
+        endif
+    endfor
+    return self
+endfunction
+"▶1 compilepipe    :: pipecontext, _ + self → self + self
+function s:constructor.compilepipe(pipe, idx)
+    let curargstr=self.argstr()
+    if a:pipe[1][0] is 'func'
+        call self.add('let '.curargstr.'='.
+                    \       self.getfunc(a:pipe[1], curargstr))
+    elseif a:pipe[1][0] is 'expr'
+        call self.add('let '.curargstr.'='.
+                    \       substitute(a:pipe[1][1],
+                    \                  '\V@.@', curargstr, 'g'))
+    else
+        " TODO
+    endif
+    return self
+endfunction
+let s:constructor.compilefilter=s:constructor.compilepipe
+"▶1 compilecheck   :: checkcontext, idx + self → self + self
+function s:constructor.compilecheck(check, idx)
+    let curargstr=self.argstr()
+    let addedcond=0
+    let idx=a:idx
+    for check in a:check[1:]
+        if check[0] is 'func'
+            let addedcond=1
+            call self.nextcond(self.getfunc(check, curargstr).' is 0')
+                        \.addthrow('funcfail', idx, '@#@')
+                        \.up()
+        elseif check[0] is 'expr'
+            let addedcond=1
+            call self.nextcond(substitute(check[1], '\V@.@', curargstr, 'g').
+                        \      ' is 0')
+                        \.addthrow('exprfail', idx, '@#@')
+                        \.up()
+        else
+            " TODO
+        endif
+        let idx+=1
+    endfor
+    if addedcond
+        call self.close()
+    endif
+    return self
+endfunction
+"▶1 compileintfunc :: intfunccontext, idx + self → self + self
+function s:constructor.compileintfunc(intfunc, idx)
+    return self['compile'.self.type]([self.type, a:intfunc], a:idx)
+endfunction
+"▶1 compileadesc   :: adescr + self → self + self
+function s:constructor.compileadesc(adescr)
+    if !has_key(a:adescr, 'minimum')
+        call s:F.getlenrange(a:adescr)
+    endif
+    if !has_key(a:adescr, 'checkedfor')
+        call self.lencheck(self.nargs[-1]+a:adescr.minimum,
+                    \      self.nargs[-1]+a:adescr.maximum)
+        let a:adescr.checkedfor=1
+    endif
+    if has_key(a:adescr, 'arg')
+        let check=['check']
+        for arg in a:adescr.arg
+            let msg=[]
+            let i=0
+            let previscheck=0
+            for proc in arg
+                if proc[0] is 'check'
+                    let previscheck=1
+                    call add(check, proc[1])
+                    let i+=1
+                    continue
+                elseif previscheck
+                    let previscheck=0
+                    call self.compilecheck(check, i+1-len(check))
+                    call remove(check, 1, -1)
+                endif
+                call call(self["compile".proc[0]], [proc, i], self)
+                let i+=1
+            endfor
+            if previscheck
+                call self.compilecheck(check, i+1-len(check))
+                call remove(check, 1, -1)
+            endif
+            let self.nargs[-1]+=1
+        endfor
+    endif
+    return self
+endfunction
+"▶1 tolstofstr     :: CTree::[String|CTree] → [String]
+function s:F.tolstofstr(ctree)
+    let r=[]
+    let items=map(a:ctree, '[0, v:val]')
+    while !empty(items)
+        let [indent, item]=remove(items, 0)
+        if type(item)==type([])
+            call extend(items, map(item, '['.(indent+1).', v:val]'), 0)
+        else
+            call add(r, repeat('    ', indent).item)
+        endif
+        unlet item
+    endwhile
+    return r
+endfunction
+"▶1 compilestr     :: String, type → [String]
+function s:F.compilestr(vars, string, type)
     "▶2 Setup self
-    let t   =   { 'tree': s:_r.fwc_parser.string(a:string)[1:],
-                \ 'type': 'check',
+    let t   =   { 'tree': s:_r.fwc_parser(a:string, a:type)[1:],
+                \ 'type': a:type,
                 \'ctree': [],
                 \'stack': [],
+                \'nargs': [0],
+                \ 'subs': [],
                 \    'l': 0,
-                \ 'argn': 0,
+                \ 'vars': a:vars,
+                \ 'vids': {},
                 \}
     call extend(t, s:constructor, 'error')
     let t.throw=s:_f.throw
     let t.l=t.stack[-1]
     "▲2
     call s:F.cleanup(t.tree)
-    " FIXME
-    echo s:F.getlenrange(t.tree[1])
-    return t.tree
+    let t.o=remove(t.tree, 0)
+    let t.tree=t.tree[0]
+    if t.type is 'check' || t.type is 'filter'
+        call   t.add('let @$@messages=[]',
+                    \'let @$@pmessages=[]',
+                    \'try').deeper()
+        if !t.o.only
+            call t.add('let @$@largs=len(@@@)')
+            if t.type is 'check' || t.type is 'filter'
+                call t.compileadesc(t.tree)
+            else
+            endif
+        endif
+        call t.up()
+        call t.add('catch /^CHECKFAILED$/')
+                \.deeper('for @$@targs in @$@messages')
+                  \.deeper('call call(@%@.F.warn, @$@targs, {})')
+                \.up().close()
+                \.add('for @$@targs in @$@pmessages')
+                  \.deeper('call call(@%@.p._f.warn, @$@targs, {})')
+                \.up().close()
+                \.add('return 0')
+              \.up().close()
+    else
+        call t.add('let @@@=[]')
+    endif
+    if t.type is 'check'
+        call t.add('return 1')
+    elseif t.type is 'filter'
+        call t.add('return @@@')
+    else
+        call t.add('return @@@')
+    endif
+    return s:F.tolstofstr(t.ctree)
 endfunction
-"▶1 FIXME this is for debugging
-if !exists('g:curtest')
-    let g:scan=s:_r.fwc_parser
-    let g:compile=s:F
-endif
+"▶1 makefunc       :: {f}, String, type → Fref + s:lastid, s:vars
+let s:lastid=0
+let s:vars={}
+function s:F.makefunc(plugdict, fdict, string, type)
+    let d={}
+    let id=printf('%s%X', a:type, s:lastid)
+    let s:lastid+=1
+    let s:vars[id]={
+                \'F': {'warn': s:_f.warn},
+                \'p': a:plugdict.g,
+            \}
+    call add(a:fdict.ids, id)
+    execute "function d.f(args)\n    ".
+                \substitute(substitute(substitute(
+                \join(s:F.compilestr(s:vars[id], a:string, a:type), "\n    "),
+                \'@@@', ((a:type is 'completer')?
+                \           ('variants'):
+                \           ('a:args')), 'g'),
+                \'@%@', 's:vars.'.id,    'g'),
+                \'@$@', '',              'g')."\n".
+            \"endfunction"
+    return d.f
+endfunction
+"▶1 delids         :: {f}
+function s:F.delids(plugdict, fdict)
+    call map(a:fdict.ids, 'remove(s:vars, v:val)')
+endfunction
+"▶1 Register feature
+call s:_f.newfeature('fwc_compile', {'cons': s:F.makefunc,
+            \                      'unload': s:F.delids,
+            \                        'init': {'ids': []}})
 "▶1
-call frawor#Lockvar(s:, '')
+call frawor#Lockvar(s:, 'lastid,vars')
 " vim: fmr=▶,▲ sw=4 ts=4 sts=4 et tw=80

plugin/frawor/fwc/parser.vim

 "▶1 readstr    :: () + self → String + self(s)
 " Gets next double-quoted string. Backslash just escapes next character, no 
 " other translations are done
-" {str} :: '"' ( ( ! '\' | '"' ) | ( '\\' | '\"' ) )* '"'
+" {dstr} :: '"' ( ( ! '\' | '"' ) | ( '\\' | '\"' ) )* '"'
+"  {str} :: {dstr} | {sstr}
 function s:parser.readstr()
     if !empty(self.ungot)
         call self.throw('int', 'strungetc')
     call self.removestr(len(c))
     return substitute(c[:-2], '\\\(.\)', '\1', 'g')
 endfunction
+"▶1 readsstr   :: () + self → String + self(s)
+" Gets next single-quoted string.
+" {sstr} :: "'" ( "''" | ( ! "'" ) )* "'"
+"  {str} :: {dstr} | {sstr}
+function s:parser.readsstr()
+    if !empty(self.ungot)
+        call self.throw('int', 'strungetc')
+    endif
+    let c=matchstr(self.s, "\\v(''|[^'])*'")
+    if empty(c)
+        call self.throw('unmatchp', "'")
+    endif
+    call self.removestr(len(c))
+    return substitute(c[:-2], "''", "'", 'g')
+endfunction
 "▶1 readreg    :: endstr + self → String + self(s)
 " Gets the next regular expression. {endstr} determines border character
 " {reg} :: ( "\" . | ! "\" {endstr} ) {endstr}
                 let close.=remove(parens, -1)
             endwhile
             let c.=close
-            if empty(parens)
-                break
-            else
+            if !empty(parens)
                 call remove(parens, -1)
             endif
         elseif stopsym is '"'
         endif
         call self.removestr(len(chunk))
         let c.=chunk
+        if empty(parens)
+            break
+        endif
     endwhile
     let c.=join(parens, '')
     if empty(c)
 "        {funcname} :: {wordchar}+
 " Output: context(intfunc, {funcname}[, contexts])
 function s:parser.intfunc()
-    let type=self.stack[-1][0]
+    let type=self.l[0]
     let c=self.readc()
     let func=s:F.getmatch(s:args[type], c)
     if func is 0 "▶2
     let c=self.readc()
     if c is '"'
         call self.add(self.readstr())
+    elseif c is "'"
+        call self.add(self.readsstr())
     elseif c is '$'
         call self.getvar()
     elseif c=~#'^\w'
         let c=self.readc()
         if c is '"'
             call self.add(self.readstr())
+        elseif c is "'"
+            call self.add(self.readsstr())
         elseif c=~#'^\d'
             call self.add(+c)
         elseif c=~#'^\w'
                 break
             elseif c is '"'
                 call self.addcon('eq', self.readstr()).scan().conclose()
+            elseif c is "'"
+                call self.addcon('eq', self.readsstr()).scan().conclose()
             elseif c is '/'
                 call self.addcon('regex', self.readreg('/')).scan().conclose()
             else
     return self.addcon('expr', self.readexpr()).conclose()
 endfunction
 "▶1 getchvar   :: &self
-" Input: "<"* {subscr}?
-" Output: context(this, Number, {subscr}?)
+" Input: ( ( "^"* ) | ( ">" | "<" )* ) {subscr}?
+" Output: context(argument, Number, {subscr}?)
+"       | context(cur, UInt, {subscr}?)
 function s:parser.getchvar()
-    call self.addcon('this', 0)
     if self.len
         let c=self.readc()
-        if c is '<'
-            let self.l[-1]+=1
+        if c is '<' || c is '>'
+            call self.addcon('argument', 0)
+            call self.ungetc(c)
             while self.len
                 let c=self.readc()
-                if c is '<'
+                if c is '>'
+                    let self.l[-1]+=1
+                elseif c is '<'
+                    let self.l[-1]-=1
+                else
+                    break
+                endif
+            endwhile
+        else
+            call self.ungetc(c).addcon('cur', 0)
+            while self.len
+                let c=self.readc()
+                if c is '^'
                     let self.l[-1]+=1
                 else
                     break
         endif
         if c is '.'
             call self.getsubscr()
-        else
+        elseif self.len
             call self.ungetc(c)
         endif
+    else
+        call self.addcon('cur', 0)
     endif
     return self.conclose()
 endfunction
         elseif c is '*'
             call self.getfunc()
         elseif c is '"'
-            call self.add(self.nextstr())
+            call self.add(self.readstr())
+        elseif c is "'"
+            call self.add(self.readsstr())
         elseif c=~#'^\w'
             call self.add(c)
         endif
         return self.getexpr()
     elseif c is '['
         call self.getlist()
+    elseif c is '*'
+        call self.getfunc()
     elseif c is '$'
         call self.addcon('evaluate').getvar().conclose()
     elseif c is '"'
         call self.addcon('string', self.readstr()).conclose()
+    elseif c is "'"
+        call self.addcon('string', self.readsstr()).conclose()
     else
         call self.throw('invvar', c)
     endif
     return self
 endfunction
 "▶1 getfunc    :: &self
-" Input: {var} ( "(" ( "$" {var} | "*" {func} | {wordchar}+ ) ")"? )?
-" Output: context(func, {var}, ({var}|{func}|context(string, String))*)
+" Input: {var} ( "(" ( "." | {var} | "," )* ")"? )?
+" Output: context(func, {var}, ({var}|context(this))*)
 function s:parser.getfunc()
     call self.addcon('func').getvar()
     if self.len
                 let c=self.readc()
                 if c is ')'
                     break
-                elseif c is '$'
-                    call self.getvar()
-                elseif c is '*'
-                    call self.getfunc()
-                elseif c=~#'^\w'
-                    call self.addcon('string', c).conclose()
-                else
-                    call self.throw('ukfarg', c)
+                elseif c is '.'
+                    call self.addcon('this').conclose()
+                elseif c isnot ','
+                    call self.ungetc(c).getvar()
                 endif
             endwhile
         else
     return self.conclose()
 endfunction
 "▶1 scanopt    :: &self
-" Input: ( ( ":" {var} )? {arg} )* "]"?
-" Output: context(optional, ({arg}|context(default, {var}, {arg}))*)
+" Input: {arg}* "]"?
+" Output: context(optional, {arg}*)
 function s:parser.scanopt()
     call self.addcon('optional')
     let prevlen=-1
         let c=self.readc()
         if c is ']'
             break
-        elseif c is ':'
-            call self.addcon('default').getvar().scan().conclose()
         else
             call self.ungetc(c).scan()
         endif
             if !exists('prefopts')
                 let prefopts=copy(s:defprefopts)
             endif
-            let argnum=1
+            let prefopts.argnum=1
             if c=~#'^\w'
                 let pref=c
-            elseif c is '+'
-                let argnum=+self.readc()
             elseif c is '"'
                 let c=self.readstr()
                 let pref=c
+            elseif c is "'"
+                let c=self.readsstr()
+                let pref=c
+            elseif c is '+'
+                let prefopts.argnum=+self.readc()
             elseif c is '?'
                 let prefopts.opt=1
             elseif c is '!'
                 let c=self.readc()
             endif
             if c is '-'
-                let argnum=0
+                let prefopts.argnum=0
             else
                 call self.ungetc(c)
             endif
-            let prefopts.argnum=argnum
+            let argnum=prefopts.argnum
             while argnum
                 call self.scan()
                 let argnum-=1
                 break
             elseif c is '"'
                 call self.addcon('action', self.readstr())
+            elseif c is "'"
+                call self.addcon('action', self.readsstr())
             elseif c is '-'
                 call self.addcon('action', 0)
             elseif c=~#'^\w'
 "                          only in
 "                     context(optional)
 function s:parser.scan()
+    "▶2 optional, prefixes, actions, next
     let c=self.readc()
-    let type=self.stack[-1][0]
+    let type=self.l[0]
     if type is 'top' || type is 'optional'
                 \|| (type is 'action' && len(self.l)>1)
         if c is '['
             return self.addcon('next').scan().conclose()
         endif
     endif
+    "▲2
     call self.addcon('arg')
+    "▶2 Default value
     if type is 'optional' && c is ':'
         call self.addcon('defval').getvar().conclose()
         let c=self.readc()
     endif
+    "▶2 Define variables used to determine how to handle second word
     let accepttext=(type is 'next' || self.o.only)
     let hasparen=0
     let hastext=0
     else
         call self.ungetc(c)
     endif
+    "▲2
     while self.len
         let c=self.readc()
         if c is '|'
         elseif (!hastext || accepttext) && c=~#'^\w'
             let hastext=1
             call self.ungetc(c).intfunc()
+            if !accepttext
+                break
+            endif
         elseif hasparen && c is ')'
             break
         else
     endif
     return self
 endfunction
-"▶1 string     :: String → SynTree
-function s:parser.string(string)
+"▶1 parsestr   :: String → SynTree
+function s:F.parsestr(string, type)
     "▶2 Setup self
     let s   =   {    's': a:string,
                 \ 'tree': ['top'],
+                \ 'type': a:type,
                 \'stack': [],
                 \'ungot': [],
                 \    'l': 0,
-                \ 'type': 'check',
                 \  'len': len(a:string),
                 \}
     call extend(s, s:parser, 'error')
     return s.tree
 endfunction
 "▶1 Post resource
-call s:_f.postresource('fwc_parser', s:parser)
+call s:_f.postresource('fwc_parser', s:F.parsestr)
 "▶1
 call frawor#Lockvar(s:, '')
 " vim: fmr=▶,▲ sw=4 ts=4 sts=4 et tw=80
+:let &rtp.=",".escape($TESTDIR, ',\').'/rtp'
+:let g:testfile="plugin/".g:curtest.".vim"
+:source test.vim
+>>> messages
+::: Section <Invalid length/Required>
+plugin/frawor/fwc/compiler:invlen
+plugin/frawor/fwc/compiler:invlen
+plugin/frawor/fwc/compiler:invlen
+plugin/frawor/fwc/compiler:invlen
+::: Section <Invalid length/Optinal>
+plugin/frawor/fwc/compiler:toolong
+plugin/frawor/fwc/compiler:toolong
+plugin/frawor/fwc/compiler:toolong
+plugin/frawor/fwc/compiler:toolong
+plugin/frawor/fwc/compiler:invlen
+::: Section <Invalid length/Required+optional>
+plugin/frawor/fwc/compiler:tooshort
+plugin/frawor/fwc/compiler:toolong
+plugin/frawor/fwc/compiler:toolong
+plugin/frawor/fwc/compiler:invlen
+::: Section <Invalid length/Required+next>
+plugin/frawor/fwc/compiler:tooshort
+plugin/frawor/fwc/compiler:tooshort
+::: Section <Invalid length/Required+optional+next>
+plugin/frawor/fwc/compiler:tooshort
+plugin/frawor/fwc/compiler:tooshort
+plugin/frawor/fwc/compiler:tooshort
+::: Section <Invalid length/Prefixes>
+plugin/frawor/fwc/compiler:tooshort
+plugin/frawor/fwc/compiler:tooshort
+plugin/frawor/fwc/compiler:tooshort
+plugin/frawor/fwc/compiler:tooshort
+plugin/frawor/fwc/compiler:tooshort
+plugin/frawor/fwc/compiler:tooshort
+plugin/frawor/fwc/compiler:invlen
+::: Section <Invalid length/Actions+required>
+plugin/frawor/fwc/compiler:invlen
+plugin/frawor/fwc/compiler:invlen
+plugin/frawor/fwc/compiler:invlen
+plugin/frawor/fwc/compiler:invlen
+plugin/frawor/fwc/compiler:tooshort
+plugin/frawor/fwc/compiler:toolong
+plugin/frawor/fwc/compiler:invlen
+plugin/frawor/fwc/compiler:invlen
+::: Section <Invalid length/Actions+optional>
+plugin/frawor/fwc/compiler:tooshort
+plugin/frawor/fwc/compiler:toolong
+plugin/frawor/fwc/compiler:invlen
+::: Section <Invalid length/Actions+actions+required>
+plugin/frawor/fwc/compiler:invlen
+plugin/frawor/fwc/compiler:invlen
+plugin/frawor/fwc/compiler:invlen
+::: Section <Pipes/Function pipes>
+::: Section <Pipes/Expression pipes>
+::: Section <Pipes/Pipes composition>
+::: Section <Checks/Function checks>
+plugin/frawor/fwc/compiler:funcfail
+plugin/frawor/fwc/compiler:funcfail
+::: Section <Checks/Expression checks>
+plugin/frawor/fwc/compiler:exprfail
+::: Section <Checks/Check composition>
+plugin/frawor/fwc/compiler:exprfail
+plugin/frawor/fwc/compiler:exprfail
+plugin/frawor/fwc/compiler:exprfail
+<<< messages

test/fwctests.dat

+#▶1 Invalid length
+#▶2 Required
+['_',                    'check'], range(0), 0
+['_',                    'check'], range(2), 0
+['_ _',                  'check'], range(3), 0
+['()',                   'check'], range(1), 0
+#▶2 Optinal
+['[_]',                  'check'], range(2), 0
+['[_][_ _]',             'check'], range(3), 0
+['[_][_[_]]',            'check'], range(4), 0
+['[_ [_ _]]',            'check'], range(4), 0
+#['[_][_ _ _]',           'check'], range(2), 0
+['[]',                   'check'], range(1), 0
+#▶2 Required+optional
+['_[_]',                 'check'], range(0), 0
+['_[_[_]]',              'check'], range(4), 0
+['_[_[_][_ _]]',         'check'], range(5), 0
+['()[]',                 'check'], range(1), 0
+#▶2 Required+next
+['_+_',                  'check'], range(0), 0
+['_ _ _ +_',             'check'], range(2), 0
+#▶2 Required+optional+next
+['_[_]+_',               'check'], range(0), 0
+['_[_+_]',               'check'], range(0), 0
+['_[[_]+_]',             'check'], range(0), 0
+#▶2 Prefixes
+['{p1 _}',               'check'], range(0), 0
+['{p1 _}',               'check'], range(1), 0
+['{+2 p1 _ _}',          'check'], range(2), 0
+['{*p1 _}',              'check'], range(0), 0
+['{p2 _ *p1 _}',         'check'], range(3), 0
+['{?p1 _ !p2 -}',        'check'], range(0), 0
+['{}',                   'check'], range(1), 0
+#▶2 Actions+required
+['<a1 ->',               'check'], range(0), 0
+['<a1 ->',               'check'], range(2), 0
+['<- _>',                'check'], range(0), 0
+['<- _>',                'check'], range(2), 0
+['<a1 - a2 _ a3 (_ _)>', 'check'], range(0), 0
+['<a1 - a2 _ a3 (_ _)>', 'check'], range(4), 0
+['<- ->',                'check'], range(1), 0
+['<>',                   'check'], range(1), 0
+#▶2 Actions+optional
+['<a1 [_]>',             'check'], range(0), 0
+['<a1 [_]>',             'check'], range(3), 0
+['<- []>',               'check'], range(1), 0
+#▶2 Actions+actions+required
+['<a1 <a2 _>>',          'check'], range(2), 0
+['<a1 <a2 _>>',          'check'], range(4), 0
+['<- <- ->>',            'check'], range(1), 0
+#▶1 Pipes
+#▶2 Function pipes
+['|*$"string"',                'filter'], ['abc'], ["'abc'"]
+["|*$'string'",                'filter'], ['abc'], ["'abc'"]
+['|*=function("string")',      'filter'], ['abc'], ["'abc'"]
+['|*revstring',                'filter'], ['abc'], ['cba']
+['|*$"string"(.)',             'filter'], ['abc'], ["'abc'"]
+['|*=function("string")(.)',   'filter'], ['abc'], ["'abc'"]
+['|*revstring(.)',             'filter'], ['abc'], ['cba']
+['|*$"split"(. ''\v.@='')',    'filter'], ['abc'], [['a', 'b', 'c']]
+['|*$"index"([abc def ghi])',  'filter'], ['abc'], [0]
+['|*@',                        'filter'], [function('string')], ['function(''string'')']
+['(|*@>) _',                   'filter'], ['abc', function('string')], ["'abc'", function('string')]
+['_ |*@<',                     'filter'], [function('string'), 'abc'], [function('string'), "'abc'"]
+['|*=((@.@[0] is "a")?(function("string")):(function("eval")))(.)', 'filter'], ['a0'], ["'a0'"]
+['|*=((@.@[0] is "a")?(function("string")):(function("eval")))(.)', 'filter'], ['0'],  [0]
+#▶2 Expression pipes
+['|=string(@.@)',              'filter'], ['abc'], ["'abc'"]
+#▶2 Built-in pipes
+#▶2 Pipes composition
+['|*$"string"|*$"string"',     'filter'], ['abc'], ["'''abc'''"]
+['|=string(@.@)|=string(@.@)', 'filter'], ['abc'], ["'''abc'''"]
+#▶1 Checks
+#▶2 Function checks
+['?*$"empty"', 'check'], ['abc'], 0
+['?*$"empty"', 'check'], [''],    1
+['?*ordered',  'check'], ['abc'], 1
+['?*ordered',  'check'], ['acb'], 0
+#▶2 Expression checks
+['?=empty(@.@)', 'check'], ['abc'], 0
+['?=empty(@.@)', 'check'], [''],    1
+#▶2 Built-in checks
+#▶2 Check composition
+['?=!empty(@.@)?=(type(@.@)==type(""))?=(@.@[0]=="a")', 'check'], ['abc'], 1
+['?=!empty(@.@)?=(type(@.@)==type(""))?=(@.@[0]=="a")', 'check'], [''],    0
+['?=!empty(@.@)?=(type(@.@)==type(""))?=(@.@[0]=="a")', 'check'], [[1]],   0
+['?=!empty(@.@)?=(type(@.@)==type(""))?=(@.@[0]=="a")', 'check'], ['bac'], 0
+#▶2 Checks and pipes composition
+#▶1 Messages
+# vim: cms=#%s fmr=▶,▲

test/rtp/plugin/fwccheck.vim

+execute frawor#Setup('0.0', {'@/fwc/compiler': '0.0'}, 1)
+let tests=map(filter(readfile('fwctests.dat'), 'v:val[0] isnot "#"'),
+            \ 'eval("[".v:val."]")')
+let i=1
+let j=1
+let sections=[]
+let prevsections=[]
+function s:.revstring(str)
+    return join(reverse(split(a:str, '\v.@=')), '')
+endfunction
+function s:.ordered(str)
+    return join(sort(split(a:str, '\v.@=')), '') is a:str
+endfunction
+for line in readfile('fwctests.dat')
+    if empty(line)
+        continue
+    elseif line[0] is '#'
+        if char2nr(line[1:])==0x25b6
+            let level=+line[4:]
+            if level<=len(sections)
+                call remove(sections, level-1, -1)
+            endif
+            call add(sections, line[4+len(level)+1:])
+            let i=1
+        endif
+        continue
+    elseif !empty($DEBUGTEST) && j!=$DEBUGTEST
+        let i+=1
+        let j+=1
+        continue
+    endif
+    if prevsections!=#sections
+        let prevsections=copy(sections)
+        echom '::: Section <'.join(sections, '/').'>'
+    endif
+    let [compargs, args, result]=eval('['.line.']')
+    let savedargs=deepcopy(args)
+    if !empty($DEBUGTEST)
+        debug let ChFunc=call(s:_f.fwc_compile, compargs, {})
+        debug let realres=call(ChFunc, [args], {})
+    else
+        let ChFunc=call(s:_f.fwc_compile, compargs, {})
+        let realres=call(ChFunc, [args], {})
+    endif
+    if type(realres)!=type(result) || realres!=result
+        echom '[[[ Test #'.i.'('.j.') '.
+                    \'in section <'.join(sections, '/').'> failed:'
+        echom 'expected '.string(result).','
+        echom 'but got  '.string(realres)
+        echom 'Checked arguments: '.string(savedargs)
+        echom 'FWC string: '.compargs[0]
+        echom 'Check type: '.compargs[1]
+        function g:.ChFunc
+        echom ']]]'
+        break
+    endif
+    let i+=1
+    let j+=1
+    unlet realres result
+endfor
+" vim: fmr=▶,▲
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.