Appearance
| 1 | namespace Syntax.Process is | |
| 2 | use Source.LOCATION | |
| 3 | use Trees | |
| 4 | ||
| 5 | use Logging.Logger | |
| 6 | ||
| 7 | use Semantic.Symbols.Function | |
| 8 | ||
| 9 | // Picks the assembly's entry point out of every candidate the project | |
| 10 | // declares, before generate-il runs. | |
| 11 | // | |
| 12 | // Three kinds of declaration can name one, and they rank against each | |
| 13 | // other rather than competing first-come: | |
| 14 | // | |
| 15 | // 1. an `@entry` (or `@IL.entrypoint`) pragma | |
| 16 | // 2. a function named by `--entry`, or `entry` when that is not given | |
| 17 | // 3. the entry synthesised from a file's top-level statements | |
| 18 | // | |
| 19 | // The highest rank any candidate reaches wins, and two candidates at | |
| 20 | // that rank are the duplicate-entrypoint error - except at rank 3, | |
| 21 | // where several files each carrying top-level statements is a project | |
| 22 | // shape rather than a mistake: none of them is selected, each is told | |
| 23 | // its statements will not run, and an executable build that ends up | |
| 24 | // with no entry point at all reports that separately. | |
| 25 | // | |
| 26 | // Ranking matters most for rank 3. A synthesised entry is a file-private | |
| 27 | // definition like any other, so a project may hold any number of them - | |
| 28 | // an umbrella project globbing a directory of one-file examples, say - | |
| 29 | // and naming the real entry point with `--entry` is what tells them | |
| 30 | // apart. `--entry` also takes rank 3 out of the running entirely: a | |
| 31 | // build that asks for an entry point by name does not want statements | |
| 32 | // from some other file instead. | |
| 33 | // A declaration of the entry-point name whose shape cannot be | |
| 34 | // entered at, and which part of it says so. | |
| 35 | class REJECTED_ENTRY_NAME(location: LOCATION, name: string, fault: string) | |
| 36 | ||
| 37 | class SELECT_ENTRY_POINT: Visitor is | |
| 38 | UNRUN_STATEMENTS: string static => "top-level-statements-not-run" | |
| 39 | ||
| 40 | _PRAGMA_RANK: int static => 0 | |
| 41 | _NAMED_RANK: int static => 1 | |
| 42 | _TOP_LEVEL_RANK: int static => 2 | |
| 43 | ||
| 44 | _logger: Logger | |
| 45 | _symbol_table: Semantic.SYMBOL_TABLE | |
| 46 | _ir_context: IR.CONTEXT | |
| 47 | _build_flags: Compiler.GLOBAL_BUILD_FLAGS | |
| 48 | ||
| 49 | _candidates: Collections.LIST[ENTRY_POINT_CANDIDATE] | |
| 50 | _pending_entry_pragma: bool | |
| 51 | ||
| 52 | // The function declaration the next synthesised `_entry` wrapper | |
| 53 | // stands in for. The wrapper is appended after the original in its | |
| 54 | // definition list, so the original is always walked first. | |
| 55 | _wrapped_original: Definitions.FUNCTION? | |
| 56 | ||
| 57 | // A declaration of the entry-point name that cannot be entered | |
| 58 | // at, held until the choice is made: whether it is a fault at | |
| 59 | // all depends on what else the build found. | |
| 60 | _rejected_named: REJECTED_ENTRY_NAME? | |
| 61 | ||
| 62 | // Whether a function the entry-point name selects was rejected | |
| 63 | // for its shape. The program has said where it starts, so the | |
| 64 | // statements of some other file are not taken in its place - | |
| 65 | // starting somewhere the author did not name is as surprising as | |
| 66 | // not starting at all, and the error at the function is the | |
| 67 | // whole of what to say about it. | |
| 68 | _entry_name_claimed: bool | |
| 69 | ||
| 70 | // Whether the project carries top-level statements that no build | |
| 71 | // will run. Read by the compiler so an executable build with no | |
| 72 | // entry point can say which of the two shapes it is in. | |
| 73 | left_top_level_statements_unrun: bool public | |
| 74 | ||
| 75 | // Whether `--entry` named a function that no candidate matches - | |
| 76 | // distinct from several top-level files leaving the choice open, | |
| 77 | // since here the caller did name one and nothing answered to it. | |
| 78 | // Read by the compiler for the same reason as the field above. | |
| 79 | named_entry_point_not_found: bool public | |
| 80 | ||
| 81 | init( | |
| 82 | logger: Logger, | |
| 83 | symbol_table: Semantic.SYMBOL_TABLE, | |
| 84 | ir_context: IR.CONTEXT, | |
| 85 | build_flags: Compiler.GLOBAL_BUILD_FLAGS | |
| 86 | ) is | |
| 87 | super.init() | |
| 88 | ||
| 89 | _logger = logger | |
| 90 | _symbol_table = symbol_table | |
| 91 | _ir_context = ir_context | |
| 92 | _build_flags = build_flags | |
| 93 | _candidates = Collections.LIST[ENTRY_POINT_CANDIDATE]() | |
| 94 | si | |
| 95 | ||
| 96 | start() is | |
| 97 | _candidates.clear() | |
| 98 | _pending_entry_pragma = false | |
| 99 | _entry_name_claimed = false | |
| 100 | _rejected_named = null | |
| 101 | left_top_level_statements_unrun = false | |
| 102 | named_entry_point_not_found = false | |
| 103 | si | |
| 104 | ||
| 105 | apply(source_file: Compiler.SOURCE_FILE) is | |
| 106 | _pending_entry_pragma = false | |
| 107 | _wrapped_original = null | |
| 108 | ||
| 109 | source_file.definition.walk(self) | |
| 110 | si | |
| 111 | ||
| 112 | pre(pragma: Definitions.PRAGMA) -> bool is | |
| 113 | let name = pragma.pragma.name.to_string() | |
| 114 | ||
| 115 | if name =~ "entry" \/ name =~ "IL.entrypoint" then | |
| 116 | _pending_entry_pragma = true | |
| 117 | fi | |
| 118 | ||
| 119 | return false | |
| 120 | si | |
| 121 | ||
| 122 | pre(function: Definitions.FUNCTION) -> bool is | |
| 123 | let is_pragma_marked = _pending_entry_pragma | |
| 124 | ||
| 125 | _pending_entry_pragma = false | |
| 126 | ||
| 127 | // Stands in for by a synthesised `_entry` wrapper: this | |
| 128 | // function's own signature is never Main-compatible, so it | |
| 129 | // is never itself a candidate. Remembered, so the wrapper's | |
| 130 | // candidate can describe this declaration - the user wrote | |
| 131 | // this one, and no diagnostic should name the wrapper. | |
| 132 | if function.wrapped_by_synthesized_main then | |
| 133 | _wrapped_original = function | |
| 134 | return true | |
| 135 | fi | |
| 136 | ||
| 137 | let name = function.name | |
| 138 | let is_named_entry = | |
| 139 | (name? /\ name.name =~ _ir_context.entry_point_name) \/ | |
| 140 | function.is_synthesized_main_wrapper | |
| 141 | ||
| 142 | // Nothing inside a body can be a candidate - a lambda is never | |
| 143 | // one, and a function cannot nest - so the walk stops at every | |
| 144 | // function header rather than descending through every | |
| 145 | // expression in the project. The three cheap tests come first | |
| 146 | // so the vast majority of headers cost a string compare and | |
| 147 | // never reach the symbol table. | |
| 148 | if !function.is_top_level_entry /\ !is_pragma_marked /\ !is_named_entry then | |
| 149 | return true | |
| 150 | fi | |
| 151 | ||
| 152 | let symbol = _function_for(function) | |
| 153 | ||
| 154 | // Only the two kinds the runtime can enter at: a global | |
| 155 | // function or a static method. An instance method named | |
| 156 | // `entry` is an ordinary member. | |
| 157 | if !symbol? \/ !(isa Semantic.Symbols.GLOBAL_FUNCTION(symbol) \/ isa Semantic.Symbols.STATIC_METHOD(symbol)) then | |
| 158 | return true | |
| 159 | fi | |
| 160 | ||
| 161 | let declared_at = if name? then name.location else function.location fi | |
| 162 | ||
| 163 | if function.is_top_level_entry then | |
| 164 | _candidates.add( | |
| 165 | ENTRY_POINT_CANDIDATE( | |
| 166 | _TOP_LEVEL_RANK, | |
| 167 | symbol, | |
| 168 | _first_statement_location(function), | |
| 169 | true, | |
| 170 | symbol.name)) | |
| 171 | elif function.is_synthesized_main_wrapper then | |
| 172 | // Emission enters the wrapper, but no diagnostic should | |
| 173 | // name it: the candidate borrows the wrapped declaration's | |
| 174 | // name and a location the user can jump to. A wrapper for a | |
| 175 | // top-level entry carries the top-level rank - several files | |
| 176 | // each holding statements stay the tolerated tie they are | |
| 177 | // without wrappers, and `--entry` still takes them all out | |
| 178 | // of the running. | |
| 179 | let original = _wrapped_original | |
| 180 | ||
| 181 | let wrapped_name = | |
| 182 | if original? /\ original.name? then | |
| 183 | original.name.name | |
| 184 | else | |
| 185 | symbol.name | |
| 186 | fi | |
| 187 | ||
| 188 | let wrapped_location = | |
| 189 | if original? /\ original.is_top_level_entry then | |
| 190 | _first_statement_location(original) | |
| 191 | elif original? /\ original.name? then | |
| 192 | original.name.location | |
| 193 | else | |
| 194 | declared_at | |
| 195 | fi | |
| 196 | ||
| 197 | let rank = if function.wraps_top_level_entry then _TOP_LEVEL_RANK else _NAMED_RANK fi | |
| 198 | ||
| 199 | _candidates.add( | |
| 200 | ENTRY_POINT_CANDIDATE( | |
| 201 | rank, | |
| 202 | symbol, | |
| 203 | wrapped_location, | |
| 204 | function.wraps_top_level_entry, | |
| 205 | wrapped_name)) | |
| 206 | elif is_pragma_marked then | |
| 207 | _candidates.add(ENTRY_POINT_CANDIDATE(_PRAGMA_RANK, symbol, declared_at, false, symbol.name)) | |
| 208 | else | |
| 209 | let fault = _entry_point_shape_fault(symbol) | |
| 210 | ||
| 211 | if !fault? then | |
| 212 | _candidates.add(ENTRY_POINT_CANDIDATE(_NAMED_RANK, symbol, declared_at, false, symbol.name)) | |
| 213 | elif | |
| 214 | _build_flags.want_executable /\ | |
| 215 | !_build_flags.want_library /\ | |
| 216 | isa Semantic.Symbols.GLOBAL_FUNCTION(symbol) /\ | |
| 217 | !_rejected_named? | |
| 218 | then | |
| 219 | // Only a global function is selected by the name: a | |
| 220 | // static method of the same name is an ordinary | |
| 221 | // member, as it is for the environment parameter. A | |
| 222 | // library enters nowhere, so the name means nothing | |
| 223 | // there either. Whether this is a fault waits for the | |
| 224 | // choice - an `@entry` pragma elsewhere names the | |
| 225 | // entry point over any name match, and then this | |
| 226 | // declaration was never where the program starts. | |
| 227 | _rejected_named = REJECTED_ENTRY_NAME(declared_at, symbol.name, fault) | |
| 228 | fi | |
| 229 | fi | |
| 230 | ||
| 231 | return true | |
| 232 | si | |
| 233 | ||
| 234 | finish() is | |
| 235 | let chosen mut = _choose() | |
| 236 | ||
| 237 | // A rejected name is a fault only where nothing outranked | |
| 238 | // it: with an `@entry` pragma elsewhere the program starts | |
| 239 | // there and this declaration was never consulted, while a | |
| 240 | // file's top-level statements rank below the name and do not | |
| 241 | // stand in for it - a program that declares where it starts | |
| 242 | // has said so. | |
| 243 | if let rejected = _rejected_named /\ (!chosen? \/ chosen.is_top_level) then | |
| 244 | _logger.error(rejected.location, "cannot start at {rejected.name}: {rejected.fault}") | |
| 245 | ||
| 246 | _entry_name_claimed = true | |
| 247 | chosen = null | |
| 248 | fi | |
| 249 | ||
| 250 | if let selected = chosen then | |
| 251 | selected.symbol.is_entry_point = true | |
| 252 | fi | |
| 253 | ||
| 254 | // `--entry` excludes every top-level candidate from `_choose`, | |
| 255 | // so a null result while it was given means nothing else | |
| 256 | // matched the name either - not the multi-file tie the | |
| 257 | // fallback message below otherwise assumes. | |
| 258 | named_entry_point_not_found = !chosen? /\ _ir_context.entry_point_name_is_explicit | |
| 259 | ||
| 260 | for candidate in _candidates do | |
| 261 | if !candidate.is_top_level \/ (chosen? /\ candidate.symbol == chosen.symbol) then | |
| 262 | continue | |
| 263 | fi | |
| 264 | ||
| 265 | left_top_level_statements_unrun = true | |
| 266 | ||
| 267 | _warn_unrun(candidate, chosen) | |
| 268 | od | |
| 269 | si | |
| 270 | ||
| 271 | // The winner, or absent when nothing is declared or when the top | |
| 272 | // rank is a tie the project is allowed to have. | |
| 273 | _choose() -> ENTRY_POINT_CANDIDATE? is | |
| 274 | let best: ENTRY_POINT_CANDIDATE? mut = null | |
| 275 | ||
| 276 | for candidate in _candidates do | |
| 277 | if candidate.rank == _TOP_LEVEL_RANK /\ _ir_context.entry_point_name_is_explicit then | |
| 278 | continue | |
| 279 | fi | |
| 280 | ||
| 281 | if !best? \/ candidate.rank < best.rank then | |
| 282 | best = candidate | |
| 283 | fi | |
| 284 | od | |
| 285 | ||
| 286 | if !best? then | |
| 287 | return null | |
| 288 | fi | |
| 289 | ||
| 290 | let winners = Collections.LIST[ENTRY_POINT_CANDIDATE]() | |
| 291 | ||
| 292 | for candidate in _candidates do | |
| 293 | if candidate.rank == best.rank /\ | |
| 294 | !(candidate.rank == _TOP_LEVEL_RANK /\ _ir_context.entry_point_name_is_explicit) | |
| 295 | then | |
| 296 | winners.add(candidate) | |
| 297 | fi | |
| 298 | od | |
| 299 | ||
| 300 | if winners.count == 1 then | |
| 301 | return winners[0] | |
| 302 | fi | |
| 303 | ||
| 304 | // Two files each carrying top-level statements is a shape an | |
| 305 | // umbrella project has on purpose, so it selects nothing and | |
| 306 | // says so per file rather than failing here. | |
| 307 | if best.rank != _TOP_LEVEL_RANK then | |
| 308 | for i in 1..winners.count do | |
| 309 | _logger.error( | |
| 310 | winners[i].location, | |
| 311 | "duplicate entrypoint", | |
| 312 | winners[0].location, | |
| 313 | "entry point already declared here") | |
| 314 | od | |
| 315 | ||
| 316 | return winners[0] | |
| 317 | fi | |
| 318 | ||
| 319 | return null | |
| 320 | si | |
| 321 | ||
| 322 | _warn_unrun(candidate: ENTRY_POINT_CANDIDATE, chosen: ENTRY_POINT_CANDIDATE?) is | |
| 323 | if _entry_name_claimed then | |
| 324 | // The error at the function named as the entry point says | |
| 325 | // why nothing runs; that these statements are what did | |
| 326 | // not run instead is not a second fault. | |
| 327 | return | |
| 328 | fi | |
| 329 | ||
| 330 | if let selected = chosen then | |
| 331 | _logger.warn( | |
| 332 | candidate.location, | |
| 333 | UNRUN_STATEMENTS, | |
| 334 | "top-level statements will not run: the entry point is {selected.display_name}", | |
| 335 | selected.location, | |
| 336 | "entry point declared here") | |
| 337 | elif named_entry_point_not_found then | |
| 338 | _logger.warn( | |
| 339 | candidate.location, | |
| 340 | UNRUN_STATEMENTS, | |
| 341 | "top-level statements will not run: no function matches the --entry name {_ir_context.entry_point_name}") | |
| 342 | else | |
| 343 | _logger.warn( | |
| 344 | candidate.location, | |
| 345 | UNRUN_STATEMENTS, | |
| 346 | "top-level statements will not run: more than one file carries them and none is named with --entry") | |
| 347 | fi | |
| 348 | si | |
| 349 | ||
| 350 | // Which part of this function's shape the runtime cannot enter | |
| 351 | // at, or null where it can: an entry point takes no arguments or | |
| 352 | // one `string[]` - a `Ghul.Environment` parameter is carried by | |
| 353 | // a synthesised wrapper, which is what reaches here in its place | |
| 354 | // - and returns `int` or `void`. A return type the walk has not | |
| 355 | // settled yet - analysis mode leaves an unopened file's body | |
| 356 | // uncompiled - is taken on trust rather than silently dropping | |
| 357 | // the candidate. | |
| 358 | _entry_point_shape_fault(symbol: Function) -> string? is | |
| 359 | let return_type = symbol.return_type | |
| 360 | ||
| 361 | if !return_type? then | |
| 362 | return null | |
| 363 | fi | |
| 364 | ||
| 365 | let lookup = IoC.CONTAINER.instance.innate_symbol_lookup | |
| 366 | ||
| 367 | // An asynchronous function is ruled out here too, by the | |
| 368 | // task-like it returns rather than by anything else about | |
| 369 | // it: neither a task nor a task-like is int or void. | |
| 370 | if !return_type.matches(lookup.get_int_type()) /\ !return_type.matches(lookup.get_void_type()) then | |
| 371 | return "the return type is neither int nor void" | |
| 372 | fi | |
| 373 | ||
| 374 | if symbol.arguments.count > 1 then | |
| 375 | return "an entry point takes at most one string[] and one Ghul.Environment" | |
| 376 | fi | |
| 377 | ||
| 378 | if symbol.arguments.count == 1 /\ | |
| 379 | !symbol.arguments[0].matches(lookup.get_array_type(lookup.get_string_type())) | |
| 380 | then | |
| 381 | return "the parameter type is neither string[] nor Ghul.Environment" | |
| 382 | fi | |
| 383 | ||
| 384 | return null | |
| 385 | si | |
| 386 | ||
| 387 | _first_statement_location(function: Definitions.FUNCTION) -> LOCATION is | |
| 388 | if let block = cast Bodies.BLOCK?(function.body) then | |
| 389 | for statement in block.statements do | |
| 390 | return statement.location | |
| 391 | od | |
| 392 | fi | |
| 393 | ||
| 394 | return function.location | |
| 395 | si | |
| 396 | ||
| 397 | _function_for(node: Trees.Node) -> Function? is | |
| 398 | let scope = _symbol_table.scope_for(node) | |
| 399 | ||
| 400 | if !scope? then | |
| 401 | return null | |
| 402 | fi | |
| 403 | ||
| 404 | return cast Function?(scope.underlying_scope) | |
| 405 | si | |
| 406 | si | |
| 407 | ||
| 408 | class ENTRY_POINT_CANDIDATE( | |
| 409 | rank: int, | |
| 410 | symbol: Function, | |
| 411 | location: LOCATION, | |
| 412 | is_top_level: bool, | |
| 413 | display_name: string | |
| 414 | ) is si | |
| 415 | si |