Appearance
| 1 | namespace IR.Emitter is | |
| 2 | use System.Reflection.Metadata.BlobBuilder | |
| 3 | use System.Reflection.Metadata.BlobContentId | |
| 4 | use System.Reflection.Metadata.DocumentHandle | |
| 5 | use System.Reflection.Metadata.Ecma335.MetadataBuilder | |
| 6 | use System.Reflection.Metadata.Ecma335.PortablePdbBuilder | |
| 7 | ||
| 8 | // One source position the debugger or a coverage tool can stop at, | |
| 9 | // paired with the instruction offset it starts at. | |
| 10 | class SRM_SEQUENCE_POINT( | |
| 11 | il_offset: int, | |
| 12 | file_name: string, | |
| 13 | start_line: int, | |
| 14 | start_column: int, | |
| 15 | end_line: int, | |
| 16 | end_column: int | |
| 17 | ) | |
| 18 | ||
| 19 | // A run of instructions an `@IL.output` pragma marks. The binary back | |
| 20 | // end records one per marked statement and carries them out of the | |
| 21 | // method as a custom attribute, which the IL test runner reads back to | |
| 22 | // slice the disassembly to just the statement's instructions. The | |
| 23 | // sequence number is compilation-global, so a test marking statements | |
| 24 | // in more than one method can rebuild their source order. | |
| 25 | class SRM_IL_OUTPUT_RANGE( | |
| 26 | path: string, | |
| 27 | start_offset: int, | |
| 28 | end_offset: int, | |
| 29 | sequence: int | |
| 30 | ) | |
| 31 | ||
| 32 | // Builds the portable PDB that sits beside the emitted assembly. | |
| 33 | // | |
| 34 | // A PDB is its own metadata file with its own tables, so this holds | |
| 35 | // a second MetadataBuilder rather than adding to the assembly's. | |
| 36 | // The one hard coupling between them is MethodDebugInformation: | |
| 37 | // its rows are not looked up by key, they are read positionally | |
| 38 | // against MethodDef, so row N here has to describe method N there. | |
| 39 | // Nothing checks - a missing row shifts every method after it onto | |
| 40 | // another method's line numbers, which reads as a compiler that | |
| 41 | // emits plausible but wrong debug info rather than as a broken | |
| 42 | // file. `add_method_debug_information` is therefore called once per | |
| 43 | // MethodDef row from the one place that adds them, including for | |
| 44 | // the methods that have no sequence points at all. | |
| 45 | class SRM_PDB_BUILDER is | |
| 46 | _metadata: MetadataBuilder | |
| 47 | _documents: Collections.MAP[string, DocumentHandle] | |
| 48 | ||
| 49 | init() is | |
| 50 | _metadata = MetadataBuilder(0, 0, 0, 0) | |
| 51 | _documents = Collections.MAP[string, DocumentHandle]() | |
| 52 | si | |
| 53 | ||
| 54 | _EMPTY_HASH: ubyte[] static => [] | |
| 55 | ||
| 56 | _document(file_name: string) -> DocumentHandle is | |
| 57 | let existing: DocumentHandle mut | |
| 58 | ||
| 59 | if _documents.try_get_value(file_name, existing ref) then | |
| 60 | return existing | |
| 61 | fi | |
| 62 | ||
| 63 | // No hash and no language: a hash commits the PDB to one | |
| 64 | // byte-for-byte revision of the source, which is what a | |
| 65 | // debugger checks before binding a breakpoint, and the | |
| 66 | // compiler does not keep the bytes it read by the time the | |
| 67 | // assembly is written. Line lookup and coverage attribution | |
| 68 | // read the name and the sequence points, and need neither. | |
| 69 | let handle = | |
| 70 | _metadata.add_document( | |
| 71 | _metadata.get_or_add_document_name(file_name), | |
| 72 | _[System.Reflection.Metadata.GuidHandle], | |
| 73 | _metadata.get_or_add_blob(_EMPTY_HASH), | |
| 74 | _[System.Reflection.Metadata.GuidHandle]) | |
| 75 | ||
| 76 | _documents[file_name] = handle | |
| 77 | ||
| 78 | return handle | |
| 79 | si | |
| 80 | ||
| 81 | // One row, whether or not there is anything to say - see the | |
| 82 | // positional coupling above. A method with no sequence points | |
| 83 | // (an abstract declaration, a compiler-synthesised member, any | |
| 84 | // method at all in a non-debug build) gets an empty row. | |
| 85 | add_method_debug_information(points: Collections.List[SRM_SEQUENCE_POINT]?) is | |
| 86 | if !points? \/ points.count == 0 then | |
| 87 | _metadata.add_method_debug_information( | |
| 88 | _[DocumentHandle], _[System.Reflection.Metadata.BlobHandle]) | |
| 89 | ||
| 90 | return | |
| 91 | fi | |
| 92 | ||
| 93 | let initial_document = _document(points[0].file_name) | |
| 94 | ||
| 95 | _metadata.add_method_debug_information( | |
| 96 | initial_document, | |
| 97 | _metadata.get_or_add_blob(_sequence_point_blob(points, initial_document))) | |
| 98 | si | |
| 99 | ||
| 100 | // The sequence-point blob, as ECMA-335 II.24.2.1 defines it: | |
| 101 | // a header naming the method's local signature, then one record | |
| 102 | // per point, each expressed as a delta from the one before so | |
| 103 | // the common case costs a handful of bytes. | |
| 104 | // | |
| 105 | // The first record carries absolute values; every later one | |
| 106 | // carries deltas, and its IL-offset delta must be non-zero, | |
| 107 | // because a zero there is the marker for a document-change | |
| 108 | // record instead. | |
| 109 | _sequence_point_blob( | |
| 110 | points: Collections.List[SRM_SEQUENCE_POINT], | |
| 111 | initial_document: DocumentHandle | |
| 112 | ) -> ubyte[] is | |
| 113 | let buffer = BlobBuilder(32) | |
| 114 | ||
| 115 | // Local signature. The back end emits no StandAloneSig row | |
| 116 | // for a body's locals, so there is none to name. | |
| 117 | buffer.write_compressed_integer(0) | |
| 118 | ||
| 119 | let previous mut = _[SRM_SEQUENCE_POINT?] | |
| 120 | let current_document mut = initial_document | |
| 121 | ||
| 122 | for point in points do | |
| 123 | let document = _document(point.file_name) | |
| 124 | ||
| 125 | if !(document =~ current_document) then | |
| 126 | // A document-change record: the zero IL-offset | |
| 127 | // delta that no ordinary record can carry, then the | |
| 128 | // document being switched to. | |
| 129 | // | |
| 130 | // It changes which document the records after it | |
| 131 | // belong to and nothing else. The deltas keep | |
| 132 | // running from the last real sequence point, so the | |
| 133 | // record following it is still a delta rather than | |
| 134 | // absolute - resetting the baseline here would | |
| 135 | // desynchronise every offset and line after it. | |
| 136 | buffer.write_compressed_integer(0) | |
| 137 | buffer.write_compressed_integer( | |
| 138 | System.Reflection.Metadata.Ecma335.MetadataTokens.get_row_number( | |
| 139 | cast System.Reflection.Metadata.EntityHandle(document))) | |
| 140 | ||
| 141 | current_document = document | |
| 142 | fi | |
| 143 | ||
| 144 | let delta_lines = point.end_line - point.start_line | |
| 145 | let delta_columns = point.end_column - point.start_column | |
| 146 | ||
| 147 | if let seen = previous then | |
| 148 | buffer.write_compressed_integer(point.il_offset - seen.il_offset) | |
| 149 | else | |
| 150 | buffer.write_compressed_integer(point.il_offset) | |
| 151 | fi | |
| 152 | ||
| 153 | buffer.write_compressed_integer(delta_lines) | |
| 154 | ||
| 155 | // On a point that begins and ends on one line the | |
| 156 | // column delta is that line's width, which cannot be | |
| 157 | // negative, so it is unsigned. Across lines the end | |
| 158 | // column can fall to the left of the start column, so | |
| 159 | // it is signed. | |
| 160 | if delta_lines == 0 then | |
| 161 | buffer.write_compressed_integer(delta_columns) | |
| 162 | else | |
| 163 | buffer.write_compressed_signed_integer(delta_columns) | |
| 164 | fi | |
| 165 | ||
| 166 | if let seen = previous then | |
| 167 | buffer.write_compressed_signed_integer( | |
| 168 | point.start_line - seen.start_line) | |
| 169 | buffer.write_compressed_signed_integer( | |
| 170 | point.start_column - seen.start_column) | |
| 171 | else | |
| 172 | buffer.write_compressed_integer(point.start_line) | |
| 173 | buffer.write_compressed_integer(point.start_column) | |
| 174 | fi | |
| 175 | ||
| 176 | previous = point | |
| 177 | od | |
| 178 | ||
| 179 | return buffer.to_array() | |
| 180 | si | |
| 181 | ||
| 182 | // Writes the PDB and hands back the identity the assembly's | |
| 183 | // debug directory has to name for a runtime to pair the two. | |
| 184 | // The serialized PDB, for a caller that wants the bytes rather | |
| 185 | // than a file - reading them back through a metadata reader is | |
| 186 | // how the blob encoding above is tested. | |
| 187 | serialize( | |
| 188 | blob: BlobBuilder, | |
| 189 | type_system_row_counts: System.Collections.Immutable.ImmutableArray[int], | |
| 190 | entry_point: System.Reflection.Metadata.MethodDefinitionHandle, | |
| 191 | content_id_provider: (Collections.Iterable[System.Reflection.Metadata.Blob]) -> BlobContentId | |
| 192 | ) -> BlobContentId => | |
| 193 | PortablePdbBuilder( | |
| 194 | _metadata, type_system_row_counts, entry_point, content_id_provider) | |
| 195 | .serialize(blob) | |
| 196 | ||
| 197 | write_to( | |
| 198 | path: string, | |
| 199 | type_system_row_counts: System.Collections.Immutable.ImmutableArray[int], | |
| 200 | entry_point: System.Reflection.Metadata.MethodDefinitionHandle, | |
| 201 | content_id_provider: (Collections.Iterable[System.Reflection.Metadata.Blob]) -> BlobContentId | |
| 202 | ) -> BlobContentId is | |
| 203 | let blob = BlobBuilder(1024) | |
| 204 | ||
| 205 | let content_id = | |
| 206 | serialize(blob, type_system_row_counts, entry_point, content_id_provider) | |
| 207 | ||
| 208 | let stream = IO.File.create(path) | |
| 209 | blob.write_content_to(stream) | |
| 210 | stream.close() | |
| 211 | ||
| 212 | return content_id | |
| 213 | si | |
| 214 | si | |
| 215 | si |