Appearance
| 1 | namespace Semantic is | |
| 2 | use Source.LOCATION | |
| 3 | ||
| 4 | use Types.Type | |
| 5 | ||
| 6 | // What an async function returns, in the shape the lowering needs: | |
| 7 | // the result its body produces (the type the builder's `set_result` | |
| 8 | // delivers, void for a task-like with no type argument), and the | |
| 9 | // unspecialized builder an async function returning the task-like | |
| 10 | // drives. | |
| 11 | // | |
| 12 | // A task-like is a type carrying AsyncMethodBuilderAttribute naming | |
| 13 | // its builder - `Tasks.TASK[T]` / `Tasks.ValueTask[T]` from the BCL, | |
| 14 | // or a type declared in ghūl source. The convention, matching the | |
| 15 | // BCL's own task-likes: a generic task-like has exactly one type | |
| 16 | // argument and it is the result type; a non-generic one carries no | |
| 17 | // result. | |
| 18 | class TASK_LIKE is | |
| 19 | task_type: Type | |
| 20 | result_type: Type? | |
| 21 | builder_type: Type | |
| 22 | ||
| 23 | is_void_like: bool => !result_type? | |
| 24 | ||
| 25 | // The one home for locating a builder member: an overload of the | |
| 26 | // name taking exactly `arity` arguments. Both the completeness | |
| 27 | // check above and the emission-time member resolution route | |
| 28 | // through here, so the contract they enforce is the same one. | |
| 29 | member_of_arity(type: Type, name: string, arity: int) -> Symbols.Function? static is | |
| 30 | if let group: Symbols.FUNCTION_GROUP = type.find_member(name) then | |
| 31 | for f in group.functions do | |
| 32 | if f.arguments.count == arity then | |
| 33 | return f | |
| 34 | fi | |
| 35 | od | |
| 36 | fi | |
| 37 | ||
| 38 | return null | |
| 39 | si | |
| 40 | ||
| 41 | init(task_type: Type, result_type: Type?, builder_type: Type) is | |
| 42 | self.task_type = task_type | |
| 43 | self.result_type = result_type | |
| 44 | self.builder_type = builder_type | |
| 45 | si | |
| 46 | si | |
| 47 | ||
| 48 | // Recognises a task-like return type. The builder is read from the | |
| 49 | // type's symbol: for an imported type, `Classy.async_builder` was | |
| 50 | // populated at import from the attribute; for a source-declared | |
| 51 | // type, from its attribute pragma during resolve-type-expressions - | |
| 52 | // both before any body is compiled, so the answer does not depend | |
| 53 | // on file order. | |
| 54 | class TASK_LIKE_RESOLVER(_logger: Logging.Logger) is | |
| 55 | // The same answer with nothing reported: for probing a type | |
| 56 | // that may not be a task-like at all, where a malformed one | |
| 57 | // is somebody else's to diagnose, at a location of its own. | |
| 58 | try_resolve(type: Type?) -> TASK_LIKE? => | |
| 59 | TASK_LIKE_RESOLVER(Logging.DIAGNOSTICS_STORE()).resolve(LOCATION.internal, type) | |
| 60 | ||
| 61 | resolve(location: LOCATION, return_type: Type?) -> TASK_LIKE? is | |
| 62 | if !return_type? \/ return_type.is_error then | |
| 63 | return null | |
| 64 | fi | |
| 65 | ||
| 66 | let classy = cast Symbols.Classy?(return_type.symbol.unspecialized_symbol) | |
| 67 | ||
| 68 | if !classy? then | |
| 69 | return null | |
| 70 | fi | |
| 71 | ||
| 72 | if classy.is_generic then | |
| 73 | // Only the single-argument convention is supported; a | |
| 74 | // generic whose argument is not the result type has no | |
| 75 | // way to say which argument is. | |
| 76 | if !isa Types.GENERIC(return_type) then | |
| 77 | return null | |
| 78 | fi | |
| 79 | ||
| 80 | let generic = cast Types.GENERIC(return_type) | |
| 81 | ||
| 82 | if generic.arguments.count != 1 then | |
| 83 | _logger.error( | |
| 84 | location, | |
| 85 | "a generic task-like type must have exactly one type argument, but {return_type} has {generic.arguments.count}" | |
| 86 | ) | |
| 87 | return null | |
| 88 | fi | |
| 89 | fi | |
| 90 | ||
| 91 | let builder_type = classy.async_builder | |
| 92 | ||
| 93 | if !builder_type? then | |
| 94 | return null | |
| 95 | fi | |
| 96 | ||
| 97 | let result_type: Type? = | |
| 98 | if isa Types.GENERIC(return_type) then | |
| 99 | (cast Types.GENERIC(return_type)).arguments[0] | |
| 100 | else | |
| 101 | null | |
| 102 | fi | |
| 103 | ||
| 104 | if !_builder_is_complete(location, builder_type, result_type) then | |
| 105 | return null | |
| 106 | fi | |
| 107 | ||
| 108 | return TASK_LIKE(return_type, result_type, builder_type) | |
| 109 | si | |
| 110 | ||
| 111 | // The members the async lowering drives a builder through. Every | |
| 112 | // one is checked here so a malformed builder is reported at the | |
| 113 | // function that returns its task-like, not as an emission-time | |
| 114 | // contract failure. | |
| 115 | _builder_is_complete(location: LOCATION, builder_type: Type, result_type: Type?) -> bool is | |
| 116 | let set_result_arity: int = if result_type? then 1 else 0 fi | |
| 117 | ||
| 118 | let required: Collections.List[(string, int)] = | |
| 119 | Collections.LIST[(string, int)]([ | |
| 120 | ("create", 0), | |
| 121 | ("start", 1), | |
| 122 | ("set_result", set_result_arity), | |
| 123 | ("set_exception", 1), | |
| 124 | ("await_on_completed", 2), | |
| 125 | ("await_unsafe_on_completed", 2) | |
| 126 | ]) | |
| 127 | ||
| 128 | let task_property = cast Symbols.Property?(builder_type.find_member("task")) | |
| 129 | ||
| 130 | if !(task_property? /\ task_property.read_function?) then | |
| 131 | _logger.error( | |
| 132 | location, | |
| 133 | "builder {builder_type} for a task-like return type has no readable task property") | |
| 134 | return false | |
| 135 | fi | |
| 136 | ||
| 137 | for (name, arity) in required do | |
| 138 | let member = TASK_LIKE.member_of_arity(builder_type, name, arity) | |
| 139 | ||
| 140 | if !member? then | |
| 141 | _logger.error( | |
| 142 | location, | |
| 143 | "builder {builder_type} for a task-like return type has no {name} method taking {arity} argument(s)") | |
| 144 | return false | |
| 145 | fi | |
| 146 | ||
| 147 | // The members the lowering passes by address: `start` | |
| 148 | // receives the state machine by reference, and both | |
| 149 | // await registrations receive the awaiter by reference. | |
| 150 | // An arity match alone would let a by-value declaration | |
| 151 | // through, and the emission always passes the address. | |
| 152 | if | |
| 153 | name =~ "start" \/ name.starts_with("await_") | |
| 154 | then | |
| 155 | if !isa Types.REFERENCE(member.arguments[0]) then | |
| 156 | _logger.error( | |
| 157 | location, | |
| 158 | "builder {builder_type} for a task-like return type: {name} must take its first argument by reference") | |
| 159 | return false | |
| 160 | fi | |
| 161 | fi | |
| 162 | od | |
| 163 | ||
| 164 | return true | |
| 165 | si | |
| 166 | si | |
| 167 | si |