Commit c1baf026f5fba05cb5619603b350445d6e40a0d5
fix merge sections
wanderer committed on 2/14/2018, 9:31:02 PMParent: 69542e9b41cc18d3434269b364a714712a5a5631
Files changed
customTypes.js | ||
---|---|---|
@@ -131,13 +131,81 @@ | ||
131 | 131 | const buf = encodeJSON(json) |
132 | 132 | return injectCustomSection(buf, wasm) |
133 | 133 | } |
134 | 134 | |
135 | +function mergeTypeSections (json) { | |
136 | + const result = { | |
137 | + types: [], | |
138 | + indexes: {}, | |
139 | + exports: {} | |
140 | + } | |
141 | + const iterator = findSections(json) | |
142 | + const mappedFuncs = new Map() | |
143 | + const mappedTypes = new Map() | |
144 | + iterator.next() | |
145 | + const {value: customType} = iterator.next('custom') | |
146 | + if (customType) { | |
147 | + const type = decodeType(customType.payload) | |
148 | + result.types = type | |
149 | + } | |
150 | + let {value: typeMap} = iterator.next('custom') | |
151 | + if (typeMap) { | |
152 | + decodeTypeMap(typeMap.payload).forEach(map => mappedFuncs.set(map.func, map.type)) | |
153 | + } | |
154 | + | |
155 | + const {value: type} = iterator.next('type') | |
156 | + const {value: imports = {entries: []}} = iterator.next('import') | |
157 | + const {value: functions} = iterator.next('function') | |
158 | + functions.entries.forEach((typeIndex, funcIndex) => { | |
159 | + let customIndex = mappedFuncs.get(funcIndex) | |
160 | + if (customIndex === undefined) { | |
161 | + customIndex = mappedTypes.get(typeIndex) | |
162 | + } | |
163 | + if (customIndex === undefined) { | |
164 | + customIndex = result.types.push(type.entries[typeIndex]) - 1 | |
165 | + mappedTypes.set(typeIndex, customIndex) | |
166 | + } | |
167 | + result.indexes[funcIndex + imports.entries.length] = customIndex | |
168 | + }) | |
169 | + | |
170 | + const {value: exports = {entries: []}} = iterator.next('export') | |
171 | + exports.entries.forEach(entry => { | |
172 | + if (entry.kind === 'function') { | |
173 | + result.exports[entry.field_str] = entry.index | |
174 | + } | |
175 | + }) | |
176 | + return result | |
177 | +} | |
178 | + | |
179 | +const wantedSections = new Set(['custom', 'type', 'function', 'export', 'import']) | |
180 | + | |
181 | +function * findSections (array) { | |
182 | + let section = array[0] | |
183 | + let index = 0 | |
184 | + let nextSection = yield null | |
185 | + | |
186 | + while (section) { | |
187 | + if (!wantedSections.has(section.name)) { | |
188 | + index++ | |
189 | + section = array[index] | |
190 | + } else { | |
191 | + if (section.name === nextSection) { | |
192 | + nextSection = yield section | |
193 | + index++ | |
194 | + section = array[index] | |
195 | + } else { | |
196 | + nextSection = yield null | |
197 | + } | |
198 | + } | |
199 | + } | |
200 | +} | |
201 | + | |
135 | 202 | module.exports = { |
136 | 203 | injectCustomSection, |
137 | 204 | inject, |
138 | 205 | decodeType, |
139 | 206 | decodeTypeMap, |
140 | 207 | encodeType, |
141 | 208 | encodeTypeMap, |
142 | - encodeJSON | |
209 | + encodeJSON, | |
210 | + mergeTypeSections | |
143 | 211 | } |
tests/wasm/caller.wasm | ||
---|---|---|
@@ -1,3 +1,4 @@ | ||
1 | - asm | |
1 | + asm | |
2 | +type`` typeMap | |
2 | 3 | ` ` funcinternalize ptable call |
3 | 4 | A A A |
tests/wasm/funcRef_caller.wasm | ||
---|---|---|
@@ -1,0 +1,3 @@ | ||
1 | + asm | |
2 | +type`` typeMap ` ` `` @funcinternalize testcheck moduleself moduleexports p $memory table call callback | |
3 | + A AA A A A callback |
tests/wasm/funcRef_reciever.wasm | ||
---|---|---|
@@ -1,0 +1,4 @@ | ||
1 | + asm | |
2 | +type`` typeMap | |
3 | +` ` funcinternalize ptable receive | |
4 | + A AA |
tests/wasmContainer.js | ||
---|---|---|
@@ -34,9 +34,9 @@ | ||
34 | 34 | return this.refs.add(obj) |
35 | 35 | } |
36 | 36 | } |
37 | 37 | |
38 | -tape.only('basic', async t => { | |
38 | +tape('basic', async t => { | |
39 | 39 | t.plan(2) |
40 | 40 | tester = t |
41 | 41 | const expectedState = { |
42 | 42 | '/': Buffer.from('4494963fb0e02312510e675fbca8b60b6e03bd00', 'hex') |
@@ -76,14 +76,13 @@ | ||
76 | 76 | |
77 | 77 | const hypervisor = new Hypervisor(tree) |
78 | 78 | hypervisor.registerContainer(TestWasmContainer) |
79 | 79 | |
80 | - const {exports: receiverExports} = await hypervisor.createActor(TestWasmContainer.typeId, recieverWasm) | |
81 | - const {exports: callerExports} = await hypervisor.createActor(TestWasmContainer.typeId, callerWasm) | |
82 | - | |
80 | + const {module: receiverMod} = await hypervisor.createActor(TestWasmContainer.typeId, recieverWasm) | |
81 | + const {module: callerMod} = await hypervisor.createActor(TestWasmContainer.typeId, callerWasm) | |
83 | 82 | const message = new Message({ |
84 | - funcRef: callerExports.call, | |
85 | - funcArguments: [receiverExports.receive] | |
83 | + funcRef: callerMod.getFuncRef('call'), | |
84 | + funcArguments: [receiverMod.getFuncRef('receive')] | |
86 | 85 | }) |
87 | 86 | |
88 | 87 | hypervisor.send(message) |
89 | 88 | const stateRoot = await hypervisor.createStateRoot() |
@@ -93,9 +92,9 @@ | ||
93 | 92 | tape('two communicating actors with callback', async t => { |
94 | 93 | // t.plan(2) |
95 | 94 | tester = t |
96 | 95 | const expectedState = { |
97 | - '/': Buffer.from('f3cc5ba63d6b1737bea2c33bd1942e5488787b82', 'hex') | |
96 | + '/': Buffer.from('51ded6c294314defc886b70f7f593434c8d53c95', 'hex') | |
98 | 97 | } |
99 | 98 | |
100 | 99 | const tree = new RadixTree({ |
101 | 100 | db |
@@ -106,19 +105,19 @@ | ||
106 | 105 | |
107 | 106 | const hypervisor = new Hypervisor(tree) |
108 | 107 | hypervisor.registerContainer(TestWasmContainer) |
109 | 108 | |
110 | - const {exports: receiverExports} = await hypervisor.createActor(TestWasmContainer.typeId, recieverWasm) | |
111 | - const {exports: callerExports} = await hypervisor.createActor(TestWasmContainer.typeId, callerWasm) | |
109 | + const {module: receiverMod} = await hypervisor.createActor(TestWasmContainer.typeId, recieverWasm) | |
110 | + const {module: callerMod} = await hypervisor.createActor(TestWasmContainer.typeId, callerWasm) | |
112 | 111 | |
113 | 112 | const message = new Message({ |
114 | - funcRef: callerExports.call, | |
115 | - funcArguments: [receiverExports.receive] | |
113 | + funcRef: callerMod.getFuncRef('call'), | |
114 | + funcArguments: [receiverMod.getFuncRef('receive')] | |
116 | 115 | }) |
117 | 116 | |
118 | 117 | hypervisor.send(message) |
119 | 118 | const stateRoot = await hypervisor.createStateRoot() |
120 | - // t.deepEquals(stateRoot, expectedState, 'expected root!') | |
119 | + t.deepEquals(stateRoot, expectedState, 'expected root!') | |
121 | 120 | t.end() |
122 | 121 | }) |
123 | 122 | |
124 | 123 | // Increment a counter. |
@@ -131,12 +130,12 @@ | ||
131 | 130 | |
132 | 131 | const hypervisor = new Hypervisor(tree) |
133 | 132 | hypervisor.registerContainer(TestWasmContainer) |
134 | 133 | |
135 | - const {exports} = await hypervisor.createActor(TestWasmContainer.typeId, wasm) | |
134 | + const {module} = await hypervisor.createActor(TestWasmContainer.typeId, wasm) | |
136 | 135 | |
137 | 136 | const message = new Message({ |
138 | - funcRef: exports.increment, | |
137 | + funcRef: module.increment, | |
139 | 138 | funcArguments: [] |
140 | 139 | }) |
141 | 140 | hypervisor.send(message) |
142 | 141 |
tests/wast/elem.wast | ||
---|---|---|
@@ -1,0 +1,13 @@ | ||
1 | +(module | |
2 | + (import "func" "internalize" (func $internalize (param i32 i32))) | |
3 | + (table (export "table") 1 1 anyfunc) | |
4 | + (elem (i32.const 0) $receive) | |
5 | + (func $receive (param i32) | |
6 | + i32.const 5 | |
7 | + get_local 0 | |
8 | + i32.const 0 | |
9 | + call $internalize | |
10 | + i32.const 0 | |
11 | + call_indirect (param i32) | |
12 | + ) | |
13 | + (export "receive" (func $receive))) |
tests/wast/funcRef_caller.json | ||
---|---|---|
@@ -1,0 +1,12 @@ | ||
1 | +{ | |
2 | + "type": [{ | |
3 | + "form": "func", | |
4 | + "params": [ | |
5 | + "func" | |
6 | + ] | |
7 | + }], | |
8 | + "typeMap": [{ | |
9 | + "func": 0, | |
10 | + "type": 0 | |
11 | + }] | |
12 | +} |
tests/wast/funcRef_caller.wast | ||
---|---|---|
@@ -1,0 +1,26 @@ | ||
1 | +(module | |
2 | + (import "func" "internalize" (func $internalize (param i32 i32))) | |
3 | + (import "test" "check" (func $check (param i32 i32))) | |
4 | + (import "module" "self" (func $self (result i32))) | |
5 | + (import "module" "exports" (func $exports (param i32 i32 i32) (result i32))) | |
6 | + (memory (export "memory") 1) | |
7 | + (data (i32.const 0) "callback") | |
8 | + (table (export "table") 1 1 anyfunc) | |
9 | + (func $call (param i32) | |
10 | + call $self | |
11 | + i32.const 0 | |
12 | + i32.const 8 | |
13 | + call $exports | |
14 | + i32.const 0 | |
15 | + get_local 0 | |
16 | + call $internalize | |
17 | + i32.const 0 | |
18 | + call_indirect (param i32) | |
19 | + ) | |
20 | + (func $callback (param i32) | |
21 | + get_local 0 | |
22 | + i32.const 5 | |
23 | + call $check | |
24 | + ) | |
25 | + (export "call" (func $call)) | |
26 | + (export "callback" (func $callback))) |
tests/wast/funcRef_reciever.json | ||
---|---|---|
@@ -1,0 +1,12 @@ | ||
1 | +{ | |
2 | + "type": [{ | |
3 | + "form": "func", | |
4 | + "params": [ | |
5 | + "func" | |
6 | + ] | |
7 | + }], | |
8 | + "typeMap": [{ | |
9 | + "func": 0, | |
10 | + "type": 0 | |
11 | + }] | |
12 | +} |
tests/wast/funcRef_reciever.wast | ||
---|---|---|
@@ -1,0 +1,12 @@ | ||
1 | +(module | |
2 | + (import "func" "internalize" (func $internalize (param i32 i32))) | |
3 | + (table (export "table") 1 1 anyfunc) | |
4 | + (func $receive (param i32) | |
5 | + get_local 0 | |
6 | + i32.const 0 | |
7 | + call $internalize | |
8 | + i32.const 5 | |
9 | + i32.const 0 | |
10 | + call_indirect (param i32) | |
11 | + ) | |
12 | + (export "receive" (func $receive))) |
tests/wast2wasm.js | ||
---|---|---|
@@ -20,11 +20,10 @@ | ||
20 | 20 | const mod = wabt.parseWat('module.wast', wat) |
21 | 21 | const r = mod.toBinary({log: true}) |
22 | 22 | let binary = Buffer.from(r.buffer) |
23 | 23 | if (json) { |
24 | - console.log(json) | |
25 | 24 | const buf = types.encodeJSON(json) |
26 | - // binary = types.injectCustomSection(buf, binary) | |
25 | + binary = types.injectCustomSection(buf, binary) | |
27 | 26 | } |
28 | 27 | fs.writeFileSync(`${__dirname}/wasm/${file}.wasm`, binary) |
29 | 28 | } |
30 | 29 | } |
wasmContainer.js | ||
---|---|---|
@@ -29,10 +29,12 @@ | ||
29 | 29 | 0x60: 'func', |
30 | 30 | 0x40: 'block_type' |
31 | 31 | } |
32 | 32 | |
33 | - | |
34 | 33 | class ElementBuffer { |
34 | + static get type () { | |
35 | + return 'elem' | |
36 | + } | |
35 | 37 | constructor (size) { |
36 | 38 | this._array = new Array(size) |
37 | 39 | } |
38 | 40 | |
@@ -44,8 +46,11 @@ | ||
44 | 46 | static deserialize (serialized) {} |
45 | 47 | } |
46 | 48 | |
47 | 49 | class DataBuffer { |
50 | + static get type () { | |
51 | + return 'data' | |
52 | + } | |
48 | 53 | constructor (memory, offset, length) { |
49 | 54 | this._data = new Uint8Array(this.instance.exports.memory.buffer, offset, length) |
50 | 55 | } |
51 | 56 | serialize () { |
@@ -54,30 +59,41 @@ | ||
54 | 59 | static deserialize (serialized) {} |
55 | 60 | } |
56 | 61 | |
57 | 62 | class LinkRef { |
63 | + static get type () { | |
64 | + return 'link' | |
65 | + } | |
58 | 66 | serialize () { |
59 | 67 | return Buffer.concat(Buffer.from([LANGUAGE_TYPES['link'], this])) |
60 | 68 | } |
61 | 69 | static deserialize (serialized) {} |
62 | 70 | } |
63 | 71 | |
64 | 72 | class FunctionRef { |
65 | 73 | static get type () { |
66 | - return 'funcRef' | |
74 | + return 'func' | |
67 | 75 | } |
68 | 76 | |
69 | - constructor (name, json, id) { | |
70 | - this.name = name | |
77 | + constructor (type, identifier, json, id) { | |
78 | + this.type = type | |
71 | 79 | this.destId = id |
72 | - this.args = [] | |
73 | - const typeIndex = json.typeMap[name] | |
74 | - const type = json.type[typeIndex] | |
75 | - const wrapper = typeCheckWrapper(type) | |
80 | + let funcIndex | |
81 | + if (type === 'export') { | |
82 | + this.indentifier = identifier | |
83 | + funcIndex = json.exports[identifier] | |
84 | + } else { | |
85 | + this.indentifier = identifier.tableIndex | |
86 | + funcIndex = Number(identifier.name) - 1 | |
87 | + } | |
88 | + const typeIndex = json.indexes[funcIndex] | |
89 | + const funcType = json.types[typeIndex] | |
90 | + | |
91 | + const wrapper = typeCheckWrapper(funcType) | |
76 | 92 | const wasm = json2wasm(wrapper) |
77 | - this.mod = WebAssembly.Module(wasm) | |
93 | + const mod = WebAssembly.Module(wasm) | |
78 | 94 | const self = this |
79 | - const instance = WebAssembly.Instance(this.mod, { | |
95 | + this.wrapper = WebAssembly.Instance(mod, { | |
80 | 96 | 'env': { |
81 | 97 | 'checkTypes': function () { |
82 | 98 | const args = [...arguments] |
83 | 99 | const checkedArgs = [] |
@@ -96,23 +112,27 @@ | ||
96 | 112 | self._container.actor.send(message) |
97 | 113 | } |
98 | 114 | } |
99 | 115 | }) |
100 | - this.wrapper = instance | |
116 | + this.wrapper.exports.check.object = this | |
101 | 117 | } |
102 | 118 | set container (container) { |
103 | 119 | this._container = container |
104 | 120 | } |
105 | 121 | } |
106 | 122 | |
107 | 123 | class ModuleRef { |
124 | + static get type () { | |
125 | + return 'mod' | |
126 | + } | |
127 | + | |
108 | 128 | constructor (json, id) { |
109 | 129 | this._json = json |
110 | 130 | this.id = id |
111 | 131 | } |
112 | 132 | |
113 | 133 | getFuncRef (name) { |
114 | - return new FunctionRef(name, this._json, this.id) | |
134 | + return new FunctionRef('export', name, this._json, this.id) | |
115 | 135 | } |
116 | 136 | |
117 | 137 | serialize () { |
118 | 138 | return this._json |
@@ -131,9 +151,9 @@ | ||
131 | 151 | if (!WebAssembly.validate(wasm)) { |
132 | 152 | throw new Error('invalid wasm binary') |
133 | 153 | } |
134 | 154 | let moduleJSON = wasm2json(wasm) |
135 | - const json = mergeTypeSections(moduleJSON) | |
155 | + const json = customTypes.mergeTypeSections(moduleJSON) | |
136 | 156 | moduleJSON = wasmMetering.meterJSON(moduleJSON, { |
137 | 157 | meterType: 'i32' |
138 | 158 | }) |
139 | 159 | wasm = json2wasm(moduleJSON) |
@@ -151,11 +171,20 @@ | ||
151 | 171 | getInterface (funcRef) { |
152 | 172 | const self = this |
153 | 173 | return { |
154 | 174 | func: { |
155 | - externalize: () => {}, | |
175 | + externalize: (index) => { | |
176 | + const func = this.instance.exports.table.get(index) | |
177 | + const object = func.object | |
178 | + if (object) { | |
179 | + return self.refs.add(object) | |
180 | + } else { | |
181 | + const ref = new FunctionRef(false, object.tableIndex, self.json, self.actor.id) | |
182 | + return self.refs.add(ref) | |
183 | + } | |
184 | + }, | |
156 | 185 | internalize: (ref, index) => { |
157 | - const funcRef = self.refs.get(ref, 'funcRef') | |
186 | + const funcRef = self.refs.get(ref) | |
158 | 187 | funcRef.container = self |
159 | 188 | this.instance.exports.table.set(index, funcRef.wrapper.exports.check) |
160 | 189 | }, |
161 | 190 | catch: (ref, catchRef) => { |
@@ -184,13 +213,13 @@ | ||
184 | 213 | exports: (modRef, offset, length) => { |
185 | 214 | const mod = this.refs.get(modRef, 'mod') |
186 | 215 | let name = this.getMemory(offset, length) |
187 | 216 | name = Buffer.from(name).toString() |
188 | - const funcRef = new FunctionRef(name, mod, this.actor.id) | |
189 | - return this.refs.add(funcRef) | |
217 | + const funcRef = mod.getFuncRef(name) | |
218 | + return this.refs.add(funcRef, 'func') | |
190 | 219 | }, |
191 | 220 | self: () => { |
192 | - return this.refs.add(this.json, 'mod') | |
221 | + return this.refs.add(this.moduleObj, 'mod') | |
193 | 222 | } |
194 | 223 | }, |
195 | 224 | memory: { |
196 | 225 | externalize: (index, length) => { |
@@ -216,12 +245,12 @@ | ||
216 | 245 | throw new Error('invalid ref') |
217 | 246 | } |
218 | 247 | } |
219 | 248 | const eleBuf = new ElementBuffer(objects) |
220 | - return this.refs.add(eleBuf, 'ele') | |
249 | + return this.refs.add(eleBuf, 'elem') | |
221 | 250 | }, |
222 | 251 | internalize: (dataRef, writeOffset, readOffset, length) => { |
223 | - let buf = this.refs.get(dataRef, 'ele') | |
252 | + let buf = this.refs.get(dataRef, 'elem') | |
224 | 253 | buf = buf.subarray(readOffset, length) |
225 | 254 | const mem = this.getMemory(writeOffset, buf.length) |
226 | 255 | mem.set(buf) |
227 | 256 | } |
@@ -240,19 +269,30 @@ | ||
240 | 269 | async onMessage (message) { |
241 | 270 | const funcRef = message.funcRef |
242 | 271 | const intef = this.getInterface(funcRef) |
243 | 272 | this.instance = WebAssembly.Instance(this.mod, intef) |
244 | - if (this.instance.exports.table) { | |
245 | - this._orginalTable = this.instance.exports.table | |
273 | + const table = this.instance.exports.table | |
274 | + if (table) { | |
275 | + let length = table.length | |
276 | + while (length--) { | |
277 | + const func = table.get(length) | |
278 | + if (func) { | |
279 | + func.tableIndex = length | |
280 | + } | |
281 | + } | |
246 | 282 | } |
247 | 283 | const args = message.funcArguments.map(arg => { |
248 | 284 | if (typeof arg === 'number') { |
249 | 285 | return arg |
250 | 286 | } else { |
251 | 287 | return this.refs.add(arg, arg.constructor.type) |
252 | 288 | } |
253 | 289 | }) |
254 | - this.instance.exports[funcRef.name](...args) | |
290 | + if (funcRef.type === 'export') { | |
291 | + this.instance.exports[funcRef.indentifier](...args) | |
292 | + } else { | |
293 | + this.instance.exports.table.get(funcRef.indentifier)(...args) | |
294 | + } | |
255 | 295 | await this.onDone() |
256 | 296 | this.refs.clear() |
257 | 297 | } |
258 | 298 | |
@@ -307,8 +347,9 @@ | ||
307 | 347 | wasm = Buffer.from(wasm, 'hex') |
308 | 348 | json = JSON.parse(json) |
309 | 349 | this.mod = WebAssembly.Module(wasm) |
310 | 350 | this.json = json |
351 | + this.moduleObj = new ModuleRef(json, this.actor.id) | |
311 | 352 | } |
312 | 353 | |
313 | 354 | getMemory (offset, length) { |
314 | 355 | return new Uint8Array(this.instance.exports.memory.buffer, offset, length) |
@@ -317,72 +358,4 @@ | ||
317 | 358 | static get typeId () { |
318 | 359 | return 9 |
319 | 360 | } |
320 | 361 | } |
321 | - | |
322 | -function mergeTypeSections (json) { | |
323 | - const typeInfo = { | |
324 | - typeMap: [], | |
325 | - type: [] | |
326 | - } | |
327 | - let typeSection = { | |
328 | - 'entries': [] | |
329 | - } | |
330 | - let importSection = { | |
331 | - 'entries': [] | |
332 | - } | |
333 | - let functionSection = { | |
334 | - 'entries': [] | |
335 | - } | |
336 | - let exportSection = { | |
337 | - 'entries': [] | |
338 | - } | |
339 | - json.forEach(section => { | |
340 | - switch (section.name) { | |
341 | - case 'type': | |
342 | - typeSection = section | |
343 | - break | |
344 | - case 'export': | |
345 | - exportSection = section | |
346 | - break | |
347 | - case 'import': | |
348 | - importSection = section | |
349 | - break | |
350 | - case 'function': | |
351 | - functionSection = section | |
352 | - break | |
353 | - case 'custom': | |
354 | - switch (section.sectionName) { | |
355 | - case 'type': | |
356 | - typeInfo.type = customTypes.decodeType(section.payload) | |
357 | - break | |
358 | - case 'typeMap': | |
359 | - typeInfo.typeMap = customTypes.decodeTypeMap(section.payload) | |
360 | - break | |
361 | - } | |
362 | - break | |
363 | - } | |
364 | - }) | |
365 | - | |
366 | - const foundTypes = new Map() | |
367 | - const mappedFuncs = new Map() | |
368 | - const newTypeMap = {} | |
369 | - typeInfo.typeMap.forEach(map => mappedFuncs.set(map.func, map.type)) | |
370 | - for (let exprt of exportSection.entries) { | |
371 | - if (exprt.kind === 'function') { | |
372 | - if (!mappedFuncs.has(exprt.index)) { | |
373 | - const typeIndex = functionSection.entries[exprt.index - importSection.entries.length] | |
374 | - if (!foundTypes.has(typeIndex)) { | |
375 | - const customIndex = typeInfo.type.push(typeSection.entries[typeIndex]) - 1 | |
376 | - foundTypes.set(typeIndex, customIndex) | |
377 | - } | |
378 | - const customIndex = foundTypes.get(typeIndex) | |
379 | - newTypeMap[exprt.field_str] = customIndex | |
380 | - } else { | |
381 | - newTypeMap[exprt.field_str] = mappedFuncs.get(exprt.index) | |
382 | - } | |
383 | - } | |
384 | - } | |
385 | - | |
386 | - typeInfo.typeMap = newTypeMap | |
387 | - return typeInfo | |
388 | -} |
Built with git-ssb-web