Skip to content
← Back

src/syntax/process/select_entry_point.ghul

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