| 1 | """ |
|---|
| 2 | The 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 | |
|---|
| 9 | Rules: |
|---|
| 10 | * Do not invoke Box.memberForName. Use Box.declForName instead. Inheritance relationships are not established during parsing. |
|---|
| 11 | """ |
|---|
| 12 | |
|---|
| 13 | |
|---|
| 14 | class 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 | |
|---|
| 31 | extend 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 | |
|---|
| 47 | class 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 | |
|---|
| 3726 | class 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> |
|---|