Wiki

root/cobra/trunk/Source/CobraParser.cobra

Revision 2638, 118.2 KB (checked in by Charles.Esterbrook, 4 weeks ago)

Add support for namespace doc strings.
ticket:277
credit:hopscc

  • Property svn:eol-style set to native
Line 
1"""
2The Cobra Parser
3    Recursive descent parser for the Cobra Language.
4    Uses CobraTokenizer to obtain a stream of Cobra lexical tokens,
5    then enumerates through these tokens according to grammar rules,
6    generating nodes (or rather subclasses thereof) for recognized constructs.
7    The root node is a Cobra Module.
8
9Rules:
10    * Do not invoke Box.memberForName. Use Box.declForName instead. Inheritance relationships are not established during parsing.
11"""
12
13
14class ErrorMessages
15
16    shared
17   
18        get unexpectedEndOfFile as String
19            return 'Unexpected end of file'
20           
21        get expectingStatementInsteadOfIndentation as String
22            return 'Expecting a statement instead of extra indentation. One indent level is 4 spaces or 1 tab with display width of 4.'
23
24        get localVariablesMustStartLowercase as String
25            return 'Local variables must start with a lowercase letter. This avoids collisions with other identifiers such as classes and enums.'
26
27        get syntaxErrorAfterDot as String
28            return 'Syntax error after "."'
29
30
31extend String
32    """
33    Among these choices:
34        name in ['foo', 'bar', 'baz']
35        name in {'foo', 'bar', 'baz'}
36        name.isOneOf('foo.bar.baz.')
37   
38    The last is fastest when using -turbo which is how Cobra
39    is installed and also built for snapshots.
40    """
41
42    def isOneOf(tokens as String) as bool
43        require tokens.endsWith('.')
44        return tokens.contains(this+'.')
45
46
47class CobraParser inherits Parser
48    """
49    Notes:
50
51        * The tokenizer's verbosity is set to 4 less than the parser's. In other words, the
52          tokenizer will not print messages unless the parser's verbosity is 5 or greater.
53
54    """
55
56    sig ParseCommandLineArgsSig(args as IList<of String>, isAvailable as out bool) as String?
57        """
58        The delegate is used to implement the 'args' compiler directive.
59        It should set isAvailable.
60        If isAvailable, it should return nil on success, or an error message.
61        Otherwise, it should return nil.
62        """
63
64    test
65        c = Compiler()  # to-do: the parser should not require a Compiler for unit tests
66        p = CobraParser()
67        p.globalNS = NameSpace(Token.empty, '(global)')
68        p.typeProvider = BasicTypeProvider()
69        p.backEnd = BasicBackEnd(c)
70        module = p.parseSource('test1', 'class SomeClass\n\tpass\n')
71        decls = (module to dynamic).topNameSpace.declsInOrder
72        decl = decls[decls.count-1]
73        if decl inherits Class
74            assert decl.name == 'SomeClass', decl.name
75        else
76            assert false, decl
77
78        p = CobraParser()
79        p.globalNS = NameSpace(Token.empty, '(global)')
80        p.typeProvider = BasicTypeProvider()
81        p.backEnd = BasicBackEnd(c)
82        p.parseSource('test2', 'class Test\n\tdef main is shared\n\t\treturn\n')
83       
84        c = TestCompiler() # to default backend to something on init
85        p = CobraParser(typeProvider=c, warningRecorder=c, errorRecorder=c, globalNS=c.globalNS, backEnd=c.backEnd)
86        p.parseSource('test3', '')
87       
88        c = TestCompiler()
89        p = CobraParser(typeProvider=c, warningRecorder=c, errorRecorder=c, globalNS=c.globalNS, backEnd=c.backEnd)
90        p.parseSource('test4', '    ')
91
92    shared
93       
94        var _tokenizer as CobraTokenizer?
95            """
96            Caching the tokenizer benefits the performance of testify and any other process that
97            compiles (or even just parses) more than once.
98            """
99
100    var _lowercaseLetters = 'abcdefghijklmnopqrstuvwxyz'
101    var _uppercaseLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
102
103    var _isNamesStack as Stack<of String>?
104        """ Stack for recording modifier for items in modifier sections (e.g.shared). """
105
106    var _boxStack as Stack<of Box>?
107        """ Stack to record the box the current level of box member declarations is for. """
108
109    var _globalNS as NameSpace?
110    var _nameSpaceStack as Stack<of NameSpace>
111
112    var _codeParts as Stack<of AbstractMethod>
113    var _curCodePart as AbstractMethod?
114
115    var _spaceAgnosticIndentLevel as int
116    var _spaceAgnosticExprLevel as int
117    var _isContractOnSameLine as bool
118
119    var _isTraceOn as bool
120
121    var _leftStack as Stack<of Expr>?
122    var _opStackStack as Stack<of Stack<of String>>?
123        """
124        Used by expression parts so the last operator can be examined.
125        A stack of stacks is needed for CallExpr's args.
126        """
127
128    var _expectAnonymousMethodExprStack as Stack<of AnonymousMethodExpr>
129
130    var _typeProvider as ITypeProvider?
131    var _backEnd as BackEnd?
132    # compiler is available from _backEnd.compiler
133
134    var _parseCommandLineArgs as ParseCommandLineArgsSig?
135   
136    var _didStartLoop as bool
137    var _curLoopBlockDepth as int
138
139    cue init
140        base.init
141        _boxStack = Stack<of Box>()
142        _nameSpaceStack = Stack<of NameSpace>()
143        _codeParts = Stack<of AbstractMethod>()
144        _expectAnonymousMethodExprStack = Stack<of AnonymousMethodExpr>()
145        _references = List<of String>()
146        _didStartLoop = false
147        _curLoopBlockDepth = 0
148
149    pro typeProvider from var
150    pro backEnd from var
151
152    pro parseCommandLineArgs from var
153        """
154        The delegate is used to implement the 'args' compiler directive.
155        If none is provided and the 'args' directive is encountered, an error is generated.
156        See also: ParseCommandLineArgsSig
157        """
158
159    get references from var as List<of String>
160        """
161        A list of references, possibly empty, from the @ref compiler directive.
162        """
163
164    pro globalNS as NameSpace
165        get
166            assert _globalNS
167            return _globalNS to !
168        set
169            require _globalNS is nil
170            _globalNS = value
171
172    get curBox as Box
173        require _boxStack.count > 0
174        # yes there is only one box at the moment, but when nested classes and structs are supported there could by many.
175        return _boxStack.peek
176
177    get curNameSpace as NameSpace
178        return _nameSpaceStack.peek
179
180    get curContainer as IContainer
181        if _boxStack.count
182            return _boxStack.peek
183        else
184            return _nameSpaceStack.peek
185
186    def parseFileNamed(fileName as String) as CobraModule
187        require .typeProvider and .backEnd
188        if .verbosity >= 2, print 'Parsing [fileName]'
189        _fileName = fileName
190        source = File.readAllText(_fileName to !)
191        return .parseSource(fileName, source)
192
193    def parseSource(source as String) as CobraModule
194        require .typeProvider and .backEnd
195        return .parseSource('(no file name)', source)
196
197    def parseSource(fileName as String, source as String) as CobraModule
198        """
199        Parses module source code and returns resulting Cobra module.
200        The fileName is recorded, but the file is not physically accessed.
201        After parsing, .references will hold a list of any @ref directive references that were parsed.
202        """
203        if not _preParseSource(fileName, source)
204            # Throw an exception to stop subsequent compilation phases.
205            # Note that this doesn't get recorded as an error message.
206            # That only happens through .throwError and .recordError
207            throw ParserException(fileName, 'Lexer errors.')
208        try
209            if source.trim.length == 0, _warning('File is empty.')
210            return _parseTokens()
211        catch pe as ParserException
212            if _errorRecorder, _errorRecorder.recordError(pe)
213            _tokenizer = nil
214            throw
215        if _tokenizer and _tokenizer.errors.count <> 0, _tokenizer = nil
216
217    def _preParseSource(fileName as String, source as String) as bool
218        """
219        Sets up for parsing - inits stack variables, generates token stream and handles explicit line continuation,
220        but does not invoke `parseTokens`.
221        Used by `parseSource` and various test sections.
222        Returns true if successful, false if there were one or more errors.
223        Upon success, you can use token methods like .grab.
224        Does not .throwError but may .recordError.
225        """
226        _fileName = fileName
227        tokVerbosity = _verbosity - 4  # in order words, tokenizer does not spew unless our verbosity is 5 or greater
228        if tokVerbosity < 0, tokVerbosity = 0
229
230        _isNamesStack = Stack<of String>()  # for `shared` for example
231        _leftStack = Stack<of Expr>()
232        _opStackStack = Stack<of Stack<of String>>()
233        .newOpStack
234
235        _tokens = nil
236        if _tokenizer is nil, _tokenizer = CobraTokenizer(tokVerbosity)
237        else, _tokenizer.restart
238        _tokenizer.typeProvider = .typeProvider
239        tokenizer = _tokenizer
240        try
241            tokens = tokenizer.startSource(_fileName, source).allTokens
242            _tokens = List<of IToken>(tokens.count)
243            for i = 0 .. tokens.count
244                if tokens[i].text == '_' and tokens[i].which == 'ID'
245                    if i < tokens.count-1 and tokens[i+1].which == 'EOL'
246                        i += 1
247                    else
248                        .recordError(tokens[i], 'Unexpected line continuation character.')
249                else
250                    _tokens.add(tokens[i])
251        catch te as TokenizerError
252            .recordError(te.token, te.message)
253            return false
254
255        compiler = .backEnd.compiler
256        compiler.noWarningLines.addRange(tokenizer.noWarningLines)
257        compiler.linesCompiled += tokenizer.linesCompiled
258        compiler.tokensCompiled += tokens.count
259
260        if tokenizer.errors.count
261            for error in tokenizer.errors
262                .recordError(error.token, error.message)
263            return false
264
265        _nextTokenIndex = 0
266        return true
267
268    def optionalStringLiteral as IToken?
269        """
270        Gets a token if it matches one of the string literals,
271        not including strings with substitution expressions such as STRING_START_SINGLE.
272        """
273        return .optional('STRING_SINGLE', 'STRING_DOUBLE', 'STRING_RAW_SINGLE', 'STRING_RAW_DOUBLE', 'STRING_NOSUB_SINGLE', 'STRING_NOSUB_DOUBLE')
274
275
276    ## Common parsing bits (docString, indent, dedent, ...)
277
278    def docString as String?
279        if .optional('DOC_STRING_START')
280            quoteToken as IToken?
281            textParts = List<of String>()
282            keepGoing = true
283            while keepGoing
284                tok = .grab
285                if tok is nil
286                    msg = 'Unexpected end of file inside doc string.'
287                    if quoteToken
288                        msg += ' Maybe line [quoteToken.lineNum] was intended to end the doc string. Use exactly three quotes.'
289                    else
290                        msg += ' Use exactly three quotes to end the doc string.'
291                    .throwError(msg)
292                branch tok.which
293                    on 'DOC_STRING_STOP'
294                        # TODO: check that indentation level is correct
295                        keepGoing = false
296                    on 'DOC_STRING_BODY_TEXT'
297                        if quoteToken is nil and tok.text.startsWith('"')
298                            quoteToken = tok
299                        textParts.add(tok.text)
300                    else
301                        .throwError('Expecting more doc string contents or the end of the doc string instead of [tok].')
302            text = textParts.join('')
303            return text
304        else if .optional('DOC_STRING_LINE')
305            return .last.value to String
306        else
307            return ''
308
309    def endOfLine
310        .oneOrMore('EOL')
311
312    def idOrKeyword as IToken
313        token = .grab
314        if token is nil
315            .throwError('Expecting an identifier or keyword.')
316        else if token.isKeyword or token.which=='ID'
317            return token to !
318        else
319            .throwError('Expecting an identifier or keyword, but got [token] instead.')
320        throw FallThroughException(token# CC: axe when throwError() can be marked as always throwing (well if C# can figure that out! (solution: Cobra can generate a "dummy" throw statement))
321
322    def indent as IToken
323        """
324        Consumes an option COLON (which generates a warning), 1 or more EOLs and an INDENT.
325        Returns the INDENT.
326        """
327        if .optional('COLON')
328            _warning('Colons are not used to start indented blocks. You can remove the colon.')
329        .endOfLine
330        return .expect('INDENT')
331
332    def dedent
333        while .optional('EOL'), pass
334        .expect('DEDENT')
335
336    def optionalIndent as IToken?
337        """
338        Consumes and warns about COLON not being necessary.
339        Expects .endOfLine and then returns optional 'INDENT'.
340        Used at the end of a declaration that can optionally have more specification indented underneath.
341        """
342        if .optional('COLON')
343            _warning('Colons are not used to start indented blocks. You can remove the colon.')
344        .endOfLine
345        return .optional('INDENT')
346
347
348    ##
349    ## Parsing
350    ##
351
352    var _module as CobraModule?
353
354    def _parseTokens as CobraModule
355        """ Parses and then returns an instance of CobraModule. """
356        .zeroOrMore('EOL')
357        docString = .docString
358        _module = CobraModule(_fileName, _verbosity, docString, _globalNS# TODO: does module really need verbosity?
359        _nameSpaceStack.push(_module.topNameSpace)
360        try
361            topNS = _module.topNameSpace
362            if not _fileName.endsWith('SystemInterfaces.cobra')
363                .backEnd.setDefaultUseDirectives(topNS)
364            while .peek, .parseTopLevel
365        finally
366            _nameSpaceStack.pop
367        return _module to !
368
369    def parseTopLevel as ISyntaxNode?
370        """ Returns the node that was parsed, but will return nil for EOL, EOF and compiler directives. """
371        what as ISyntaxNode?
372        tok = .peek
373        if tok is nil, return nil
374        isGood = true
375        branch tok.which
376            on 'AT_ID'
377                .compilerDirective
378            on 'PERCENTPERCENT'  # deprecated
379                .compilerDirective
380            on 'USE'
381                _module.topNameSpace.addUseDirective(.useDirective)
382#           on 'IMPORT'
383#               what = .importDirective
384            on 'CLASS'
385                what = .classDecl
386            on 'MIXIN'
387                what = .mixinDecl
388            on 'INTERFACE'
389                what = .interfaceDecl
390            on 'SIG'
391                what = .declareMethodSig
392            on 'STRUCT'
393                what = .structDecl
394            on 'ENUM'
395                what = .enumDecl
396            on 'EXTEND'
397                what = .extendDecl
398            on 'EOL'
399                .grab
400            on 'NAMESPACE'
401                what = .nameSpaceDecl
402            on 'ID'
403                if tok.text == 'assembly'  # pseudo keyword
404                    what = .assemblyDecl
405                else
406                    isGood = false
407            else
408                isGood = false
409        if not isGood
410            sugg = if(tok.text.length, Compiler.suggestionFor(tok.text), nil)
411            sugg = if(sugg, ' Try "[sugg]".', '')
412            .throwError('Expecting use, assembly, namespace, class, interface or enum, but got [tok].[sugg]')
413        if what implements INameSpaceMember and not what inherits NameSpace
414            what = _nameSpaceAddDecl(_module.topNameSpace, what to INameSpaceMember)
415            if what inherits Box
416                assert what.parentNameSpace
417        return what
418
419    def compilerDirective
420        if .peek.which == 'AT_ID'
421            token = .grab
422        else
423            pp = .expect('PERCENTPERCENT')
424            token = .grab
425            _warning(pp, 'Use "@[token.text]" instead of "%% [token.text]" for compiler directives.')
426        branch token.value to String
427            on 'help'
428                if .peek is nil, .throwError(ErrorMessages.unexpectedEndOfFile)
429                node = .parseTopLevel
430                if node is nil, _warning(token, '@help is not implemented for this element.')
431                else, node.isHelpRequested = true
432            on 'throw'
433                # throw an internal error. used for testing that the compiler will catch and report these as internal errors
434                .expect('EOL')
435                # TODO: throw AssertException(SourceSite sourceSite, object[] expressions, object thiss, object info)
436                assert false, IdentifierExpr(token, 'throw')
437            on 'error'
438                # emit an error message
439                msg = '@error directive'
440                if .optionalStringLiteral
441                    msg = .last.value to String
442                .throwError(msg)
443            on 'warning'
444                # emit a warning
445                msg = '@warning directive'
446                if .optionalStringLiteral
447                    msg = .last.value to String
448                _warning(msg)
449            on 'number'   # TODO: fix this to work on a per-file basis
450                # number 'decimal' | 'float' | 'float32' | 'float64'
451                typeName = .grab
452                if not typeName.text.isOneOf('decimal.float.float32.float64.')
453                    .throwError('Compiler directive "number": unrecognized type "[typeName.text]". Must be one of "decimal", "float", "float32" or "float64".')
454                .expect('EOL')
455                .backEnd.compiler.numberTypeName = typeName.text
456            on 'platform'
457                plat = .grab   
458                if not plat.text.isOneOf('any.portable.clr.jvm.objc.')
459                    .throwError('Compiler directive "platform": unrecognized platform name "[plat.text]". Must be one of "portable" or "any", "clr", "jvm" or "objc".')
460                .expect('EOL')
461                if plat.text in[ 'portable', 'any']
462                    pass
463                else if not .backEnd.name.endsWith(plat.text)
464                    .throwError('This file is marked as platform specific to "[plat.text]"  but this compile is using "[.backEnd.name]" backend.')
465            on 'ref'
466                pathToken = .grab
467                if not pathToken.which.isOneOf('ID.STRING_SINGLE.STRING_DOUBLE.')
468                    .throwError('Expecting a string literal or identifier after "@ref".')
469                path = pathToken.text
470                if pathToken.which.isOneOf('STRING_SINGLE.STRING_DOUBLE.')
471                    path = path[1:-1]
472                if path.trim == ''
473                    .throwError('Expecting a non-empty string literal or identifier after "@ref".')
474                .references.add(path)
475            on 'args'   # remaining tokens are cmdline args
476                # args -ref:foo.dll -target:exe
477                args = List<of String>()
478                token = .grab
479                argStr = token.text
480                endLast = token.colNum + token.length
481                while token.which <> 'EOL'
482                    token = .grab
483                    if endLast < token.colNum or token.which == 'EOL'
484                        args.add(argStr)
485                        argStr = token.text
486                    else
487                        argStr += token.text
488                    endLast = token.colNum + token.length
489                   
490                if not args.count
491                    .throwError('args directive needs at least one arg following.')
492
493                isAvailable = false
494                if .parseCommandLineArgs
495                    # trace args
496                    parseCommandLineArgs = .parseCommandLineArgs
497                    # errorMsg = parseCommandLineArgs(args, out isAvailable) -- error: COBRA INTERNAL ERROR / NullReferenceException / Object reference not set to an instance of an object.
498                    if true
499                        errorMsg = '' to ?
500                        sharp'errorMsg = parseCommandLineArgs(args, out isAvailable)'
501                        CobraCore.noOp(parseCommandLineArgs)
502                    if isAvailable, if errorMsg, .throwError(errorMsg)
503                if not isAvailable
504                    .throwError('Cannot set command line arguments from directive because no arguments parser is available.')
505            else
506                .throwError('Unknown compiler directive "[token.text]".')
507
508    def useDirective as UseDirective
509        """
510        Example source:
511            use System.Net
512            use Foo
513        """
514        token = .expect('USE')
515        names = List<of String>()
516        while true
517            id = .expect('ID')
518            names.add(id.text)
519            dot = .optional('DOT')
520            if not dot, break
521        if .optional('FROM')
522            if .peek.which.startsWith('STRING_') and not .peek.which.startsWith('STRING_START')
523                fileName = .grab.value to String
524            else if .peek.which == 'ID'
525                fileNameParts = List<of String>()
526                while true
527                    id = .expect('ID')
528                    fileNameParts.add(id.text)
529                    dot = .optional('DOT')
530                    if not dot, break
531                fileName = fileNameParts.join('.')
532            else
533                .throwError('Expecting a file name (sans extension, with or without quotes) after "from".')
534            if fileName.endsWith('.dll') or fileName.endsWith('.exe')
535                .throwError('Do not include the extension in the file name.')
536        .endOfLine
537        return UseDirective(token, names, fileName, true)
538
539    var _syntaxForClassInheritanceMsg = 'The syntax for inheritance is to put "inherits BaseClass" on the following line, indented.'
540
541    def assemblyDecl as AssemblyDecl
542        """ Parse a module-level `assembly` declaration containing attributes. """
543        token = .expect('ID')
544        assert token.text == 'assembly'
545        .indent
546        if .peek and .peek.which == 'PASS'
547            .grab
548            .endOfLine
549            attribs = AttributeList()
550        else
551            attribs = .hasAttribs
552            # assembly can have multiple "has" lines in case you want to stack your attributes vertically
553            while .peek and .peek.which == 'HAS'
554                attribs.addRange(.hasAttribs)
555        .dedent
556        return AssemblyDecl(token, attribs)
557
558    def typeSpecDecls(token as IToken, genericParams as List<of GenericParam>) as TypeSpecs
559        """
560        Parses the specifications for a type declaration:
561            modifiers (is-names)
562            attributes
563            generic constraints
564            inheritance
565            implements interfaces
566            adds mixins
567       
568        These results are returned in a TypeSpecs instance, but note that the generic constraints
569        are attached directly to their generic parameters.
570       
571        Parsing rules:
572            * these clauses can appear in any order
573            * no repetition is allowed except for "where" which can appear once per generic parameter
574            * all "where" clauses must be consecutive
575            * these clauses can be on the same line or separate lines
576            * this all implies that a clause is terminated by a keyword or EOL
577        """
578        isNames = List<of String>()
579        attribs = AttributeList()
580        inheritsProxies = List<of ITypeProxy>()
581        implementsProxies = List<of ITypeProxy>()
582        addsProxies = List<of ITypeProxy>()
583
584        encountered = List<of String>()  # token types
585        didIndent = false
586        isDone = false
587        while true
588            peek = .peek
589            if peek is nil, .throwError('Unexpected end of source.')
590            last = .peek.which
591            branch last
592                on 'EOL'
593                    .grab
594                on 'INDENT'
595                    .grab
596                    if didIndent, .throwError('Unexpected indent.')
597                    didIndent = true
598                on 'IS'
599                    isNames = .isDeclNames
600                on 'HAS'
601                    attribs = .hasAttribs
602                on 'WHERE'
603                    .genericConstraints(token, genericParams)
604                on 'INHERITS'
605                    if .optional('INHERITS')
606                        _commaSepTypes(inheritsProxies)
607                on 'IMPLEMENTS'
608                    if .optional('IMPLEMENTS')
609                        _commaSepTypes(implementsProxies)
610                on 'ADDS'
611                    if .optional('ADDS')
612                        _commaSepTypes(addsProxies)
613                else
614                    isDone = true
615            if last.isOneOf('EOL.INDENT.'), continue
616            if isDone, break
617            if last in encountered and last <> 'WHERE'
618                .throwError('Encountered "[last]" twice.')
619            encountered.add(last)
620        if not didIndent, .indent
621        return TypeSpecs(isNames, attribs, inheritsProxies, implementsProxies, addsProxies)
622
623    def _commaSepTypes(proxies as List<of ITypeProxy>)
624        expectComma = false
625        while true
626            if expectComma
627                if .peek.isKeyword, break
628                .expect('COMMA')
629            proxies.add(.typeId)
630            expectComma = true
631            if .peek.which == 'EOL'
632                .grab
633                break
634            else if .peek.isKeyword
635                break
636
637    def endOfTypeSpecClause
638        """
639        Ends a type clause on EOL or a keyword.
640        Throws an error for any other kind of token or end-of-tokens.
641        Used by .typeSpecDecls.
642        """
643        if .optional('COLON')
644            _warning('Colons are not used to start indented blocks. You can remove the colon.')
645        if .peek is nil
646            .throwError('Unexpected end of source.')
647        else if .peek.which == 'EOL'
648            .endOfLine
649        else if .peek.isKeyword
650            pass
651        else
652            .throwError('Expecting end-of-line or next clause instead of "[.peek.which]".')
653
654    def classDecl as Class
655        wordToken = .expect('CLASS')
656        peek = .peek.which
657        if peek == 'ID'
658            idToken = .expect('ID')
659            name = idToken.value to String
660        else if peek == 'OPEN_GENERIC'
661            idToken = .expect('OPEN_GENERIC')
662            name = idToken.value to String
663        else if peek == 'OPEN_CALL'
664            .throwError(_syntaxForClassInheritanceMsg)
665        else
666            .throwError('Expecting a class name.')
667        if .peek.which=='COLON' and .peek(1)<>nil and .peek(1).which=='ID'
668            .throwError(_syntaxForClassInheritanceMsg)
669        if name[0] not in _uppercaseLetters
670            .throwError('Class names must start with an uppercase letter in order to avoid collisions with other identifiers such as arguments and local variables.')
671
672        genericParams = .declGenericParams(idToken)
673        name = .nameForDeclGenericParams(idToken, genericParams)
674        typeSpecs = .typeSpecDecls(idToken, genericParams)
675        inheritsProxy as ITypeProxy?
676        if typeSpecs.inheritsProxies.count > 1
677            .throwError('Cannot inherit from multiple types. Put classes after "inherits", interfaces after "implements" and mixins after "adds".')
678        else if typeSpecs.inheritsProxies.count == 1
679            inheritsProxy = typeSpecs.inheritsProxies[0]
680        docString = .docString
681       
682        theClass = Class(wordToken, idToken, name, .makeList<of IType>(genericParams), typeSpecs.isNames, typeSpecs.attributes, inheritsProxy, typeSpecs.implementsProxies, typeSpecs.addsProxies, docString)
683
684        # For nested classes, set .parentBox reference
685        if _boxStack.count
686            parentBox = .curBox
687            if parentBox inherits ClassOrStruct
688                theClass.parentBox = parentBox
689            else
690                .throwError('Cannot nest a type underneath a [parentBox.englishName]')  # TODO: needs a test case
691
692        _boxStack.push(theClass)
693#       .isNamesStack = Stack<of String>()
694        .bodiedBoxMemberDecls(theClass)
695        _boxStack.pop
696
697        return theClass
698
699    def makeList<of TOut>(inList as System.Collections.IList) as List<of TOut>
700        # This feels awkward as hell, but it's a .NET typing thing, not a Cobra thing.
701        # I need List<of GenericParam> in the my local code for declaring generics, but the various box inits need to accept List<of IType>
702        # TODO: remove this somehow. Maybe Cobra could have a promotion feature:
703        # List<of IType>(params promote to IEnumerable<of IType>)
704        # "promote to" works for generics where the new promo type has parameter types that are the same or ancestors to the original parameter types *and* ... ???
705        outList = List<of TOut>()
706        for item in inList
707            outList.add(item to TOut)
708        return outList
709
710    def hasAttribs as AttributeList
711        attribs = AttributeList()
712        if .optional('HAS')
713            while true
714                isReturnTarget = .optional('RETURN') is not nil
715                expr = .attribExpr(0)
716                attribs.add(AttributeDecl(expr, isReturnTarget))
717                if not .optional('COMMA'), break
718            .endOfTypeSpecClause
719        return attribs
720
721    def attribExpr(level as int) as Expr?
722        exprs = List<of Expr>()
723        dots  = List<of IToken>()
724        expr as Expr?
725        while true
726            branch .peek.which
727                on 'ID'
728                    expr = if(exprs.count, MemberExpr(.grab), IdentifierExpr(.grab))
729                on 'OPEN_CALL'
730                    token = .grab
731                    args = .commaSepExprs('RPAREN.', true, true)
732                    if exprs.count
733                        expr = CallExpr(token, token.value to String, args, true)
734                    else
735                        expr = PostCallExpr(token, IdentifierExpr(token, token.value to String), args)
736                else
737                    .throwError('Syntax error when expecting attribute.')
738            exprs.add(expr)
739            if .peek.which == 'DOT'
740                dots.add(.grab)
741            else
742                break
743        assert exprs.count
744        if exprs.count > 1
745            expr = exprs[0]
746            for i in 1 : exprs.count
747                expr = DotExpr(dots[i-1], 'DOT', expr, exprs[i])
748        if not expr inherits IdentifierExpr and not expr inherits PostCallExpr and not expr inherits CallExpr and not expr inherits DotExpr
749            .throwError('Invalid attribute.')
750        if expr inherits DotExpr
751            assert expr.left inherits IdentifierExpr or expr.left inherits DotExpr
752        return expr
753           
754    def isDeclNames as List<of String>
755        """
756        Example source:
757            # The | below is not literal--it's where this method starts parsing.
758            def compute
759                |is virtual, protected
760        Example return values:
761            []
762            ['shared']
763            ['private', 'shared']
764        Errors:
765            TODO
766        Used by: classDecl, interfaceDecl, enumDecl
767        """
768        hasIsNames = false
769        names = _isDeclNamesNoEOL(out hasIsNames)
770        if hasIsNames
771            .endOfTypeSpecClause
772        return names
773
774    def _isDeclNamesNoEOL(hasIsNames as out bool) as List<of String>
775        """ Parse for possible stream of isNames without terminating EOL. """
776        names = List<of String>(_isNamesStack)
777        hasIsNames = false
778        if .optional('IS') is nil
779            return names
780        hasIsNames = true
781        while true
782            what = .grab.text
783            if what in .validIsNames
784                if what == 'fake'
785                    _warning(.last, '"fake" has been deprecated. Use "extern" instead.'# deprecated on 2008-10-03
786                    what = 'extern'
787                names.add(what)
788            else
789                if what == 'final'
790                    .throwError(.last, 'Use "readonly" or "const" rather than "final".')
791                else   
792                    .throwError('Not expecting "[what]".')
793            if not .optional('COMMA')
794                if .peek.text in .validIsNames
795                    .throwError(.peek, 'Multiple access modifiers should be separated by commas such as "[what], [.peek.text]".')
796                break
797        # TODO: error on virtual and shared
798        # to-do: move these to _bindInt
799        # error on non virtual and override
800        if 'nonvirtual' in names and 'override' in names
801            .recordError(.last, 'Cannot specify both "nonvirtual" and "override".')
802        # to-do: cannot specify 'new' and 'override'
803        # error if 2 or more of 'public', 'protected', 'private', 'internal'
804        accessModifierCount = 0
805        for vam in _validAccessModifiers
806            if vam in names, accessModifierCount += 1
807        if accessModifierCount > 1
808            .recordError(.last, 'Can only specify one of "public", "protected", "private" or "internal".')
809        return names
810
811    var _validIsNames as List<of String>?
812    var _validAccessModifiers = ['public', 'protected', 'private', 'internal']
813
814    get validIsNames as List<of String>
815        if _validIsNames is nil
816            _validIsNames = [
817                'fake', 'extern', 'shared', 'virtual', 'nonvirtual', 'override', 'new',
818                'public', 'protected', 'private', 'internal',
819                'abstract', 'partial', 'readonly',
820            ]
821        return _validIsNames to !
822
823    def declGenericParams(token as IToken) as List<of GenericParam>
824        """
825        This parses and returns the generic params for a box
826        declaration. It does NOT work for the generic params in other
827        types such as a return type or a base class type--those can have
828        other kinds parameters including other generic types and basic
829        types. Box declarations only have generic parameter names.
830        """
831        params = List<of GenericParam>()
832        # trace token
833        if token.which == 'OPEN_GENERIC'
834            expectComma = false
835            while true
836                if .peek.which == 'GT'
837                    .grab
838                    break
839                if expectComma
840                    .expect('COMMA')
841                ident = .expect('ID').text
842                if ident.startsWithLowerLetter
843                    .throwError('Generic parameter names must start with an uppercase letter in order to avoid collisions with other identifiers such as arguments and local variables.')
844                params.add(GenericParam(ident))
845                expectComma = true
846        # trace params
847        return params
848
849    def nameForDeclGenericParams(token as IToken, paramList as List<of GenericParam>) as String
850        """
851        This is called after `declGenericParams` to update the name of the declaring type.
852        CC: add an "out name" parameter to `declGenericParams` and axe this method.
853        """
854        name = token.text.trim
855        if token.which=='OPEN_GENERIC'
856            for i in paramList.count-1, name += ','
857            name += '>'
858        return name
859
860    def mixinDecl as Mixin
861        wordToken = .expect('MIXIN')
862        peek = .peek.which
863        if peek == 'ID'
864            idToken = .expect('ID')
865            name = idToken.value to String
866        else if peek == 'OPEN_GENERIC'
867            idToken = .expect('OPEN_GENERIC')
868            name = idToken.value to String
869        else if peek == 'OPEN_CALL'
870            .throwError(_syntaxForClassInheritanceMsg)
871        else
872            .throwError('Expecting a name.')
873        if name[0] not in _uppercaseLetters
874            .throwError('Mixin names must start with an uppercase letter in order to avoid collisions with other identifiers such as arguments and local variables.')
875
876        genericParams = .declGenericParams(idToken)
877        name = .nameForDeclGenericParams(idToken, genericParams)
878        typeSpecs = .typeSpecDecls(idToken, genericParams)
879        inheritsProxy as ITypeProxy?
880        # note: these restrictions may be temporary
881        if typeSpecs.inheritsProxies.count > 0
882            .throwError('Mixins cannot inherit from other types.')
883        if typeSpecs.implementsProxies.count > 0
884            .throwError('Mixins cannot implement interfaces.')
885        if typeSpecs.addsProxies.count > 0
886            .throwError('Mixins cannot add other mixins.')
887        docString = .docString
888       
889        theMixin = Mixin(wordToken, idToken, name, .makeList<of IType>(genericParams), typeSpecs.isNames, typeSpecs.attributes, inheritsProxy, typeSpecs.implementsProxies, typeSpecs.addsProxies, docString)
890
891        # TODO when supporting nested classes, look at the _boxStack and set a back pointer here
892        _boxStack.push(theMixin)
893#       .isNamesStack = Stack<of String>()
894        .bodiedBoxMemberDecls(theMixin)
895        _boxStack.pop
896
897        return theMixin
898
899    var _syntaxForInterfaceInheritanceMsg = 'The syntax for inheritance is to put "inherits BaseInterfaceA, BaseInterfaceB" on the following line, indented.'
900
901    def interfaceDecl as Interface
902        wordToken = .expect('INTERFACE')
903        peek = .peek.which
904        if peek == 'ID'
905            idToken = .expect('ID')
906            name = idToken.value to String
907        else if peek == 'OPEN_GENERIC'
908            idToken = .expect('OPEN_GENERIC')
909            name = idToken.value to String
910        else if peek == 'OPEN_CALL'
911            .throwError(_syntaxForInterfaceInheritanceMsg)
912        else
913            .throwError('Expecting an interface name.')
914        if name[0] not in _uppercaseLetters
915            .throwError('Interface names must start with an uppercase letter in order to avoid collisions with other identifiers such as arguments and local variables.')
916        # too draconian
917        #if not name.startsWith('I')
918        #   .throwError('Interfaces must start with a capital "I".')
919        if .peek.which=='COLON' and .peek(1)<>nil and .peek(1).which=='ID'
920            .throwError(_syntaxForInterfaceInheritanceMsg)
921
922        genericParams = .declGenericParams(idToken)
923        name = .nameForDeclGenericParams(idToken, genericParams)
924        typeSpecs = .typeSpecDecls(idToken, genericParams)
925        if typeSpecs.implementsProxies.count
926            .throwError('Encountered "implements" in interface declaration. Use "inherits" instead.')
927        docString = .docString
928        # TODO: can an interface be nested in another interface?
929        theInterface = Interface(wordToken, idToken, name, .makeList<of IType>(genericParams), typeSpecs.isNames, typeSpecs.attributes, typeSpecs.inheritsProxies, docString)
930
931        _boxStack.push(theInterface)
932#       .isNamesStack = Stack<of String>()
933        .bodiedBoxMemberDecls(theInterface)  # TODO: this shouldn't be bodiedBoxMemberDecls, right?
934        _boxStack.pop
935
936        return theInterface
937
938    def structDecl as Struct
939        wordToken = .expect('STRUCT')
940        peek = .peek.which
941        if peek == 'ID'
942            idToken = .expect('ID')
943            name = idToken.value to String
944        else if peek == 'OPEN_GENERIC'
945            idToken = .expect('OPEN_GENERIC')
946            name = idToken.value to String
947        else
948            .throwError('Expecting a struct name.')
949        if name[0] not in _uppercaseLetters
950            .throwError('Struct names must start with an uppercase letter in order to avoid collisions with other identifiers such as arguments and local variables.')
951
952        genericParams = .declGenericParams(idToken)
953        name = .nameForDeclGenericParams(idToken, genericParams)
954        typeSpecs = .typeSpecDecls(idToken, genericParams)
955        if typeSpecs.inheritsProxies.count > 0
956            .throwError('Structs cannot inherit. If you mean to implement an interface, use "implements" instead.')
957        docString = .docString
958
959        theStruct = Struct(wordToken, idToken, name, .makeList<of IType>(genericParams), typeSpecs.isNames, typeSpecs.attributes, nil, typeSpecs.implementsProxies, typeSpecs.addsProxies, docString)
960
961        # For nested classes, set .parentBox reference
962        if _boxStack.count
963            parentBox = .curBox
964            if parentBox inherits ClassOrStruct
965                theStruct.parentBox = parentBox
966            else
967                .throwError('Cannot nest a type underneath a [parentBox.englishName]')  # TODO: needs a test case
968
969        _boxStack.push(theStruct)
970#       .isNamesStack = Stack<of String>()
971        .bodiedBoxMemberDecls(theStruct)
972        _boxStack.pop
973
974        return theStruct
975
976    def eventDecl as BoxEvent
977        token = .expect('EVENT')
978        idToken = .expect('ID')
979        name = idToken.value to String
980        .expect('AS')
981        handlerType = .typeId
982        # ahem, duplicated from .boxVarDecl
983        docString = '' to ?
984        isNames = List<of String>(_isNamesStack)
985        if .peek.which=='IS'
986            isNames = .isDeclNames
987            attribs = .hasAttribs
988            assert .last.which=='EOL'
989            .ungrab  # need the EOL
990            if .optionalIndent
991                docString = .docString
992                .dedent
993        else if .optionalIndent
994            isNames = .isDeclNames
995            attribs = .hasAttribs
996            docString = .docString
997            .dedent
998        else
999            attribs = AttributeList()
1000        return BoxEvent(token, idToken, .curBox, name, isNames, attribs, docString, handlerType)
1001
1002    def enumDecl as EnumDecl
1003        wordToken = .expect('ENUM')
1004        idToken = .expect('ID')
1005        name = idToken.value to String
1006        if name[0] not in _uppercaseLetters
1007            .throwError('Enum types must start with uppercase letters to avoid collisions with other identifiers such as properties and methods.')
1008        .indent
1009        isNames = .isDeclNames
1010        attribs = .hasAttribs
1011        if .optional('OF')
1012            storageType = .typeId to ?
1013        docString = .docString
1014        enumMembers = List<of EnumMember>()
1015        nameSet = Set<of String>()
1016        while .peek.which <> 'DEDENT'
1017            .zeroOrMore('EOL')
1018            enumNameToken = .expect('ID')
1019            if .peek.which=='ASSIGN'
1020                .grab
1021                enumValue = .expect('INTEGER_LIT').value to int?
1022            else
1023                enumValue = nil
1024            if not .optional('COMMA')
1025                .endOfLine
1026            if enumNameToken.text in nameSet
1027                .recordError(enumNameToken, 'Already defined "[enumNameToken.text]" earlier.')
1028            else
1029                enumMembers.add(EnumMember(enumNameToken, enumValue))
1030                nameSet.add(enumNameToken.text)
1031            # TODO: check for values that repeat or go backwards
1032        .dedent
1033        if not enumMembers.count
1034            .throwError('Missing enum members.')
1035        if _boxStack.count
1036            parent = .curBox to IParentSpace
1037        else
1038            parent = .curNameSpace to IParentSpace
1039        return EnumDecl(parent, wordToken, idToken, name, isNames, attribs, storageType, docString, enumMembers)
1040
1041    def extendDecl as Extension
1042        # TODO: extend Qualified.Name
1043        wordToken = .expect('EXTEND')
1044        extendedTypeId = .typeId
1045
1046        .indent
1047
1048        isNames = .isDeclNames
1049        attribs = .hasAttribs
1050        if attribs.count
1051            .throwError('Extensions cannot add attributes.')
1052
1053        if .optional('WHERE')
1054            .throwError('Extensions cannot add constraints.')
1055
1056        if .optional('INHERITS')
1057            .throwError('Extensions cannot change inheritance.')
1058
1059        if .optional('IMPLEMENTS')
1060            .throwError('Extensions cannot add interface implementations.')
1061
1062        docString = .docString
1063
1064        ext = Extension(wordToken, extendedTypeId.token, extendedTypeId, isNames, docString)
1065
1066        _boxStack.push(ext)
1067        .bodiedBoxMemberDecls(ext)
1068        _boxStack.pop
1069
1070        return ext
1071
1072    def genericConstraints(token as IToken, params as List<of GenericParam>)
1073        while .optional('WHERE')
1074            if token.which <> 'OPEN_GENERIC'
1075                .throwError('Unexpected where clause for non-generic declaration.')
1076            paramName = .expect('ID').value
1077            found = false
1078            for param in params
1079                if param.name == paramName
1080                    found = true
1081                    break
1082            if not found, .throwError('Unknown generic parameter "[paramName]".')
1083            if param.constraints.count, .throwError('Already specified constraints for "[paramName]".')
1084            .expect('MUST')
1085            .expect('BE')
1086            expectComma = false
1087            while true
1088                if expectComma, .expect('COMMA')
1089                param.constraints.add(.genericConstraint)
1090                if .optional('EOL'), break
1091                expectComma = true
1092
1093    def genericConstraint as GenericConstraint
1094        """
1095        Consumes a generic constraint and returns it.
1096        Constraints include classes, interfaces and the keywords:
1097            class struct callable
1098        """
1099        peek = .peek.which
1100        branch peek
1101            on 'CLASS', return GenericClassConstraint(.grab)
1102            on 'STRUCT', return GenericStructConstraint(.grab)
1103            on 'CALLABLE', return GenericCallableConstraint(.grab)
1104            else, return GenericTypeConstraint(.typeId)
1105
1106    def nameSpaceDecl as NameSpace
1107        .expect('NAMESPACE')
1108        curNameSpace = .curNameSpace
1109        assert not curNameSpace.isUnified
1110        idTokens = [.expect('ID')]
1111        while true
1112            if .peek.which == 'DOT'
1113                .grab
1114                idTokens.add(.expect('ID'))
1115            else
1116                break
1117        firstNameSpace as NameSpace?
1118        for tok in idTokens
1119            name = tok.value to String
1120            curNameSpace = curNameSpace.getOrMakeNameSpaceNamed(tok, name)
1121            if firstNameSpace is nil
1122                firstNameSpace = curNameSpace
1123            assert not curNameSpace.isUnified
1124            _nameSpaceStack.push(curNameSpace)         
1125        try
1126            .indent
1127            curNameSpace.addDocString(.docString)
1128            .zeroOrMore('EOL')
1129            while true
1130                peek = .peek
1131                if peek is nil, .throwError('Expecting a namespace member, but source code ended.')
1132                tok = peek to !
1133                if tok.which == 'DEDENT', break
1134                branch tok.which
1135                    on 'CLASS',     _nameSpaceAddDecl(curNameSpace, .classDecl)
1136                    on 'INTERFACE', _nameSpaceAddDecl(curNameSpace, .interfaceDecl)
1137                    on 'STRUCT',    _nameSpaceAddDecl(curNameSpace, .structDecl)
1138                    on 'EXTEND',    _nameSpaceAddDecl(curNameSpace, .extendDecl)
1139                    on 'USE',       curNameSpace.addUseDirective(.useDirective)
1140                    on 'NAMESPACE', .nameSpaceDecl
1141                    on 'ENUM',      _nameSpaceAddDecl(curNameSpace, .enumDecl)
1142                    on 'SIG',       _nameSpaceAddDecl(curNameSpace, .declareMethodSig)
1143                    else,           .throwError('Expecting a namespace member but got [tok].')
1144            .dedent
1145        finally
1146            for tok in idTokens
1147                _nameSpaceStack.pop
1148        return firstNameSpace to !
1149
1150    def _nameSpaceAddDecl(ns as NameSpace, decl as INameSpaceMember) as INameSpaceMember
1151        """
1152        Adds the decl to the given namespace or throws an error for duplicate declarations.
1153        Also, handles `partial` classes and structs.
1154        Returns the same declaration, or in the case of `partial`, the original declaration.
1155        """
1156        # TODO: complain if inheritance or is-names are different. at least for inheritance, that needs to be done post-parsing
1157        checkForDups = true
1158        if 'partial' in decl.isNames
1159            if decl inherits Box
1160                if not (decl inherits Class or decl inherits Struct or decl inherits Interface)
1161                    .throwError(decl.token, '[decl.englishName.capitalized] cannot be "partial".')
1162                checkForDups = false
1163                otherDecl = ns.unifiedNameSpace.declForName(decl.name)
1164                if otherDecl
1165                    if otherDecl.getType is not decl.getType
1166                        .throwError(decl.token, 'The other partial declaration is a "[otherDecl.englishName]", not a "[decl.englishName]".')
1167                    if 'partial' not in otherDecl.isNames
1168                        .throwError(decl.token, 'The other declaration is not marked "partial".')
1169                    otherBox = otherDecl to Box  # will always work because of above check
1170                   
1171                    # deal with partial classes and inheritance
1172                    if otherBox inherits Class
1173                        myDecl = decl to Class # safe because already checked that it was the same type as otherBox
1174                        if otherBox.baseNode is nil                 
1175                            if myDecl.baseNode is not nil
1176                                otherBox.baseNode = myDecl.baseNode
1177                        else
1178                            myOtherType = otherBox.baseNode to AbstractTypeIdentifier
1179                            if otherBox.baseNode == myDecl.baseNode
1180                                _warning(myDecl.token, 'The class "[otherBox.name]" already inherits from "[myOtherType.name]".')
1181                            else if myDecl.baseNode is not nil
1182                                thisType = myDecl.baseNode to AbstractTypeIdentifier
1183                                .throwError(myDecl.token, 'The class "[otherBox.name]" already inherits from "[myOtherType.name]" and cannot inherit from "[thisType.name]" too.')
1184                   
1185                    for memberDecl in decl.declsInOrder
1186                        overload = _overloadIfNeeded(memberDecl.idToken, otherBox, memberDecl.name)
1187                        if overload
1188                            (memberDecl to IOverloadable).overloadGroup = nil
1189                            overload.addMember(memberDecl to IOverloadable)
1190                        else
1191                            otherBox.addDecl(memberDecl)
1192                        memberDecl.mergedIntoPartialBox(otherBox)
1193                    return otherDecl
1194            else
1195                .throwError(decl.token, '[decl.englishName.capitalized] cannot be "partial".')
1196        if checkForDups
1197            if ns.declForName(decl.name)
1198                .throwError((decl to dynamic).token to IToken, 'The namespace "[ns.fullName]" already contains a declaration named "[decl.name]".')  # TODO: give an "error" for the location
1199            if ns.unifiedNameSpace.declForName(decl.name)
1200                .throwError((decl to dynamic).token to IToken, 'The namespace "[ns.fullName]" already contains a declaration named "[decl.name]" in another file.')  # TODO: give an "error" for the location
1201        ns.addDecl(decl)
1202        return decl
1203
1204    def bodiedBoxMemberDecls(box as Box)
1205# TODO: remove this when SystemInterfaces.cobra and "is extern" goes away. 2007-12-30: "is extern" might be around for a long time.
1206#       require
1207#           not box inherits Interface
1208        body
1209            breakLoop = false  # cannot use 'break' to stop a 'while' loop in a branch statement. CC?
1210            while not breakLoop
1211                _isTraceOn = true
1212                branch .peek.which
1213                    on 'PASS'
1214                        .classPass
1215                        breakLoop = true
1216                    on 'DEDENT', breakLoop = true
1217                    on 'CUE', .addDecl(box, .declareCue)
1218                    on 'DEF', .addDecl(box, .declareMethod)
1219                    on 'GET', .addDecl(box, .declareGetOnlyProperty)
1220                    on 'SET', .addDecl(box, .declareSetOnlyProperty)
1221                    on 'PRO', .addDecl(box, .declareProperty)
1222                    on 'VAR', .addDecl(box, .boxFieldDecl(true))
1223                    on 'CONST', .addDecl(box, .boxFieldDecl(false))
1224                    on 'INVARIANT', .declareInvariant
1225                    on 'EOL', .endOfLine
1226                    on 'ENUM', .addDecl(box, .enumDecl)
1227                    on 'EVENT', .addDecl(box, .eventDecl)
1228                    on 'SIG', .addDecl(box, .declareMethodSig)
1229                    on 'SHARED', .bodiedBoxMemberDeclsShared(box)
1230                    on 'TEST', .testSection(box)
1231                    on 'CLASS',  .addDecl(box, .classDecl)  # nested types
1232                    on 'STRUCT', .addDecl(box, .structDecl) # nested types
1233                    else
1234                        branch .peek.text
1235                            on 'ensure', sugg = 'invariant'
1236                            on 'void', sugg = 'def'
1237                            else, sugg = ''
1238                        if sugg <> ''
1239                            sugg = ' Instead of "[.peek.text]", try "[sugg]".'
1240                        .throwError('Got [.peek] when expecting class, def, enum, get, invariant, pro, set, shared, sig, struct, test or var.[sugg]')
1241            .dedent
1242
1243    def addDecl(box as Box, member as IBoxMember?)
1244        if member
1245            other = box.declForNameCI(member.name)
1246            if other and other.name <> member.name
1247                .throwError('Cannot have members with the same name in different case ("[member.name]" here and "[other.name]" on line [(other to SyntaxNode).token.lineNum]).')
1248                # TODO: Give another error with the line number of the other definition (and then change message above)
1249            box.addDecl(member)
1250
1251    def bodiedBoxMemberDeclsShared(box as Box)
1252        .expect('SHARED')
1253        .indent
1254        _isNamesStack.push('shared')
1255        try
1256            .bodiedBoxMemberDecls(box)
1257        finally
1258            _isNamesStack.pop
1259
1260    def classPass
1261        if .curBox.declsInOrder.count
1262            _warning('Encountered "pass" in a class that already has declared members.'# TODO: change to an error
1263        .grab
1264        .endOfLine
1265
1266    def boxFieldDecl(isVar as bool) as BoxField
1267        token = .expect(if(isVar, 'VAR', 'CONST'))
1268        idToken = .expect('ID')
1269        name = idToken.text
1270        other = .curBox.declForName(name)
1271        if other
1272            .throwError('The name "[name]" was already declared earlier.')  # TODO: show the location of the previous symbol
1273        numUnderscores = 0
1274        s = name
1275        while s.startsWith('_')
1276            s = s.substring(1)
1277            numUnderscores += 1
1278        if not s.length
1279            .throwError('A class variable must be made of more than underscores. Try "[name]x" or "[name]1".')
1280        if isVar and not s[0].isLower
1281            sugg = String(c'_', numUnderscores) + s[0].toString.toLower + s[1:]
1282            sugg = ' Try "[sugg]".'
1283            .throwError('Class variables must start with lowercase letters (after the underscore(s)) to distinguish them from other types of identifiers.[sugg]')
1284        type = if(.optional('AS'), .typeId, nil) to ITypeProxy?
1285        if .optional('ASSIGN')
1286            initExpr = .expression to ?
1287        else
1288            initExpr = nil
1289            if type is nil
1290                type = .typeProvider.defaultType
1291        docString = '' to ?
1292        isNames = List<of String>(_isNamesStack)
1293        if .peek.which=='IS'
1294            isNames = .isDeclNames
1295            attribs = .hasAttribs
1296            assert .last.which=='EOL'
1297            .ungrab  # need the EOL
1298            if .optionalIndent
1299                docString = .docString
1300                .dedent
1301        else if .optionalIndent
1302            isNames = .isDeclNames
1303            attribs = .hasAttribs
1304            docString = .docString
1305            .dedent
1306        if isVar
1307            field = BoxVar(token, idToken, .curBox, name, type, isNames, initExpr, attribs, docString) to BoxField
1308        else
1309            field = BoxConst(token, idToken, .curBox, name, type, isNames, initExpr, attribs, docString)
1310        return field
1311               
1312    def declareInvariant
1313        .expect('INVARIANT')
1314        if .optional('COLON')
1315            if .peek.which == 'EOL'
1316                _warning('Colons are not used to start indented blocks. You can remove the colon.')
1317            else
1318                _warning('Colons are not required with invariants. You can remove the colon.')
1319        if .peek.which == 'EOL'
1320            .indent
1321            count = 0
1322            while true
1323                if .peek.which == 'EOL'
1324                    .grab
1325                    continue
1326                if .peek.which == 'DEDENT'
1327                    if count == 0
1328                        .throwError('Expecting one or more expressions for "invariant".')
1329                    break
1330                .curBox.invariants.add(.expression)
1331                .expect('EOL')
1332                count += 1
1333            .dedent
1334        else
1335            .curBox.invariants.add(.expression)
1336            .endOfLine
1337
1338    def declareCue as AbstractMethod?
1339        m = .declareMethod
1340        if m
1341            name = m.name
1342            if name.startsWith('cue.'), name = name[4:]
1343            if not name.isOneOf('init.finalize.hash.compare.equals.enumerate.')
1344                .throwError('Unknown cue "[m.name]". Expecting "init", "finalize", "hash", "compare", "equals" or "enumerate".')
1345        return m
1346
1347    def declareMethod as AbstractMethod?
1348        require _typeProvider
1349        token = .expect('DEF', 'CUE')
1350        opener = .grab to !
1351        if not opener.which.isOneOf('ID.OPEN_CALL.OPEN_GENERIC.') and not opener.isKeyword
1352            .throwError('Encountered [opener.which] when expecting an identifier.')
1353        genericParams = _methodGenericParams(opener)
1354
1355        name = opener.value to String
1356        curBox = .curBox
1357        if name == curBox.name or name.capitalized == curBox.name
1358            .throwError('Method names cannot be the same as their enclosing [curBox.englishName]. Use `cue init` for creating an initializer/constructor or choose another name.')  # TODO list the enclosing types location
1359
1360        params = _methodParams(opener)
1361        returnType = _methodReturnType
1362
1363        isNames = List<of String>(_isNamesStack)
1364        attribs = AttributeList()
1365        implementsType = nil to ITypeProxy?
1366
1367        # Rules:
1368        #  * if curBox is an Interface then no body
1369        #  * if 'abstract' is in the is-names then no body
1370        #  * is-names and has-attrs can be on the same line or the next
1371        #  * no `init` in interfaces
1372        #  * no return type for `init`
1373        #  * no `implements` in interfaces or for `init`
1374
1375        encountered = List<of String>()  # token types     
1376        didIndent = false
1377        isDone = false
1378        while true
1379            peek = .peek
1380            if peek is nil, .throwError('Unexpected end of source.')
1381            last = .peek.which
1382            branch last
1383                on 'EOL'
1384                    .grab
1385                on 'INDENT'
1386                    .grab
1387                    if didIndent
1388                        if _spaceAgnosticIndentLevel == 0
1389                            .throwError(ErrorMessages.expectingStatementInsteadOfIndentation)
1390                        else
1391                            _spaceAgnosticIndentLevel += 1
1392                    didIndent = true
1393                on 'IS'
1394                    isNames = .isDeclNames
1395                on 'HAS'
1396                    attribs = .hasAttribs
1397                on 'WHERE'
1398                    .genericConstraints(opener, genericParams)
1399                on 'IMPLEMENTS'
1400                    .grab
1401                    implementsType = .typeId
1402                else
1403                    isDone = true
1404            if last.isOneOf('EOL.INDENT.'), continue
1405            if isDone, break
1406            if last in encountered and last <> 'WHERE'
1407                .throwError('Encountered "[last]" twice.')
1408            encountered.add(last)
1409        assert .last.which.isOneOf('EOL.INDENT.')
1410
1411        nothingMore = _endMethodDecl(didIndent)
1412        docString = .docString
1413        if token.which == 'CUE' and name == 'init'
1414            _verifyInitializer(returnType, implementsType)
1415            method = Initializer(token, opener, .curBox, params, isNames, attribs, docString) to AbstractMethod
1416        else
1417            method = Method(token, opener, .curBox, name, .makeList<of IType>(genericParams), params, returnType, implementsType, isNames, attribs, docString)
1418        if nothingMore
1419            if method.bodyExclusion is nil, .throwError('Missing method body for "[name]".')
1420        else
1421            .statementsFor(method)
1422        _verifyMethodStatements(method, isNames, curBox)
1423        overload = _overloadIfNeeded(method.token, curBox, method.name# also checks for various errors
1424        if overload
1425            overload.addMember(method)
1426            return nil
1427        return method
1428
1429    def _methodGenericParams(opener as IToken) as List<of GenericParam>
1430        """ Returns a list of declared GenericParams, possibly empty if there are none. Helper for .declareMethod. """
1431        if opener.which == 'OPEN_GENERIC'
1432            genericParams = .declGenericParams(opener)
1433        else
1434            genericParams = List<of GenericParam>()
1435        return genericParams   
1436       
1437    def _methodReturnType as ITypeProxy
1438        """ Returns declared method return type or void type if none declared. Helper for .declareMethod. """
1439        if .optional('AS')
1440            returnType = .typeId to ITypeProxy
1441        else
1442            returnType = _typeProvider.voidType
1443        return returnType
1444
1445    def _methodParams(opener as IToken) as List<of Param>
1446        """ Returns a list of declared parameters, possibly empty if there were none declared. Helper for .declareMethod. """
1447        unnecessary = false
1448        if opener.which == 'OPEN_CALL'
1449            params = .paramDecls(/#skipParen=#/true)
1450            if params.count == 0, unnecessary = true
1451        else if opener.which == 'OPEN_GENERIC'
1452            if .optional('LPAREN')
1453                params = .paramDecls(/#skipParen=#/true)
1454                if params.count == 0, unnecessary = true
1455            else
1456                params = List<of Param>()
1457        else if opener.which == 'ID'  and  .optional('LPAREN')
1458            # def id (... - misplaced space after name 
1459            .throwError('Misplaced space between method name and opening brace "("') 
1460        else
1461            params = List<of Param>()
1462        if unnecessary, _warning(opener, 'Unnecessary parentheses. You can remove them.')
1463        return params   
1464
1465    def _verifyMethodStatements(method as AbstractMethod, isNames as IList<of String>, curBox as Box)
1466        """ Error checks on statements. Helper for .declareMethod. """
1467        if method.statements.count
1468            branch method.bodyExclusion
1469                on nil, pass
1470                on 'abstract',   .throwError('Cannot have statements for an abstract method.')
1471                on 'interface',  .throwError('Cannot have statements for a method in an [curBox.englishName].')
1472                on 'dll import', .throwError('Cannot have statements for a DllImport method.')
1473                on 'extern box', .throwError('Cannot have statements for an extern type.')
1474                else, throw FallThroughException([method.bodyExclusion, method])
1475        else
1476            if method.bodyExclusion is nil
1477                .throwError('Missing statements. Use "pass" or other statements.')
1478
1479    def _endMethodDecl(didIndent as bool) as bool   
1480        """ Slurp up indentation and indicate if end of declaration. Helper for .declareMethod. """
1481        if _spaceAgnosticIndentLevel
1482            sail = _spaceAgnosticIndentLevel
1483            if sail > 0, _spaceAgnosticIndentLevel -= 1  # for missing indent
1484            if sail < 0, _spaceAgnosticIndentLevel += 1  # for missing dedent
1485            _finishSpaceAgnostic
1486            if sail > 0
1487                nothingMore = false  # TODO: feels wrong
1488            else
1489                nothingMore = .last.which == 'INDENT' and .peek.which <> 'INDENT'  # _spaceAgnosticIndentLevel == 0
1490        else
1491            nothingMore = not didIndent and .last.which == 'EOL'
1492        return nothingMore
1493       
1494    def _verifyInitializer(returnType as ITypeProxy, implementsType as ITypeProxy?)
1495        """ Error checks on initializer. Helper for .declareMethod. """
1496        if .curBox inherits Interface
1497            .throwError('Cannot declare an initializer for an interface.')
1498        if returnType is not .typeProvider.voidType
1499            .throwError('Cannot declare a return type for an initializer.')
1500        if implementsType
1501            .throwError('Cannot specify `implements` for an initializer.')
1502
1503    def declareMethodSig as MethodSig
1504        require _typeProvider
1505        wordToken = .expect('SIG')
1506        opener = .grab
1507        if not opener.which.isOneOf('ID.OPEN_CALL.') and not opener.isKeyword
1508            .throwError('Encountered [opener.which] when expecting an identifier.')
1509        name = opener.value to String
1510        curContainer = .curContainer
1511        other = curContainer.declForName(name)
1512        if other
1513            .throwError('There is already another class member with the name "[name]".')  # TODO list its location and possibly what it is
1514        else
1515            other = curContainer.declForName(name)  # TODO: should be a CI there for case-insensitive
1516            if other
1517                .throwError('There is already another class member with the name "[other.name]". You must differentiate member names by more than just case.')
1518            if not name[0].isUpper
1519                .recordError('Method signatures are types and must start with an uppercase letter to distinguish them from other types of identifiers. ([name])')
1520
1521        if opener.which=='OPEN_CALL'
1522            params = .paramDecls(true)
1523            if params.count == 0
1524                _warning(opener, 'Unnecessary parentheses. You can remove them.')
1525        else
1526            params = List<of Param>()
1527
1528        if .optional('AS')
1529            returnType = .typeId to ITypeProxy?
1530        else
1531            returnType = _typeProvider.voidType
1532        assert returnType
1533
1534        hasIsNames = false
1535        if .peek.which == 'IS'
1536            isNames = .isDeclNames
1537            hasIsNames = true
1538        else
1539            .endOfLine
1540
1541        if not .optional('INDENT')
1542            # with no indent there can be no additional specs like attributes, contracts or body
1543            if not hasIsNames
1544                isNames = List<of String>(_isNamesStack)
1545            attribs = AttributeList()
1546            docString = '' to ?
1547        else
1548            # parse additional method declaration
1549            if not hasIsNames
1550                isNames = .isDeclNames
1551            attribs = .hasAttribs
1552            docString = .docString
1553            .dedent
1554
1555        methodSig = MethodSig(wordToken, opener, curContainer to IParentSpace, name, params, returnType, isNames, attribs, docString)
1556
1557        return methodSig
1558       
1559    def declareProperty as ProperDexer?
1560        """
1561        Parse full ('pro') form of a property (and indexer)
1562        Example source
1563            pro age as int
1564                get
1565                    return _age
1566                set
1567                    assert value>0
1568                    _age = value
1569
1570        If .curBox is an Interface, then no body.
1571        If 'abstract' is in the "is names" then no body.
1572        The "is names" can be on the same line or the next.
1573        No explicit return type implies "as dynamic" (same as arguments).
1574        No "implements" in interfaces.
1575        """
1576        curBox = .curBox
1577        prop as ProperDexer?
1578        token = .expect('PRO')
1579        overload as MemberOverload?
1580        if .optional('LBRACKET')
1581            idToken = .last to !
1582            params = .paramDecls(true, 'RBRACKET')
1583            name = r'[]'
1584            overload = _overloadIfNeeded(idToken, curBox, name)
1585        else
1586            idToken = .idOrKeyword
1587            name = idToken.text
1588            .checkProperty(name)
1589            if .optional('FROM')
1590                return .declarePropertyFrom(token, idToken, name, 'getset')
1591            params = List<of Param>()
1592        if .optional('AS')
1593            returnType = .typeId to ITypeProxy
1594        else
1595            returnType = _typeProvider.defaultType
1596        # TODO: implements?
1597        hasIsNames = false
1598        isAbstract = false
1599        if .peek.which == 'IS'
1600            isNames = .isDeclNames
1601            isAbstract = 'abstract' in isNames
1602            hasIsNames = true
1603            indent = .optional('INDENT')
1604        else
1605            indent = .optionalIndent
1606        if indent
1607            if not hasIsNames
1608                isNames = .isDeclNames
1609            isAbstract = 'abstract' in isNames
1610            hasIsNames = true
1611            attribs = .hasAttribs
1612            docString = .docString
1613        else
1614            # no additional specs
1615            if not isAbstract and curBox.canHaveStatements
1616                .throwError('Missing body for property.')
1617            if not hasIsNames
1618                isNames = List<of String>(_isNamesStack)
1619            attribs = AttributeList()
1620            docString = ''
1621        if params.count
1622            prop = Indexer(token, idToken, curBox, name, params, returnType, isNames, attribs, docString)
1623        else
1624            prop = Property(token, idToken, curBox, name, returnType, isNames, attribs, docString)
1625        if indent
1626            while .peek.which == 'TEST'
1627                if curBox inherits Interface
1628                    _warning(.peek, 'Interface `test` sections are parsed, but ignored. In the future, classes will acquire the tests of the interfaces they implement.')
1629                # TODO: what about abstract?
1630                .testSection(prop to !)
1631            getWord = .optional('GET')
1632            if getWord
1633                if isAbstract or not curBox.canHaveStatements
1634                    prop.makeGetPart(getWord)
1635                else
1636                    .indent
1637                    .statementsFor(prop.makeGetPart(getWord))
1638            setWord = .optional('SET')
1639            if setWord
1640                if isAbstract or not curBox.canHaveStatements
1641                    prop.makeSetPart(setWord)
1642                else
1643                    .indent
1644                    .statementsFor(prop.makeSetPart(setWord))
1645            if not getWord and not setWord
1646                if isAbstract or not curBox.canHaveStatements
1647                    prop.makeGetPart(TokenFix.empty)
1648                    prop.makeSetPart(TokenFix.empty)
1649                else
1650                    .throwError('Expecting "get" or "set" for the property.')
1651            .dedent
1652        else
1653            prop.makeGetPart(TokenFix.empty)
1654            prop.makeSetPart(TokenFix.empty)
1655        if isAbstract and not curBox inherits Class
1656            .throwError('Only properties in classes can be marked abstract.')
1657        if overload
1658            assert prop
1659            overload.addMember(prop to Indexer)
1660            return nil
1661        else
1662            return prop
1663
1664    def declarePropertyFrom(token as IToken, idToken as IToken, name as String, coverWhat as String) as Property
1665        """ Parse the 'from ...' form of pro/get/set property decl. """
1666        require coverWhat.isOneOf('get.set.getset.')
1667        if .optional('VAR')
1668            varName = '_' + name
1669        else
1670            varName = .expect('ID').text
1671        declVarType = if(.optional('AS'), .typeId, nil)
1672        if .curBox inherits Interface
1673            .throwError('Cannot use the "from" form of a property inside an interface declaration.')
1674        if .peek.which == 'IS'
1675            hasIsNames = false
1676            isNames = _isDeclNamesNoEOL(out hasIsNames)
1677        if .optional('ASSIGN')
1678            initExpr = .expression to ?
1679        .endOfLine
1680        if .optional('INDENT')
1681            if not hasIsNames, isNames = .isDeclNames
1682            attribs = .hasAttribs
1683            docString = .docString
1684            .dedent
1685        else
1686            if not hasIsNames, isNames = List<of String>(_isNamesStack)
1687            attribs = AttributeList()
1688            docString = ''
1689        varDef = _genVarDef(declVarType, token, name, varName, initExpr)
1690        return Property(token, idToken, .curBox, name, isNames, attribs, varDef, coverWhat, docString)
1691       
1692    def _genVarDef(declVarType as ITypeProxy?, token as IToken, name as String, varName as String, initExpr as Expr?) as BoxVar
1693        """ Find existing backing variable for property or make one and return it."""
1694        # TODO: move this to a different phase. maybe the var decl comes later or in a partial class
1695        # TODO: if the var was declared separately, then warn about redeclaring its type, or error if the type is different
1696        possibleVarDef = .curBox.declForName(varName)
1697        if not possibleVarDef
1698            if initExpr or declVarType
1699                varDef = BoxVar(token, token, .curBox, varName, declVarType, List<of String>(_isNamesStack), initExpr, nil, 'Automatic backing var for property "[name]".')
1700                .curBox.addDecl(varDef)
1701                return varDef
1702            if not varName.startsWith('_') or varName.startsWith('__')
1703                .throwError('There is no variable named "[varName]" to match the property "[name]".')
1704            # tried looking for '_varName' now try '__varName'
1705            varName0, varName = varName, '_' + varName
1706            possibleVarDef = .curBox.declForName(varName)
1707            if not possibleVarDef
1708                .throwError('There is no variable named "[varName0]" or "[varName]" to match the property "[name]".')
1709               
1710        if possibleVarDef inherits BoxVar
1711            varDef = possibleVarDef
1712        else
1713            .throwError('A property can only cover for variables. [varName] is a [possibleVarDef].')  # TODO: .englishName?
1714        if initExpr and not varDef.setInitExpr(initExpr to !)   
1715            .throwError('Property backing variable "[varName]" has already been initialized.') # include line# of backing variable decl
1716        return varDef
1717           
1718    def declareGetOnlyProperty as ProperDexer?
1719        return _declareGetOrSetOnlyProperty(0)
1720
1721    def declareSetOnlyProperty as ProperDexer?
1722        return _declareGetOrSetOnlyProperty(1)
1723
1724    def _declareGetOrSetOnlyProperty(getOrSet as int) as ProperDexer?
1725        """
1726        Parse shortcut forms (get and set) of property (and indexer) declarations.
1727       
1728        Example source
1729            get meaningOfLife as int
1730                return 42
1731        """
1732        require getOrSet in [0, 1]
1733        curBox = .curBox
1734        prop as ProperDexer?
1735        token = .expect(if(getOrSet, 'SET', 'GET'))
1736        overload as MemberOverload?
1737        if .optional('LBRACKET')
1738            idToken = .last to !
1739            params = .paramDecls(true, 'RBRACKET')
1740            name = r'[]'
1741            overload = _overloadIfNeeded(idToken, curBox, name)
1742        else
1743            idToken = .idOrKeyword
1744            name = idToken.text
1745            .checkProperty(name)
1746            if .optional('FROM')
1747                return .declarePropertyFrom(token, idToken, name, if(getOrSet, 'set', 'get'))
1748            params = List<of Param>()
1749        if .optional('AS')
1750            returnType = .typeId to ITypeProxy
1751        else
1752            returnType = _typeProvider.defaultType
1753        # TODO: implements?
1754        hasIsNames = false
1755        isAbstract = false
1756        if .peek.which == 'IS'
1757            isNames = .isDeclNames
1758            isAbstract = 'abstract' in isNames
1759            hasIsNames = true
1760            indent = .optional('INDENT')
1761        else
1762            indent = .optionalIndent
1763        if indent
1764            if not hasIsNames
1765                isNames = .isDeclNames
1766            isAbstract = 'abstract' in isNames
1767            hasIsNames = true
1768            attribs = .hasAttribs
1769            docString = .docString
1770        else
1771            # no additional specs
1772            if not isAbstract and curBox.canHaveStatements
1773                .throwError('Missing body for property.')
1774            if not hasIsNames
1775                isNames = List<of String>(_isNamesStack)
1776            attribs = AttributeList()
1777            docString = ''
1778        if params.count
1779            prop = Indexer(token, idToken, curBox, name, params, returnType, isNames, attribs, docString)
1780        else
1781            prop = Property(token, idToken, curBox, name, returnType, isNames, attribs, docString)
1782        if indent
1783            while .peek.which == 'TEST'
1784                if curBox inherits Interface
1785                    _warning(.peek, 'Interface `test` sections are parsed, but ignored. In the future, classes will acquire the tests of the interfaces they implement.')
1786                # TODO: what about abstract?
1787                .testSection(prop to !)
1788            part = if(getOrSet, prop.makeSetPart(token), prop.makeGetPart(token))
1789            if isAbstract or not curBox.canHaveStatements
1790                .dedent
1791            else
1792                .statementsFor(part, prop)
1793        else
1794            if getOrSet
1795                prop.makeSetPart(TokenFix.empty)
1796            else
1797                prop.makeGetPart(TokenFix.empty)
1798        if isAbstract and not curBox inherits Class
1799            .throwError('Only properties in classes can be marked abstract.')
1800        if overload
1801            assert prop
1802            overload.addMember(prop to Indexer)
1803            return nil
1804        else
1805            return prop
1806
1807
1808    ##
1809    ## Parameter declarations
1810    ##
1811
1812    def paramDecls(skipParen as bool) as List<of Param>
1813        return .paramDecls(skipParen, 'RPAREN', true)
1814
1815    def paramDecls(skipParen as bool, rightParen as String) as List<of Param>
1816        return .paramDecls(skipParen, rightParen, false) 
1817       
1818    def paramDecls(skipParen as bool, rightParen as String, isSpaceAgnostic as bool) as List<of Param>
1819        if not skipParen, .expect('LPAREN')
1820        params = List<of Param>()
1821        expectComma = false
1822        while true
1823            if isSpaceAgnostic, _spaceAgnostic
1824            if .peek.which == rightParen
1825                .grab
1826                break
1827            if expectComma, .expect('COMMA')
1828            if isSpaceAgnostic, _spaceAgnostic
1829            param = .paramDecl
1830            params.add(param)
1831            if params.count==1 and param.name=='self' and param.isMissingType
1832                _warning('The first parameter is "self" which may be a Python carry-over on your part. Cobra does not require that (and calls it "this" anyway).')
1833            expectComma = true
1834        return params
1835
1836    def paramDecl as Param
1837        """
1838        Example source:
1839            foo as int
1840            foo as int?
1841            foo as vari String
1842            foo as List<of int>
1843            foo   # default type (dynamic?)
1844            foo as Type has Attribute(arg1, arg2)
1845            foo has Attribute(arg1, arg2)
1846        """
1847        if .peek.which == 'OPEN_GENERIC'
1848            # the programmer likely declared a parameter in the C syntax: List<of int> numbers
1849            .throwError('The correct parameter syntax is "paramName as ParamType".')
1850        if .looksLikeType(0) and .looksLikeVarNameIsNext(1)
1851            # the programmer likely declared a parameter in the C syntax: String s
1852            .throwError('The correct parameter syntax is "paramName as ParamType". Try "[.peek(1).text] as [.peek(0).text]".')
1853        if .peek.which == 'OUT'
1854            .throwError('The correct parameter syntax is "paramName as out ParamType".')
1855        if .peek.which.isOneOf('INOUT.REF.')
1856            .throwError('The correct parameter syntax is "paramName as inout ParamType".')
1857       
1858        token = .expect('ID')
1859        identifier = token.value to String
1860        .checkStartsLowercase(identifier, 'Parameter')
1861        dir, declaredAsUnused = Direction.In, false
1862        type as ITypeProxy?
1863        if .optional('AS')
1864
1865            # unused (virtual keyword)
1866            if .peek and .peek.which == 'ID' and .peek.text == 'unused'
1867                .grab
1868                declaredAsUnused = true
1869                   
1870            # code below expresses an undocumented dependency and ordering
1871            # allows only    VARI <type>  or  {OUT,INOUT} <type>
1872            # i.e cant have VARI and {OUT,INOUT}, if have VARI or OUT,INOUT must specify type
1873            branch .peek.which
1874                on 'VARI'
1875                    type = VariTypeIdentifier(.grab, .typeId)
1876                on 'OUT'
1877                    dir = Direction.Out
1878                    .grab
1879                    type = .typeId
1880                on 'INOUT'
1881                    dir = Direction.InOut
1882                    .grab
1883                    type = .typeId
1884                on 'REF'
1885                    .throwError('The correct keyword is "inout" rather than "ref" which is used in expressions to refer to methods.')
1886                else
1887                    type = .typeId
1888            isMissingType = false
1889        else
1890            type = TypeProxy(_typeProvider.defaultType)
1891            isMissingType = true
1892        attribs = AttributeList()
1893        if .optional('HAS')
1894            # TODO: multiple attribs like: x as Type has (Attrib1, Attrib2)
1895            attribs.add(AttributeDecl(.attribExpr(0)))
1896        # note: isMissingType is currently used to generate a warning in .paramDecls above
1897        # and may be used for anonymous method parameter type inference at some point
1898        return Param(token, type, isMissingType=isMissingType, direction=dir, isDeclaredAsUnused=declaredAsUnused, attributes=attribs)
1899
1900
1901    ##
1902    ## Top Level Statement Entry
1903    ##
1904
1905    def statementsFor(codePart as AbstractMethod)
1906        .statementsFor(codePart, nil)
1907
1908    def statementsFor(codePart as AbstractMethod, codePartContainingTest as BoxMember?)
1909        """
1910        Example source
1911            <any statement 1>
1912            <any statement 2>
1913            <any statement N>
1914        Example source
1915            require
1916                <cond 1>
1917                <cond N>
1918            ensure
1919                <cond 1>
1920                <cond N>
1921            test
1922                <any statement 1>
1923                <any statement 2>
1924                <any statement N>
1925            [body
1926                <any statement 1>
1927                <any statement 2>
1928                <any statement N>]
1929        Returns
1930            Nothing.
1931        Errors
1932            Already encountered "code" block.
1933            Already encountered "test" block.
1934        Notes
1935            The caller must check whether or not statements were found and issue an error when appropriate.
1936            For example, interface members and abstract members cannot have any statements.
1937            Other members must have at least one.
1938        """
1939        _pushCodePart(codePart)
1940        if codePartContainingTest is nil
1941            codePartContainingTest = codePart to BoxMember  # TODO: figure out better typing for this assignment and the method sig of this method
1942        try
1943            while .peek.which == 'EOL', .grab
1944            if .peek.which.isOneOf('BODY.TEST.REQUIRE.ENSURE.OR.AND.')
1945                # sectional
1946                # not flexible. sequence is signature, contract, test, implementation
1947                _isContractOnSameLine = false
1948                if .peek.which.isOneOf('REQUIRE.OR.')
1949                    .requireSection(codePart)
1950                if .peek.which.isOneOf('ENSURE.AND.')
1951                    .ensureSection(codePart)
1952                while .peek.which == 'TEST'
1953                    .testSection(codePartContainingTest to !)
1954                if .peek.which=='BODY'
1955                    .grab
1956                    .indent
1957                    _statementsFor(codePart)
1958                else
1959                    if _isContractOnSameLine
1960                        _statementsFor(codePart)
1961                    else
1962                        pass
1963                        # There is no body for abstract or interface members. Error checking is done elsewhere.
1964                        # .throwError('Expecting `body` section.')
1965                if not _isContractOnSameLine
1966                    .dedent
1967            else
1968                # non-sectional
1969                _statementsFor(codePart)
1970        finally
1971            _popCodePart
1972            # warn on horrendously long methods, enabling this and cutoff should be user configurable
1973            if false and codePart.statements.count > 0  # disabled until option controlled
1974                startLine = codePart.statements[0].token.lineNum
1975                numLines = codePart.statements[codePart.statements.count-1].token.lineNum - startLine + 1
1976                fullName = '[codePart.parentBox.name].[codePart.name]'
1977                if numLines > 100
1978                    # TODO: is the toplevel statement really useful?
1979                    _warning('More than 100 lines of code ([codePart.statements.count] toplevel statements) in method [fullName].')
1980                #print '[fullName.padLeft(50)]\t[nLines]\t[codePart.statements.count]'
1981
1982    def _statementsFor(codePart as HasAddStmt)
1983        """
1984        Utility method for .statementsFor.
1985        """
1986        while .peek.which <> 'DEDENT'
1987            stmt = .stmt
1988            if stmt, codePart.addStmt(stmt)
1989            ame = if(_expectAnonymousMethodExprStack.count, _expectAnonymousMethodExprStack.pop, nil)
1990            if ame
1991                .zeroOrMore('EOL')
1992                .expect('INDENT')
1993                _statementsFor(ame)
1994        .dedent
1995
1996    def stmt as Stmt?
1997        token = .peek
1998        s as Stmt? # the statement (node)
1999        expectEOL = true
2000        branch token.which
2001            on 'AT_ID'
2002                if token.text <> '@help', .throwError(token, 'Unexpected compiler directive.')
2003                .grab
2004                s = .stmt
2005                s.isHelpRequested = true
2006                return s
2007            on 'ASSERT'
2008                s = .assertStmt
2009            on 'BRANCH'
2010                s = .branchStmt
2011                expectEOL = false
2012            on 'BREAK'
2013                if _curLoopBlockDepth == 0
2014                    .recordError(token, 'Cannot use "break" outside of a loop.')
2015                s = .breakStmt
2016            on 'CONTINUE'
2017                if _curLoopBlockDepth == 0
2018                    .recordError(token, 'Cannot use "continue" outside of a loop.')
2019                s = .continueStmt
2020            on 'EXPECT'
2021                s = .expectStmt
2022                expectEOL = false
2023            on 'FOR'
2024                _didStartLoop = true
2025                s = .forStmtBeginning
2026                expectEOL = false
2027#           on 'DEF'
2028#               s = .declareMethod  # nested method
2029            on 'IF'
2030                s = .ifStmt
2031                expectEOL = false
2032            on 'GET'
2033                .throwError('Cannot use "get" for a statement. If you mistakenly started a property above with "def", "get" or "set", then use "pro" instead.')
2034            on 'LISTEN'
2035                s = .listenStmt
2036            on 'INDENT'
2037                ame = if(_expectAnonymousMethodExprStack.count, _expectAnonymousMethodExprStack.pop, nil)
2038                if ame
2039                    .expect('INDENT')
2040                    _statementsFor(ame)
2041                    expectEOL = false
2042                else
2043                    .throwError(ErrorMessages.expectingStatementInsteadOfIndentation)
2044            on 'IGNORE'
2045                s = .ignoreStmt
2046            on 'LOCK'
2047                s = .lockStmt
2048                expectEOL = false
2049            on 'PASS'
2050                s = .passStmt
2051            on 'POST'
2052                _didStartLoop = true
2053                s = .postWhileStmt
2054                expectEOL = false
2055            on 'PRINT'
2056                s = .printStmt
2057                expectEOL = false
2058            on 'RAISE'
2059                s = .raiseStmt
2060            on 'RETURN'
2061                s = .returnStmt
2062            on 'THROW'
2063                s = .throwStmt
2064            on 'TRACE'
2065                s = .traceStmt
2066                expectEOL = false
2067            on 'TRY'
2068                s = .tryStmt
2069                expectEOL = false
2070            on 'USING'
2071                s = .usingStmt
2072                expectEOL = false
2073            on 'WHILE'
2074                _didStartLoop = true
2075                s = .whileStmt
2076                expectEOL = false
2077            on 'YIELD'
2078                s = .yieldStmt
2079            on 'EOL'
2080                .grab  # ignore stray EOL (can especially come up at the end of a file)
2081                expectEOL = false
2082            else
2083                # Can't do this (or at least not this simply) because it's legit to say:
2084                # SomeClass<of Blah>()
2085                #if token.which=='OPEN_GENERIC'
2086                #   .throwError('The correct local variable syntax is "name as Type" or "name = initValue".')
2087                if token.which == 'ID' and token.text == 'ct_trace'  # TODO: need something other than ct_trace
2088                    s = CompileTimeTraceStmt(.grab, .expression)
2089                else
2090                    if .looksLikeType(0) and .looksLikeVarNameIsNext(1)
2091                        .throwError('The correct local variable syntax is "name as Type" or "name = initValue". Try "[.peek(1).text] as [.peek(0).text]."')
2092                    s = .expression
2093                    s.afterParserRecognizesStatement
2094                    if .optional('COMMA')
2095                        s = .multiTargetAssign(s to Expr)
2096        if expectEOL, _handleStmtEOL(token, s)
2097        _finishSpaceAgnostic
2098        return s
2099       
2100    def _handleStmtEOL(token as IToken?, s as Stmt?)   
2101        if .verbosity>=5
2102            print '<> last statement start token=[token]'
2103            print '<> s = [s]'
2104        try
2105            .expect('EOL')
2106        catch pe as ParserException
2107            # example: puts 5
2108            token = .last(1)
2109            if token <> nil
2110                sugg = if(token.text.length, Compiler.suggestionFor(token.text), nil)
2111                sugg = if(sugg, ' Try "[sugg]" instead of "[token.text]".', nil)
2112                if sugg, pe = pe.cloneWithMessage(pe.message + sugg)
2113            throw pe
2114
2115
2116    ##
2117    ## Individual Statements
2118    ##
2119
2120    def assertStmt as Stmt
2121        token = .expect('ASSERT')
2122        expr = .expression
2123        info = if(.optional('COMMA'), .expression, nil)
2124        return AssertStmt(token, expr, info)
2125
2126    def branchStmt as Stmt
2127        token = .expect('BRANCH')
2128        e = .expression
2129        .indent
2130        onParts = List<of BranchOnPart>()
2131        elsePart as BlockStmt?
2132        shouldContinue = true  # CC: axe this when 'break' can be used in a branch inside a loop
2133        while shouldContinue
2134            branch .peek.which
2135                on 'ON'
2136                    .grab
2137                    if elsePart
2138                        .throwError('Cannot have "on" parts after an "else" part.')
2139                    exprs = List<of Expr>()
2140                    expr = .expression
2141                    while expr inherits BinaryBoolExpr
2142                        binExpr = expr to BinaryBoolExpr
2143                        if binExpr.op == 'OR'
2144                            right = binExpr.right
2145                            if right inherits TruthExpr
2146                                right = right.expr
2147                            exprs.add(right)
2148                            expr = binExpr.left
2149                        else
2150                            .throwError('Unexpected "[expr.token.text]" in "on" value.')
2151                    if expr inherits TruthExpr
2152                        expr = expr.expr
2153                    exprs.add(expr)
2154                    if CobraCore.willCheckAssert
2155                        for expr in exprs
2156                            assert not expr inherits TruthExpr
2157                            assert not expr inherits BinaryBoolExpr
2158                    block = .block
2159                    onParts.add(BranchOnPart(exprs, block))
2160                on 'ELSE'
2161                    .grab
2162                    if elsePart
2163                        .throwError('Cannot have more than one "else" in a branch.')
2164                    if not onParts.count
2165                        .throwError('Cannot have an "else" in a branch without at least one "on".')
2166                    elsePart = .branchPartStatements
2167                on 'DEDENT'
2168                    shouldContinue = false
2169                on 'EOL'
2170                    .grab
2171                else
2172                    .throwError('Expecting "on", "else" or end of branch statement. Encountered [.peek.which]')
2173        .dedent
2174        return BranchStmt(token, e, onParts, elsePart)
2175
2176    def branchPartStatements as BlockStmt
2177        if .peek.which=='COLON'
2178            .grab
2179            stmt = .stmt
2180            if stmt is nil
2181                .throwError('Need a statement.')
2182            block = BlockStmt(stmt.token, [stmt])
2183        else
2184            block = .block
2185        return block
2186
2187    def breakStmt as Stmt
2188        return BreakStmt(.expect('BREAK'))
2189
2190    def continueStmt as Stmt
2191        return ContinueStmt(.expect('CONTINUE'))
2192
2193    def expectStmt as Stmt
2194        # expect FooException
2195        #     block
2196        token = .expect('EXPECT')
2197        type = .typeId
2198        block = .block
2199        return ExpectStmt(token, type, block)
2200
2201    def forStmtBeginning as Stmt
2202        """
2203        numeric    for int x = 0 up to n step 2
2204        enumerable for cust as Customer in customers
2205                   for k, v in dict
2206                   for i, j, k in listOfThrees
2207        """
2208        token = .expect('FOR')
2209        varr = .nameExpr
2210        if .optional('COMMA')  # forStmt multiarg for v1,... in
2211            args = .commaSepExprsPartial('IN.EOL.', 'IN')
2212            if .last.which <> 'IN'
2213                .throwError('Comma separated nameId list in forStatement needs to terminate with an IN token ("in")')
2214            if args.count == 0  # "for v1, in ..."  single multiarg variable
2215                # TODO: should probably be a syntax error
2216                .ungrab  # 'in' token
2217                return .forStmt(token, varr)
2218            else
2219                multiArgs = [varr]
2220                for arg in args
2221                    if arg inherits NameExpr, multiArgs.add(arg)
2222                    else, .recordError('Expression "[arg.toCobraSource]" of comma-separated list is not an identifier or identifier-as-type.')
2223                what = .expression
2224                name = 'forEnumVar[what.serialNum]'  # will get made more unique by For Stmt
2225                nameToken = token.copy('ID', name)
2226                varr = IdentifierExpr(nameToken, name)
2227                return ForEnumerableStmt(token, varr, multiArgs, what, .block)
2228
2229        # forStmt single variable assign - for v {in,=} ...
2230        peek = .peek.which
2231        if peek=='ASSIGN'
2232            return .oldNumericForStmt(token, varr)
2233        else if peek=='IN'
2234            return .forStmt(token, varr)
2235        else
2236            .throwError('Expecting "=" or "in".')
2237            throw FallThroughException(peek# make C# code flow analysis happy
2238
2239    def oldNumericForStmt(token as IToken, varr as NameExpr) as ForStmt
2240        """
2241        Syntax:
2242            for x = 0 .. t.count ++ 2
2243        deprecated 2008-03: the use of .. and ++ just doesn't relate to anything
2244        See forEnumerableStmt below.
2245        """
2246        .expect('ASSIGN')
2247        start = .expression
2248        .expect('DOTDOT')
2249        stopp = .expression
2250        dirToken = .optional('PLUSPLUS')
2251        if dirToken
2252            dir = 1
2253        else
2254            dirToken = .optional('MINUSMINUS')
2255            if dirToken
2256                dir = -1
2257        stepp as Expr?
2258        if dirToken is nil
2259            dir = 1
2260            stepp = nil
2261        else
2262            stepp = .expression
2263        stmts = .block
2264        return OldForNumericStmt(token, varr, start, stopp, dir, stepp, stmts)
2265
2266    def forStmt(token as IToken, varr as NameExpr) as ForStmt
2267        """
2268        Syntax:
2269            for x in stuff
2270                statements
2271            for x in i : j
2272                statements
2273            for x in i : j : k
2274                statements
2275        """
2276        .expect('IN')
2277        what = .expression
2278        if .optional('COLON')
2279            # for x in start : stop
2280            # for x in start : stop : step
2281            b = .expression
2282            if .optional('COLON')
2283                c = .expression
2284                return ForNumericStmt(token, varr, what, b, 0, c, .block)
2285            else
2286                return ForNumericStmt(token, varr, what, b, 0, nil, .block)
2287        else
2288            # for x in stuff
2289            return ForEnumerableStmt(token, varr, what, .block)
2290
2291    def ifStmt as Stmt
2292        token = .expect('IF')
2293        cond = .expression
2294        trueStmts = .block
2295        falseStmts as BlockStmt?
2296        if .peek.which=='ELSE'
2297            .grab
2298            peek = .peek.which
2299            if peek.isOneOf('EOL.COMMA.COLON.')
2300                falseStmts = .block
2301            else if peek=='IF'
2302                falseStmts = BlockStmt(.peek, [.ifStmt])
2303            else
2304                .throwError('Syntax error. Expecting end-of-line or "if" after an "else".')
2305        return IfStmt(token, cond, trueStmts, falseStmts)
2306
2307    def ignoreStmt as IgnoreStmt
2308        token = .expect('IGNORE')
2309        eventRef = .expression
2310        # TODO: error checking
2311        .expect('COMMA')
2312        target = .expression
2313        # TODO: error checking
2314        return IgnoreStmt(token, eventRef, target)
2315
2316    def listenStmt as ListenStmt
2317        token = .expect('LISTEN')
2318        eventRef = .expression
2319        # TODO: error checking
2320        .expect('COMMA')
2321        target = .expression
2322        # TODO: error checking
2323        return ListenStmt(token, eventRef, target)
2324
2325    def lockStmt as Stmt
2326        # syntax: lock e, block
2327        token = .expect('LOCK')
2328        expr = .expression
2329        block = .block
2330        return LockStmt(token, expr, block)
2331
2332    def passStmt as Stmt
2333        return PassStmt(.grab)
2334
2335    def postWhileStmt as Stmt
2336        token = .expect('POST')
2337        .expect('WHILE')
2338        return PostWhileStmt(token, .expression, .block)
2339
2340    def traceStmt as TraceStmt?
2341        """
2342        Example source:
2343            trace
2344            trace x
2345            trace this, x, foo.bar
2346            trace all
2347            trace on
2348            trace off
2349        """
2350        token = .expect('TRACE')
2351        peek = .peek.which
2352        branch peek
2353            on 'ON'
2354                .expect('ON', 'EOL')
2355                _isTraceOn = true
2356                return nil
2357            on 'OFF'
2358                .expect('OFF', 'EOL')
2359                _isTraceOn = false
2360                return nil
2361            on 'ALL'
2362                .expect('ALL', 'EOL')
2363                return if(_isTraceOn, TraceAllStmt(token, _curCodePart), nil)
2364            on 'EOL'
2365                .expect('EOL')
2366                return if(_isTraceOn, TraceLocationStmt(token, _curCodePart), nil)
2367            else
2368                if _isTraceOn
2369                    return TraceExprsStmt(token, _curCodePart, .commaSepExprs('EOL.'))
2370                else
2371                    .commaSepExprs('EOL.')
2372                    return nil
2373        throw FallThroughException()  # 'branch..else should have returned'
2374
2375    def printStmt as Stmt
2376        """
2377        Example source:
2378            print arg
2379            print a, b, c
2380            print to sw, a, b
2381            print to sw, a, b stop
2382            print a, b, c stop
2383            print to sw
2384                body
2385        """
2386        destination as Expr?
2387        block as BlockStmt?
2388        token = .expect('PRINT')
2389        args = List<of Expr>()
2390        stopp = false
2391        if .optional('TO')
2392            destination = .expression
2393            peek = .peek.which
2394            if peek=='COMMA'
2395                .grab
2396            else if peek=='EOL'
2397                block = .block
2398            else
2399                .throwError('Expecting a comma and print arguments, or a code block.')
2400        if not block
2401            args = .commaSepExprs('EOL.STOP.')
2402            terminator = .last
2403            if terminator.which=='STOP'
2404                stopp = true
2405                .expect('EOL')
2406        if block
2407            return PrintRedirectStmt(token, destination, block)
2408        else
2409            return PrintStmt(token, destination, args, stopp)
2410
2411    def raiseStmt as Stmt
2412        token = .expect('RAISE')
2413        exprs = .commaSepExprs('EOL.')
2414        assert .last.which=='EOL'
2415        .ungrab  # need EOL
2416        if exprs.count == 0
2417            .throwError('Expecting one or more expressions after "raise", starting with the event. If you meant to throw the currently caught exception, use "throw" instead.')
2418        return RaiseStmt(token, exprs)
2419
2420    def returnStmt as Stmt
2421        token = .expect('RETURN')
2422        expr = if(.peek.which == 'EOL', nil, .expression)
2423        return ReturnStmt(token, expr)
2424
2425    def requireSection(codeMember as AbstractMethod) as ContractPart
2426        return _requireOrEnsure(codeMember, 'OR', 'REQUIRE', RequirePart)
2427
2428    def ensureSection(codeMember as AbstractMethod) as ContractPart
2429        return _requireOrEnsure(codeMember, 'AND', 'ENSURE', EnsurePart)
2430
2431    def _requireOrEnsure(codeMember as AbstractMethod, connectWhich as String, mainWhich as String, theClass as Type) as ContractPart
2432        connectToken = .optional(connectWhich)
2433        mainToken = .expect(mainWhich)
2434        if .peek.which.isOneOf('EOL.COLON.')
2435            .indent
2436            exprs = List<of Expr>()
2437            while true
2438                if .peek.which=='EOL'
2439                    .grab
2440                    continue
2441                if exprs.count and .peek.which == 'DEDENT'
2442                    break
2443                exprs.add(.expression)
2444                .expect('EOL')
2445            .dedent
2446        else
2447            # one expression, on the same line
2448            exprs = [.expression]
2449            .endOfLine
2450            _isContractOnSameLine = true
2451        return theClass(connectToken, mainToken, codeMember, exprs) to ContractPart
2452
2453    def throwStmt as Stmt
2454        token = .expect('THROW')
2455        expr = if(.peek.which == 'EOL', nil, .expression)
2456        return ThrowStmt(token, expr)
2457
2458    def tryStmt as Stmt
2459        # try... except... success... finally...
2460        token = .expect('TRY')
2461        tryBlock = .block
2462        catchBlocks = List<of CatchBlock>()
2463        didParseCatchAnyBlock = false  # meaning the catch that specifies no specific type of exception
2464        useCatchMsg = 'Use "catch" instead of "except". (Also, use "throw" for throwing exceptions and "raise" for raising events.)'
2465        if .peek.which=='EXCEPT'
2466            .throwError(useCatchMsg)
2467        while .peek.which=='CATCH'
2468            catchToken = .grab
2469            if .peek.which.isOneOf('COLON.EOL.')
2470                if didParseCatchAnyBlock
2471                    .throwError('Already encountered the "catch every exception" block.')
2472                anyCatchBlock = .block
2473                catchBlocks.add(CatchBlock(catchToken, anyCatchBlock))
2474                didParseCatchAnyBlock = true
2475            else
2476                if didParseCatchAnyBlock
2477                    .throwError('Cannot have a specific exception block after the "catch every exception" block.')
2478                if .peek(1).which=='AS'
2479                    catchVar = .localVarDecl
2480                    catchBlock = .block
2481                    catchBlocks.add(CatchBlock(catchBlock.token, catchVar, catchBlock))
2482                else
2483                    catchType = .typeId
2484                    catchBlock = .block
2485                    catchBlocks.add(CatchBlock(catchBlock.token, catchType, catchBlock))
2486        if .peek.which=='EXCEPT'
2487            .throwError(useCatchMsg)
2488        if .peek.which=='ELSE'
2489            .throwError('There is no "else" for a "try". There is a "success" however.')
2490        if .peek.which=='SUCCESS'
2491            .grab
2492            successBlock = .block to ?
2493        else
2494            successBlock = nil
2495        if .peek.which=='FINALLY'
2496            .grab
2497            finallyBlock = .block to ?
2498        else
2499            finallyBlock = nil
2500        if not catchBlocks.count and not successBlock and not finallyBlock
2501            .throwError('A try needs at least one "catch", "success" or "finally" block.')
2502        return TryStmt(token, tryBlock, catchBlocks, successBlock, finallyBlock)
2503
2504    def testSection(codeMember as BoxMember) as TestMethod
2505        """
2506        Parses the `test` section and sets codeMember.testMethod.
2507        Returns the test method.
2508        """
2509        # TODO: consider pushing the test method as the current code member
2510        token = .expect('TEST')
2511        .indent
2512        testMethod = TestMethod(token, codeMember)
2513        .statementsFor(testMethod)
2514        codeMember.testMethods.add(testMethod)
2515        return testMethod
2516
2517    def testSection(box as Box) as TestMethod
2518        # TODO: consider pushing the test method as the current code member
2519        token = .expect('TEST')
2520        .indent
2521        testMethod = TestMethod(token, box)
2522        .statementsFor(testMethod)
2523        box.testMethods.add(testMethod)
2524        return testMethod
2525
2526    def usingStmt as Stmt
2527        # syntax: using x = e  block
2528        token = .expect('USING')
2529        varr = .nameExpr
2530        .expect('ASSIGN')
2531        initExpr = .expression
2532        block = .block
2533        return UsingStmt(token, varr, initExpr, block)
2534
2535    def whileStmt as Stmt
2536        return WhileStmt(.expect('WHILE'), .expression, .block)
2537
2538    def yieldStmt as Stmt
2539        token = .expect('YIELD')
2540        peek = .peek.which
2541        if peek == 'BREAK'
2542            .expect('BREAK')
2543            return YieldBreakStmt(token)
2544        else
2545            if peek == 'RETURN'
2546                .throwError('Use "yield" instead of "yield return".') 
2547            expr = if(peek == 'EOL', nil, .expression)
2548            return YieldReturnStmt(token, expr)
2549
2550
2551    ##
2552    ## Misc parts
2553    ##
2554
2555    def block as BlockStmt
2556        """
2557        Used by if, while, print-to, etc.
2558        Consumes the (optional colon,) indent, statements and dedent.
2559        Returns a BlockStmt.
2560        """
2561        stmts = List<of Stmt>()
2562        done = false
2563        if _didStartLoop
2564            _didStartLoop = false
2565            _curLoopBlockDepth += 1
2566        else if _curLoopBlockDepth > 0
2567            _curLoopBlockDepth += 1
2568        if .optional('COMMA')
2569            token = .last
2570            stmt = .stmt
2571            if stmt
2572                stmts.add(stmt)
2573                if _curLoopBlockDepth > 0, _curLoopBlockDepth -= 1
2574            else
2575                .throwError('Missing statement after comma.')
2576            done = true
2577        else if .optional('COLON')
2578            token = .last
2579            if not .optional('EOL')
2580                _warning('Colons are not used to put a target statement on the same line. Use a comma (,) instead.')
2581                stmt = .stmt
2582                if stmt
2583                    stmts.add(stmt)
2584                    if _curLoopBlockDepth > 0, _curLoopBlockDepth -= 1
2585                else
2586                    .throwError('Missing statement after colon.')
2587                done = true
2588        if not done
2589            token = .indent
2590            while true
2591                stmt = .stmt
2592                if stmt, stmts.add(stmt)
2593                if .peek.which=='DEDENT'
2594                    if _curLoopBlockDepth > 0, _curLoopBlockDepth -= 1
2595                    break
2596            if not stmts.count
2597                .throwError('Missing statements in block. Add a real statement or a "pass".')
2598            .dedent
2599        return BlockStmt(token, stmts)
2600
2601    def localVarDecl as AbstractLocalVar
2602        return .localVarDecl(.typeProvider.unspecifiedType)
2603
2604    def localVarDecl(defaultType as IType?) as AbstractLocalVar
2605        """
2606        Variable declarations for `using`, `for` and `catch`.
2607        Not class vars (see `boxVarDecl`) or parameters (see `paramDecl`).
2608        Example source:
2609            x   # default type is dynamic   # TODO: should be unspecified
2610            i as int
2611            cust as Customer
2612        Arguments:
2613            theClass is typically BoxVarDecl, LocalVar or Param
2614            whatName could be set to 'Parameter' for example.
2615        Returns:
2616            A theClass(name, type)
2617        Errors:
2618            None
2619        """
2620        token = .expect('ID')
2621        name = token.value to String
2622        .checkStartsLowercase(name, 'Variable')
2623        type as ITypeProxy?
2624        if .peek.which=='AS'
2625            .grab
2626            type = .typeId
2627        else
2628            # maybe the var already exists?
2629            definition = _curCodePart.findLocal(name)
2630            if definition
2631                return definition
2632            type = nil
2633
2634        type = type ? defaultType
2635        assert type
2636
2637        definition = _curCodePart.findLocal(name)
2638
2639        # TODO: put this kind of check in bindImp maybe?
2640        if definition
2641            if definition.typeNode
2642                if definition.typeNode==type
2643                    return definition
2644                else
2645                    # this should probably be moved to the bindImp phase since types can have different names like "int" and "System.Int32"
2646                    .throwError('Cannot redeclare "[name]" from "[definition.typeNode]" to "[type]". Previous definition is on line [definition.token.lineNum].')
2647            else if definition.type==type
2648                return definition
2649            else
2650                # this should probably be moved to the bindImp phase since types can have different names like "int" and "System.Int32"
2651                .throwError('Cannot redeclare "[name]" from "[definition.type]" to "[type]". Previous definition is on line [definition.token.lineNum].')
2652
2653        # new def
2654        varr = LocalVar(token, type)
2655        _curCodePart.addLocal(varr)
2656
2657        return varr
2658
2659    ##
2660    ## Expressions
2661    ##
2662
2663    shared
2664       
2665        var _binaryOpPrec = {
2666            # CANNOT USE 0 AS A VALUE IN THIS DICTIONARY
2667            'DOT':              80,
2668            'LBRACKET':         80,
2669            'LPAREN':           80,
2670            'ARRAY_OPEN':       80,
2671
2672            'STARSTAR':         70,  # right associative
2673
2674            'QUESTION':         68,
2675            'BANG':             68,
2676
2677            'TO':               65,
2678            'TOQ':              65,
2679
2680            'STAR':             60,
2681            'SLASH':            60,
2682            'SLASHSLASH':       60,
2683            'PERCENT':          60,
2684
2685            'PLUS':             50,
2686            'MINUS':            50,
2687
2688            # bitwise shift
2689            'DOUBLE_LT':        47,
2690            'DOUBLE_GT':        47,
2691
2692            # bitwise and or xor
2693            'AMPERSAND':        45,
2694            'VERTICAL_BAR':     45,
2695            'CARET':            45,
2696
2697            # comparison
2698            'EQ':               40,
2699            'NE':               40,
2700            'LT':               40,
2701            'GT':               40,
2702            'LE':               40,
2703            'GE':               40,
2704            'IS':               40,
2705            'ISNOT':            40,
2706            'INHERITS':         40,
2707            'IMPLEMENTS':       40,
2708
2709            'IN':               35,
2710            'NOTIN':            35,
2711
2712            'AND':              30,
2713            'OR':               30,
2714
2715            'IMPLIES':          20,
2716
2717            'ASSIGN':           20,
2718            'PLUS_EQUALS':      20,
2719            'MINUS_EQUALS':     20,
2720            'STAR_EQUALS':      20,
2721            'STARSTAR_EQUALS':  20,
2722            'SLASH_EQUALS':     20,
2723            'PERCENT_EQUALS':   20,
2724            'QUESTION_EQUALS':  20,
2725            'BANG_EQUALS':      20,
2726
2727            'AMPERSAND_EQUALS':     20,
2728            'VERTICAL_BAR_EQUALS':  20,
2729            'CARET_EQUALS':         20,
2730            'DOUBLE_LT_EQUALS':     20,
2731            'DOUBLE_GT_EQUALS':     20,
2732        }
2733
2734        get binaryOpPrec from var
2735       
2736        var _unaryOpPrec = {
2737            'MINUS': _binaryOpPrec['MINUS']+1,
2738            'PLUS':  _binaryOpPrec['PLUS']+1,
2739            'TILDE': _binaryOpPrec['PLUS']+1,
2740            'NOT':   _binaryOpPrec['AND']+1,
2741            'ALL':   _binaryOpPrec['AND']+1,  # have to admit I'm just guessing at the precendence level for 'any' and 'all'. experience will tell. TODO: fix or not, then retire this comment (2008-07-31)
2742            'ANY':   _binaryOpPrec['AND']+1,
2743            'REF':   _binaryOpPrec['STARSTAR']+1,
2744            'OLD':   _binaryOpPrec['STARSTAR']+1,
2745            'AT_ID': _binaryOpPrec['STARSTAR']+1,
2746        }
2747
2748    var _inExpression as int
2749   
2750    def expression as Expr
2751        test
2752            assert 0 not in _binaryOpPrec.values
2753        body
2754            _inExpression += 1
2755            try
2756                expr = .expression(0, nil)
2757                if expr.isParened and expr.token.lineNum == .last.lineNum
2758                    _warning(expr.token, 'Unnecessary parentheses around expression. You can remove them.')
2759                return expr
2760            finally
2761                _inExpression -= 1
2762
2763    def expression(precedence as int) as Expr
2764        return .expression(precedence, nil)
2765
2766    def expression(precedence as int, left as Expr?) as Expr
2767        if left is nil
2768            left = .expression2
2769        while true
2770            peek = .peek.which
2771            # handle multi-word operators
2772            op as String? = nil
2773            if peek=='IS' and .peek(+1).which=='NOT'
2774                # 'is not' is a 2 keyword operator
2775                op = 'ISNOT'
2776            else if peek=='NOT' and .peek(+1).which=='IN'
2777                op = 'NOTIN'
2778            # handle precedence (and detect non-binary operators)
2779            binaryOpPrec = _binaryOpPrec.get(op ? peek, -1)
2780            if binaryOpPrec==-1 or binaryOpPrec<precedence
2781                break
2782            # continue...
2783            if peek=='LBRACKET'
2784                # requires special handling - IndexExpr or SliceExpr
2785                return .expression(precedence, .indexOrSliceExpr(left to !))
2786            else if peek=='LPAREN'
2787                # requires special handling - PostCallExpr
2788                # this happens for something like: foo[i]('x')
2789                if not PostCallExpr.isTargetAcceptable(left to !)
2790                    .throwError('Unexpected call.')  # example: t = x to List<of String>()
2791                token = .grab
2792                exprs = .commaSepExprs('RPAREN.')
2793                return .expression(precedence, PostCallExpr(token, left, exprs))
2794            else
2795                # most operators are one-word affairs
2796                if op is nil
2797                    opToken = .grab
2798                    op = opToken.which
2799                else
2800                    # op was set earlier for a two word operator. ISNOT NOTIN
2801                    opToken = .grab
2802                    .grab
2803                if op=='TO' or op=='TOQ'
2804                    getTypeExprForRightHandSide = true  # required to handle "x to String?", for example
2805            assert _binaryOpPrec.containsKey(op to !)
2806            _leftStack.push(left to !)
2807            .opStack.push(op to !)
2808            try
2809                # get the right hand side of a binary operator expression
2810                prec = if(OperatorSpecs.rightAssoc.containsKey(op to !), binaryOpPrec, binaryOpPrec+1)
2811                if op == 'TO' and .peek.which.isOneOf('QUESTION.BANG.')
2812                    # ex: x to !
2813                    # ex: x to ?
2814                    getTypeExprForRightHandSide = false
2815                    rightTok = .grab to !
2816                    branch rightTok.which
2817                        on 'QUESTION', left = ToNilableExpr(opToken to !, rightTok, left to !)
2818                        on 'BANG', left = ToNonNilableExpr(opToken to !, rightTok, left to !)
2819                        else, throw FallThroughException(rightTok.which)
2820                else
2821                    if getTypeExprForRightHandSide
2822                        # support to expression. Ex: x to int  Ex: x to? Shape
2823                        right = .typeExpr to Expr
2824                        getTypeExprForRightHandSide = false
2825                    else if op == 'DOT' and .peek and .peek.isKeyword
2826                        # support foo.bar where bar is a keyword. Ex: foo.this
2827                        right = MemberExpr(.grab) to Expr
2828                    else if op == 'DOT' and _isHelpDirective(.peek)
2829                        # support foo.@help
2830                        left.isHelpRequested = true
2831                        op = ''  # cancel the creation of a new binary op expr
2832                        .grab
2833                    else
2834                        right = .expression(prec)
2835                    if op == 'DOT' and not right inherits IDotRightExpr
2836                        .throwError(ErrorMessages.syntaxErrorAfterDot)
2837                    if op.length, left = BinaryOpExpr.make(opToken to !, op to !, left to !, right)
2838            finally
2839                .opStack.pop
2840                _leftStack.pop
2841        assert left
2842        return left to !
2843
2844    def expression2 as Expr
2845        if _spaceAgnosticExprLevel > 0, _spaceAgnostic
2846        peekToken = .peek
2847        peek = peekToken.which
2848        if _unaryOpPrec.containsKey(peek)
2849            token = .grab
2850            prec = _unaryOpPrec[peek]
2851            unaryExpr = .expression(prec)
2852            branch token.which
2853                on 'ALL', return AllExpr(token, unaryExpr)
2854                on 'ANY', return AnyExpr(token, unaryExpr)
2855                on 'OLD', return OldExpr(token, unaryExpr)
2856                on 'REF', return RefExpr(token, unaryExpr)
2857                on 'AT_ID'
2858                    if token.text == '@help'
2859                        unaryExpr.isHelpRequested = true
2860                        return unaryExpr
2861                    else
2862                        .recordError(token, 'Unexpected compiler directive.')
2863                else, return UnaryOpExpr(token, peek, unaryExpr)
2864        branch peek
2865            on 'LPAREN'
2866                .grab
2867                _spaceAgnosticExprLevel += 1
2868                node = .expression(0, nil)
2869                .expect('RPAREN')
2870                _spaceAgnosticExprLevel -= 1
2871                node.isParened = true
2872                return node
2873            on 'DOT'
2874                # leading dot
2875                token = .grab
2876                .opStack.push('DOT')
2877                try
2878                    peekToken = .peek
2879                    peek = peekToken.which
2880                    if peek=='ID' or peekToken.isKeyword
2881                        memberToken = .idOrKeyword
2882                        expr = MemberExpr(memberToken) to Expr
2883                    else if peek=='OPEN_CALL'
2884                        expr = .callExpr
2885                    else if peek=='OPEN_GENERIC'
2886                        expr = .callExpr
2887                    else if _isHelpDirective(peekToken)
2888                        .grab
2889                        return ThisLit(token, isImplicit=true, isHelpRequested=true)
2890                    else
2891                        .throwError(ErrorMessages.syntaxErrorAfterDot)
2892                finally
2893                    .opStack.pop
2894                return BinaryOpExpr.make(token to !, 'DOT', ThisLit(token, isImplicit=true), expr)
2895            on 'NIL'
2896                return NilLiteral(.grab)
2897            on 'TRUE'
2898                return BoolLit(.grab)
2899            on 'FALSE'
2900                return BoolLit(.grab)
2901            on 'THIS'
2902                return ThisLit(.grab)
2903            on 'BASE'
2904                return BaseLit(.grab)
2905            on 'VAR'
2906                assert _curCodePart
2907                if _curCodePart inherits ProperDexerXetter
2908                    return VarLit(.grab, _curCodePart)
2909                else
2910                    .throwError('Cannot refer to `var` in expressions outside of a property `get` or `set`.')
2911                    throw FallThroughException() # stop a warning
2912            on 'CHAR_LIT_SINGLE'
2913                return CharLit(.grab)
2914            on 'CHAR_LIT_DOUBLE'
2915                return CharLit(.grab)
2916            on 'STRING_START_SINGLE'
2917                return .stringWithSubstitutionLit('STRING_START_SINGLE', 'STRING_PART_SINGLE', 'STRING_STOP_SINGLE')
2918            on 'STRING_START_DOUBLE'
2919                return .stringWithSubstitutionLit('STRING_START_DOUBLE', 'STRING_PART_DOUBLE', 'STRING_STOP_DOUBLE')
2920            on 'STRING_SINGLE'
2921                return StringLit(.grab)
2922            on 'STRING_DOUBLE'
2923                return StringLit(.grab)
2924            on 'INTEGER_LIT'
2925                return IntegerLit(.grab)
2926            on 'DECIMAL_LIT'
2927                return DecimalLit(.grab)
2928            on 'FRACTIONAL_LIT'
2929                return FractionalLit(.grab)
2930            on 'FLOAT_LIT'
2931                return FloatLit(.grab)
2932            on 'LBRACKET'
2933                return .literalList
2934            on 'ARRAY_OPEN'
2935                return .literalArray
2936            on 'LCURLY'
2937                return .literalDictOrSet
2938            on 'OPEN_DO' or 'DO'
2939                return .doExpr
2940            on 'OPEN_IF'
2941                return .ifExpr
2942            on 'FOR'
2943                return .forExpr
2944            on 'OPEN_CALL'
2945                return .callExpr
2946            on 'OPEN_GENERIC'
2947                if .opStack.count and .opStack.peek == 'DOT'
2948                    return .callExpr
2949                else
2950                    return TypeExpr(.typeId)
2951            on 'ID'
2952                return .identifierExpr
2953            on 'SHARP_OPEN' or 'SHARP_SINGLE' or 'SHARP_DOUBLE'
2954                return .sharpExpr
2955            else
2956                if .opStack.count and .opStack.peek=='DOT' and .peek.isKeyword
2957                    return .identifierExpr
2958                else
2959                    try
2960                        return TypeExpr(.nonqualifiedTypeId)
2961                    catch pe as ParserException
2962                        if .allowKeywordAssignment and peekToken.isKeyword and .peek is not nil and .peek.which == 'ASSIGN'
2963                            # example: f = Foo(where=3)  # note that 'where' is a keyword
2964                            assert .last is peekToken
2965                            word, op = .last, .grab
2966                            if word.text.startsWithNonLowerLetter
2967                                # shouldn't happen in practice because keywords are always lowercase
2968                                .recordError(ErrorMessages.localVariablesMustStartLowercase)
2969                            return AssignExpr(op, 'ASSIGN', IdentifierExpr(word), .expression)
2970                        else if pe.message.contains('Unrecognized type')
2971                            msg = 'Expecting an expression.'
2972                            if peekToken.isKeyword
2973                                msg += ' "[peekToken.text]" is a reserved keyword that is not expected here.'
2974                            .throwError(msg)
2975                            throw FallThroughException()
2976                        else
2977                            throw
2978
2979    def multiTargetAssign(arg0 as Expr) as Stmt
2980        # id, |[id,]... = <expr>
2981        args = .commaSepExprsPartial('ASSIGN.EOL.', 'ASSIGN')
2982        args.insert(0, arg0)
2983        if .last.which <> 'ASSIGN'
2984            .throwError('Comma-separated assignment targets must end with "=", or this is a general syntax error.')
2985        assignTok = .last to !
2986        rhs as Expr? = .expression
2987        if .optional('COMMA')
2988            rhsList = .commaSepExprs('EOL.')
2989            assert .last.which=='EOL'
2990            .ungrab  # need EOL
2991            rhsList.insert(0, rhs)
2992            rhs = nil
2993        return MultiTargetAssignStatement(assignTok, args, rhs, rhsList)
2994       
2995    def callExpr as Expr
2996        """
2997        Syntax:
2998            foo(args)
2999            foo<of T>(args)
3000        """
3001        token = .expect('OPEN_CALL', 'OPEN_GENERIC')
3002        callName = token.value to String
3003        branch token.which
3004            on 'OPEN_CALL'
3005                assert not callName.endsWith('(')
3006                args = .commaSepExprs('RPAREN.', true, true)
3007                if .opStack.count and .opStack.peek == 'DOT'
3008                    return CallExpr(token, callName, args, true)
3009                else
3010                    return PostCallExpr(token, IdentifierExpr(token, callName), args)
3011            on 'OPEN_GENERIC'
3012                assert not callName.endsWith('<of') and not callName.endsWith('<')
3013                typeArgs = List<of ITypeProxy>()
3014                isDone = false
3015                post while not isDone
3016                    typeArgs.add(.typeId)
3017                    branch .grab.which
3018                        on 'COMMA', pass
3019                        on 'GT', isDone = true
3020                        else, .throwError('Unexpected token [.last] in type arguments.')
3021                if .optional('LPAREN')
3022                    args = .commaSepExprs('RPAREN.', true, true)
3023                    parens = true
3024                else
3025                    args = List<of Expr>()
3026                    parens = false
3027                return CallExpr(token, callName, typeArgs, args, parens)
3028            else
3029                throw FallThroughException(token)               
3030
3031    get allowKeywordAssignment from var as bool
3032   
3033    def argument as Expr
3034        """
3035        In support of .callExpr and others, for when it's legal to write `out x` and such.
3036        """
3037        if .peek
3038            branch .peek.which
3039                on 'OUT', label = Direction.Out
3040                on 'INOUT', label = Direction.InOut
3041                else, label = Direction.In
3042                # TODO? on 'VARI': ...
3043            if label <> Direction.In, .grab
3044        _allowKeywordAssignment = true
3045        try
3046            expr = .expression
3047        finally
3048            _allowKeywordAssignment = false
3049        expr.direction = label
3050        return expr
3051
3052    def commaSepExprsPartial(terminators as String, binOpBreakWhich as String) as List<of Expr>
3053        require
3054            binOpBreakWhich.isOneOf('ASSIGN.IN.')
3055            .binaryOpPrec.containsKey(binOpBreakWhich)
3056        body
3057            # As commaSepExprs but setup to break out of middle of binOpExpression on terminating token
3058            realPrec = _binaryOpPrec[binOpBreakWhich]
3059            _binaryOpPrec[binOpBreakWhich] = -1  # reset precedence to exit expression parser when hit this op
3060            try
3061                exprs = .commaSepExprs(terminators)
3062            finally
3063                _binaryOpPrec[binOpBreakWhich] = realPrec
3064            return exprs
3065           
3066    def commaSepExprs(terminators as String) as List<of Expr>
3067        require terminators.endsWith('.')
3068        return .commaSepExprs(terminators, false, false)
3069
3070    def commaSepExprs(terminators as String, isSpaceAgnostic as bool) as List<of Expr>
3071        require terminators.endsWith('.')
3072        return .commaSepExprs(terminators, isSpaceAgnostic, false)
3073
3074    def commaSepExprs(terminators as String, isSpaceAgnostic as bool, expectingArguments as bool) as List<of Expr>
3075        """
3076        Example source
3077            ... expr TERMINATOR
3078            ... expr, expr TERMINATOR
3079            ... expr, expr, expr, TERMINATOR
3080        Returns
3081            A list of expressions.
3082        Notes
3083            Popular terminators are 'EOL' and 'RPAREN'.
3084            The terminator token is consumed, but can be examined with .last.
3085        """
3086        require terminators.endsWith('.')
3087        expectSep = false
3088        sep = 'COMMA'
3089        exprs = List<of Expr>()
3090        while true
3091            if isSpaceAgnostic
3092                _spaceAgnostic
3093            if .peek is nil
3094                literal = {'RPAREN': ')', 'RBRACKET': ']'}
3095                what = (for t in terminators.split(c'.') where t.trim<>'' get '"[if(literal.containsKey(t), literal[t], t)]"').join(' or ')
3096                .throwError('Expecting "," or [what].')
3097            if .peek.which.isOneOf(terminators)
3098                .grab
3099                break
3100            if expectSep
3101                .expect(sep)
3102            if .peek.which.isOneOf(terminators)
3103                .grab
3104                break
3105            if isSpaceAgnostic
3106                _spaceAgnostic
3107            if .peek.which.isOneOf(terminators)
3108                .grab
3109                break
3110            .newOpStack
3111            try
3112                if expectingArguments
3113                    exprs.add(.argument)
3114                else
3115                    exprs.add(.expression)
3116            finally
3117                .delOpStack
3118            expectSep = true
3119        return exprs
3120   
3121    def doExpr as AnonymousExpr
3122        """
3123        Example:
3124            ... do(int a, int b) ...
3125                return a + b
3126        Format:
3127            ... do(<params>) ...
3128                <statements>
3129        Notes:
3130            The indented statements are not picked up here. Instead an AnonymousMethodExpr is pushed
3131            on _expectAnonymousMethodExprStack which then triggers the consumption of the indented
3132            statements in another part of the parser.
3133        """
3134        require .peek.which.isOneOf('OPEN_DO.DO.')
3135        token = .grab
3136        params = if(token.which == 'OPEN_DO', .paramDecls(true), List<of Param>())
3137        if token.which == 'OPEN_DO' and params.count == 0
3138            _warning(token, 'Unnecessary parentheses. You can remove them.')
3139        if .optional('ASSIGN')
3140            expr = .expression
3141            return LambdaExpr(token, params, nil, expr)
3142        else
3143            returnTypeId = if(.optional('AS'), .typeId, nil)
3144            ame = AnonymousMethodExpr(token, params, returnTypeId)
3145            _expectAnonymousMethodExprStack.push(ame)
3146            return ame
3147
3148    def forExpr as ForExpr
3149        """
3150        t = for x in stuff where x<0 get x*x
3151        grammar: for VAR in EXPR [where EXPR] get EXPR
3152        """
3153        token = .expect('FOR')
3154        nameExpr = .nameExpr
3155        # TODO: support numeric for expressions?
3156        #peek = .peek.which
3157        #if peek=='ASSIGN'
3158        #   return .forNumericStmt(token, varr)
3159        #else if peek=='IN'
3160        #   return .forEnumerableStmt(token, varr)
3161        #else
3162        #   .throwError('Expecting "=" or "in".')
3163        #   throw FallThroughException(peek)  # make C# code flow analysis happy
3164        .expect('IN')
3165        what = .expression
3166        if .optional('COLON')
3167            # for x in start : stop ...
3168            # for x in start : stop : step ....
3169            stopExpr = .expression
3170            if .optional('COLON')
3171                stepExpr = .expression
3172        if .optional('WHERE')
3173            whereExpr as Expr? = .expression
3174            if .optional('GET')
3175                getExpr = .expression
3176            else
3177                getExpr = IdentifierExpr(nameExpr.token, nameExpr.name)
3178        else
3179            .expect('GET')
3180            getExpr = .expression
3181        return ForExpr(token, nameExpr, what, stopExpr, stepExpr, whereExpr, getExpr)
3182
3183    def identifierExpr as Expr
3184        """
3185        Can return an IdentifierExpr or an AsExpr if the user says "i as int", for example.
3186        """
3187        nameToken = .idOrKeyword
3188        name = nameToken.text
3189        if .opStack.count and .opStack.peek=='DOT'
3190            return MemberExpr(nameToken)
3191        if .peek.which=='AS'
3192            return AsExpr(.grab, nameToken, .typeId)
3193        else
3194            return IdentifierExpr(nameToken, name)
3195
3196    def ifExpr as IfExpr
3197        token = .expect('OPEN_IF')
3198        expr = .expression
3199        .expect('COMMA')
3200        texpr = .expression
3201        .expect('COMMA')
3202        fexpr = .expression
3203        .expect('RPAREN')
3204        return IfExpr(token, expr, texpr, fexpr)
3205
3206    def indexOrSliceExpr(left as Expr) as Expr
3207        # note: this code is similar to, but not identical to commaSepExprs
3208        # this code has to deal with the case that in slices, expressions can be omitted
3209        token = .grab
3210        assert token.which=='LBRACKET'
3211        expectSep = false
3212        sep as String?
3213        exprs = List<of Expr?>()
3214        separators = ['COMMA', 'COLON']
3215        isSpaceAgnostic = false # TODO: try making true for isSpaceAgnostic
3216        while true
3217            if isSpaceAgnostic
3218                _spaceAgnostic
3219            if .peek.which=='RBRACKET'
3220                .grab
3221                break
3222            if expectSep
3223                if sep
3224                    .expect(sep)
3225                else
3226                    for which in separators
3227                        if .peek.which==which
3228                            .grab
3229                            sep = which
3230                            break
3231                    if sep is nil
3232                        .throwError('Expecting one of: [separators.join(", ")], but encountered [.peek.which]')
3233                if sep=='COLON'
3234                    lastThingWasColon = true
3235            if .peek.which=='RBRACKET'
3236                .grab
3237                break
3238            if .peek.which=='COLON'
3239                if sep=='COMMA'
3240                    .throwError('Not expecting a colon.')
3241                .grab
3242                exprs.add(nil)
3243                sep = 'COLON'  # because sep could be nil
3244                lastThingWasColon = true
3245                continue
3246            if isSpaceAgnostic
3247                _spaceAgnostic
3248            if .peek.which=='RBRACKET'
3249                .grab
3250                break
3251            .newOpStack
3252            try
3253                exprs.add(.expression)
3254                lastThingWasColon = false
3255            finally
3256                .delOpStack
3257            expectSep = true
3258        if lastThingWasColon
3259            exprs.add(nil)
3260        if sep=='COLON'
3261            assert exprs.count>=2
3262            if exprs.count>3
3263                .throwError('There are [exprs.count] expressions for the slice. There can only be up to three (start, stop and step).')
3264            start = exprs[0]
3265            stopp = exprs[1]
3266            stepp = if(exprs.count==3, exprs[2], nil)
3267            return SliceExpr(token, left, start, stopp, stepp)
3268        else
3269            for expr in exprs
3270                assert expr, exprs
3271            return IndexExpr(token, left, exprs)
3272
3273    def literalList as ListLit
3274        token = .expect('LBRACKET')
3275        exprs = .commaSepExprs('RBRACKET.', true)
3276        return ListLit(token, exprs)
3277
3278    def literalArray as ArrayLit
3279        token = .expect('ARRAY_OPEN')
3280        exprs = .commaSepExprs('RBRACKET.', true)
3281        return ArrayLit(token, exprs)
3282
3283    def literalDictOrSet as CompositeLiteral
3284        token = .expect('LCURLY')
3285        _spaceAgnostic
3286        expr as Expr?
3287        branch .peek.which
3288            on 'COMMA'
3289                return _literalSet(token, expr)
3290            on 'COLON'
3291                return _literalDict(token, expr)
3292            on 'RCURLY'
3293                .grab
3294                _warning('Assuming empty dictionary, but please use "{:}" for empty dictionary or "{,}" for empty set')
3295                return DictLit(token, List<of List<of Expr>>())
3296        expr = .expression
3297        _spaceAgnostic
3298        branch .peek.which
3299            on 'COMMA'
3300                return _literalSet(token, expr)
3301            on 'COLON'
3302                return _literalDict(token, expr)
3303            on 'RCURLY'
3304                .grab
3305                return SetLit(token, [expr])  # example: {1}
3306            else
3307                .throwError('Expecting a comma, colon or right curly brace for set literal or dictionary literal.')
3308                throw Exception(''# for code flow analysis
3309
3310    def _literalSet(token as IToken, expr as Expr?) as SetLit
3311        if expr
3312            exprs = [expr]
3313            while true
3314                .expect('COMMA')
3315                _spaceAgnostic
3316                if .optional('RCURLY')
3317                    break
3318                .newOpStack
3319                try
3320                    exprs.add(.expression)
3321                finally
3322                    .delOpStack
3323                _spaceAgnostic
3324                if .optional('RCURLY')
3325                    break
3326            return SetLit(token, exprs)
3327        else
3328            .expect('COMMA')
3329            _spaceAgnostic
3330            .expect('RCURLY')
3331            return SetLit(token, List<of Expr>())
3332
3333    def _literalDict(token as IToken, expr as Expr?) as DictLit
3334        if expr
3335            expectComma = false
3336            entries = List<of List<of Expr>>()
3337            first = true
3338            while true
3339                if first
3340                    key = expr
3341                    first = false
3342                else
3343                    _spaceAgnostic
3344                    if .peek.which=='RCURLY'
3345                        .grab
3346                        break
3347                    if expectComma
3348                        .expect('COMMA')
3349                    if .peek.which=='RCURLY'
3350                        .grab
3351                        break
3352                    _spaceAgnostic
3353                    if .peek.which=='RCURLY'
3354                        .grab
3355                        break
3356                    key = .expression
3357                .expect('COLON')
3358                value = .expression
3359                entries.add([key, value])
3360                expectComma = true
3361            return DictLit(token, entries)
3362        else
3363            .expect('COLON')
3364            _spaceAgnostic
3365            .expect('RCURLY')
3366            return DictLit(token, List<of List<of Expr>>())
3367       
3368    def nameExpr as NameExpr
3369        nameToken = .expect('ID')
3370        name = nameToken.text
3371        if .peek.which=='AS'
3372            return AsExpr(.grab, nameToken, .typeId)
3373        else
3374            return IdentifierExpr(nameToken, name)
3375
3376    def sharpExpr as SharpExpr
3377        token = .grab
3378        branch token.which
3379            on 'SHARP_SINGLE'
3380                assert token.text.startsWith("sharp'")
3381                return SharpExpr(token, token.text["sharp'".length:-1])
3382            on 'SHARP_DOUBLE'
3383                assert token.text.startsWith('sharp"')
3384                return SharpExpr(token, token.text['sharp"'.length:-1])
3385            on 'SHARP_OPEN' # deprecated
3386                expr = .expression
3387                .expect('RPAREN')
3388                sharpStr = "sharp'...'"
3389                _warning('The $sharp() form has been deprecated. Please use a sharp string literal instead (sharp"..." or [sharpStr])')
3390                return SharpExpr(token, expr)
3391            else
3392                throw FallThroughException(token)
3393
3394    def stringWithSubstitutionLit(whichStart as String, whichPart as String, whichStop as String) as StringSubstLit
3395        # comment this mo-fo
3396        items = List<of Expr>()
3397        item = .expect(whichStart)
3398        items.add(StringLit(item))
3399        while true
3400            expr = .expression
3401            fmt = .optional('STRING_PART_FORMAT')
3402            if fmt
3403                assert fmt.text.startsWith('')
3404                items.add(FormattedExpr(expr, fmt.text.substring(1)))
3405            else
3406                items.add(expr)
3407            peek = .peek.which
3408            if peek==whichPart
3409                items.add(StringLit(.grab))
3410            else if peek==whichStop
3411                items.add(StringLit(.grab))
3412                break
3413            else
3414                if _verbosity>=4
3415                    print '<> stringWithSubstitutionLit([whichStart], [whichPart], [whichStop]), peek=[peek]'
3416                .throwError('Expecting more string contents or the end of string after the bracketed expression.')
3417        return StringSubstLit(items)
3418
3419    def typeExpr as TypeExpr
3420        return TypeExpr(.typeId)
3421
3422    ##
3423    ## Types
3424    ##
3425
3426    def typeId as AbstractTypeIdentifier
3427        return .qualifiedTypeId
3428
3429    def qualifiedTypeId as AbstractTypeIdentifier
3430        """ May actually return a non-qualified type. """
3431        types = List<of AbstractTypeIdentifier>()
3432        while true
3433            t = .nonqualifiedTypeId
3434            types.add(t)
3435            if .peek.which == 'DOT'
3436                if .peek(1).which == 'OPEN_CALL'
3437                    # ex: Test<of int>.check(5)
3438                    # See Tests/240-generics/300-declare-generic-classes/102-generic-class-shared.cobra
3439                    break
3440                else
3441                    .grab
3442            else
3443                break
3444        assert types.count
3445        if types.count==1
3446            return types[0]
3447        else
3448            # if the last type is an array we need to fix things up--the array applies to the whole qualified type
3449            lastTypeId = types.last
3450            if lastTypeId inherits ArrayTypeIdentifier
3451                types[types.count-1] = lastTypeId.theWrappedTypeIdentifier
3452                innerType = QualifiedTypeIdentifier(types)
3453                return ArrayTypeIdentifier(lastTypeId.token, innerType)
3454            else
3455                return QualifiedTypeIdentifier(types)
3456
3457    var _validIntSizes = [8, 16, 32, 64]
3458    var _validFloatSizes = [32, 64]
3459
3460    def nonqualifiedTypeId as AbstractTypeIdentifier
3461        token = .grab
3462        assert token
3463
3464        t as AbstractTypeIdentifier?
3465
3466        branch token.text
3467            on 'int', t = TypeIdentifier(token, .typeProvider.intType)
3468            on 'uint', t = TypeIdentifier(token, .typeProvider.uintType)
3469            on 'bool', t = TypeIdentifier(token, .typeProvider.boolType)
3470            on 'char', t = TypeIdentifier(token, .typeProvider.charType)
3471            on 'decimal', t = TypeIdentifier(token, .typeProvider.decimalType)
3472            on 'float', t = TypeIdentifier(token, .typeProvider.floatType)
3473            on 'number', t = TypeIdentifier(token, .typeProvider.numberType)
3474            on 'passthrough', t = TypeIdentifier(token, .typeProvider.passThroughType)
3475            on 'dynamic', t = TypeIdentifier(token, .typeProvider.dynamicType)
3476            else 
3477                branch token.which
3478                    on 'INT_SIZE'
3479                        size = token.value to int
3480                        if size not in _validIntSizes
3481                            .throwError('Unsupported integer size: [size]. Try int8, int16, int32 or int64. Or, for non-types, use a name different than the form "intNN" which is reserved for integer types.')
3482                        t = TypeIdentifier(token, .typeProvider.intType(true, size))
3483                    on 'UINT_SIZE'
3484                        size = token.value to int
3485                        if size not in _validIntSizes
3486                            .throwError('Unsupported integer size: [size]. Try uint8, uint16, uint32 or uint64. Or, for non-types, use a name different than the form "uintNN" which is reserved for unsigned integer types.')
3487                        t = TypeIdentifier(token, .typeProvider.intType(false, size))
3488                    on 'FLOAT_SIZE'
3489                        size = token.value to int
3490                        if size not in _validFloatSizes
3491                            .throwError('Unsupported float size: [size]. Try 32 or 64.')
3492                        t = TypeIdentifier(token, .typeProvider.floatType(size))
3493                    on 'ID'
3494                        t = TypeIdentifier(token)
3495                        if not _inExpression and .peek and .peek.which == 'LT'
3496                            .throwError('Unexpected "<" after type name. If you are naming a generic, use "of " right after "<" as in "[token.text]<of ...".')
3497                    on 'OPEN_GENERIC', t = .genericTypeId(token to !)
3498                    else
3499                        .throwError('Unrecognized type: [token]')
3500                        return nil to passthrough  # CC: remove
3501
3502        assert t
3503
3504        # TODO: the array and nilable check should probably be at the bottom of qualifiedType
3505
3506        # check for array
3507        bracket = .optional('LBRACKET')
3508        if bracket
3509            if .peek.which=='INTEGER_LIT'
3510                .throwError('The size of the array is not part of its type. Specify the size when creating the array such as: [t.name]\[]([.peek.text]).')
3511            .expect('RBRACKET')
3512            t = ArrayTypeIdentifier(bracket, t)
3513
3514        while true
3515            # check for 'optional' aka 'can be nil'
3516            if .optional('QUESTION')
3517                t = NilableTypeIdentifier(.last, t)
3518   
3519            # check for 'stream' aka 'multiple' aka 'zero or more' aka 'enumerable'
3520            else if .optional('STAR')
3521                t = StreamTypeIdentifier(.last, t)
3522
3523            else
3524                break
3525
3526        return t to !
3527
3528    def genericTypeId(openGenericToken as IToken) as AbstractTypeIdentifier
3529        require openGenericToken.text.trim.endsWith('<of')
3530        fullName = openGenericToken.text.trim + ' '
3531        rootName = fullName[:-4]
3532        numArgs = 1
3533        types = List<of ITypeProxy>()
3534        while true
3535            if .peek.which == 'GT'
3536                .grab
3537                break
3538            if .peek.which  == 'DOUBLE_GT'
3539                # example source code: Dictionary<of String, List<of String>>
3540                .replace(.peek.copy('GT'))  # tricky, but effective. note that modifying the token directly can cause problems when running testify on multiple files (such as a whole directory)--which is the norm
3541                break
3542            if .peek.which == 'COMMA'
3543                .grab
3544                fullName += ', '
3545                numArgs += 1
3546            else
3547                t = .typeId
3548                types.add(t)
3549                fullName += t.name
3550        fullName += '>'
3551       
3552        if types.count and types.count <> numArgs
3553            # TODO: could detect this when it happens in the loop above
3554            .throwError(openGenericToken, 'Invalid generic type due to extra commas.')
3555        if types.count == 0
3556            # ex:   List<of>   Dictionary<of,>
3557            fullName = fullName.replace(' ', '')
3558            return GenericTypeIdentifier(openGenericToken, rootName, fullName, numArgs)
3559        else
3560            # ex:   List<of int>   Dictionary<of int, bool>
3561            return GenericTypeIdentifier(openGenericToken, rootName, fullName, types)
3562
3563
3564    ##
3565    ## Op stack
3566    ##
3567
3568    def newOpStack
3569        require _opStackStack
3570        _opStackStack.push(Stack<of String>())
3571
3572    def delOpStack
3573        require _opStackStack
3574        _opStackStack.pop
3575
3576    get opStack as Stack<of String>
3577        """
3578        Returns the current opStack.
3579        """
3580        return _opStackStack.peek
3581
3582
3583    ##
3584    ## Protected self utility
3585    ##
3586
3587    def checkProperty(name as String)
3588        box = .curBox
3589        if name==box.name
3590            .throwError('Property names cannot be the same as their enclosing type.')  # TODO: list the enclosing types location
3591        other = box.declForName(name)
3592        if other
3593            .throwError('There is already another class member with the name "[name]".')  # TODO: list its location and possibly what it is
3594        other = box.declForNameCI(name)
3595        if other
3596            .throwError('There is already another class member with the name "[other.name]". You must differentiate member names by more than just case.')
3597        if name.startsWithNonLowerLetter
3598            .throwError('Property names must start with lowercase letters. ([name])')
3599
3600    def checkStartsLowercase(identifier as String, whatName as String)
3601        """
3602        Makes an error if identifier does not match 'foo'.
3603        whatName should be capitalized.
3604        """
3605        if identifier[0] == '_'
3606            sugg = identifier[1:]
3607            while sugg.startsWith('_')
3608                sugg = sugg[1:]
3609            if sugg.length == 0
3610                sugg = ''
3611            else if sugg.length == 1
3612                sugg = ' Try "[sugg.toLower]".'
3613            else
3614                sugg = sugg[0].toString.toLower + sugg[1:]
3615                sugg = ' Try "[sugg]".'
3616            .recordError('[whatName] declarations cannot start with an underscore. Those are reserved for class variables.[sugg]')
3617        if identifier.startsWithNonLowerLetter
3618            sugg = identifier[0].toString.toLower + identifier[1:]
3619            .recordError('[whatName] declarations must start with a lowercase letter to distinguish them from other types of identifiers. Try "[sugg]".')
3620
3621    def looksLikeType(peekAhead as int) as bool
3622        """
3623        Returns true if the token looks like a type because
3624            * it's an uppercase identifier, or
3625            * it's a primitive type (bool, char, etc.)
3626
3627        The users of this method have to handle OPEN_GENERIC in a separate way which is why this
3628        method does not check for that.
3629
3630        Also, this method cannot check for dotted names since it only works with one token.
3631
3632        This method supports the feature where C# style syntax for params and locals is detected
3633        (`int x = 5` instead of `x as int = 5` or `x = 5`) in order to give a more useful error
3634        message to the programmer.
3635        """
3636        token = .peek(peekAhead)
3637        if token.which == 'ID'
3638            return (token.value to String).startsWithNonLowerLetter
3639        return .isOneOfKeywords(token, ['bool', 'char', 'decimal', 'int', 'uint', 'float', 'number'])
3640
3641    def looksLikeVarNameIsNext(peekAhead as int) as bool
3642        token = .peek(peekAhead)
3643        return token is not nil and token.which=='ID' and token.text.startsWithLowerLetter
3644       
3645    def isOneOfKeywords(nToken as IToken?, keywords as List<of String>) as bool
3646        token = nToken to !
3647        return token.isKeyword and token.text in keywords
3648
3649    def _isHelpDirective(token as IToken?) as bool
3650        return token and token.which == 'AT_ID' and token.text == '@help'
3651
3652    def _overloadIfNeeded(token as IToken, box as Box, name as String) as MemberOverload?
3653        """
3654        Creates an overload for a new member going into box--if needed.
3655        May throw various appropriate errors.
3656        """
3657        overload as MemberOverload?
3658        other = box.declForName(name)
3659        if other
3660            if other inherits MemberOverload
3661                overload = other
3662            else if other implements IOverloadable
3663                overload = MemberOverload(other)
3664                box.registerOverload(overload to !)
3665            else
3666                .throwError(token, 'There is already another class member with the name "[name]".')  # TODO list its location and possibly what it is
3667        else
3668            other = box.declForName(name)  # TODO: should be a CI there for case-insensitive
3669            if other
3670                .throwError(token, 'There is already another class member with the name "[other.name]". You must differentiate member names by more than just case.')
3671            if name[0] in _uppercaseLetters
3672                .recordError(token, 'Method names must start with lowercase letters. ([name])')
3673
3674        return overload
3675
3676    def _pushCodePart(codePart as AbstractMethod)
3677        _codeParts.push(codePart)
3678        _curCodePart = codePart
3679
3680    def _popCodePart
3681        require _codeParts.count
3682        _codeParts.pop
3683        _curCodePart = if(_codeParts.count, _codeParts.peek, nil)
3684
3685    def _spaceAgnostic
3686        """
3687        Eats up EOLs, INDENTs and DEDENTs.
3688        Call this to go into "space agnostic" mode.
3689        Call _finishSpaceAgnostic afterwards to eat up subsequent INDENTs and DEDENTs.
3690        """
3691        while true
3692            peek = .peek
3693            if peek
3694                branch .peek.which
3695                    on 'EOL'
3696                        .grab
3697                        continue
3698                    on 'INDENT'
3699                        .grab
3700                        _spaceAgnosticIndentLevel += 1
3701                        continue
3702                    on 'DEDENT'
3703                        .grab
3704                        _spaceAgnosticIndentLevel -= 1
3705                        continue
3706            break
3707        if _verbosity >= 5
3708            print '<> spaceAgnostic level=[_spaceAgnosticIndentLevel]'
3709
3710    def _finishSpaceAgnostic
3711        """
3712        Eats up the DEDENTs and INDENTs that balance out the ones encountered in spaceAgnostic.
3713        """
3714        if _verbosity >= 5
3715            print '<> finishSpaceAgnostic level=[_spaceAgnosticIndentLevel]'
3716        if _spaceAgnosticIndentLevel
3717            while _spaceAgnosticIndentLevel > 0
3718                .dedent
3719                _spaceAgnosticIndentLevel -= 1
3720            while _spaceAgnosticIndentLevel < 0
3721                .expect('INDENT')
3722                _spaceAgnosticIndentLevel += 1
3723        assert _spaceAgnosticIndentLevel==0  # cobra: make this an ensure
3724
3725
3726class TypeSpecs
3727    """
3728    This is a results container for the parser.
3729    """
3730   
3731    cue init(isNames as IList<of String>, attributes as AttributeList, inheritsProxies as List<of ITypeProxy>, implementsProxies as List<of ITypeProxy>, addsProxies as List<of ITypeProxy>)
3732        base.init
3733        _isNames = isNames
3734        _attributes = attributes
3735        _inheritsProxies = inheritsProxies
3736        _implementsProxies = implementsProxies
3737        _addsProxies = addsProxies
3738   
3739    get isNames from var as IList<of String>
3740   
3741    get attributes from var as AttributeList
3742   
3743    get inheritsProxies from var as List<of ITypeProxy>
3744   
3745    get implementsProxies from var as List<of ITypeProxy>
3746
3747    get addsProxies from var as List<of ITypeProxy>
Note: See TracBrowser for help on using the browser.