const dot = {}; dot.ModelFactory = class { async match(context) { const reader = await context.read('text', 0x10000); if (reader) { try { for (let i = 0; i < 64; i++) { const line = reader.read('\n'); if (line === undefined) { break; } if (line.trim().startsWith('//') || line.trim().startsWith('#')) { continue; } if (line.trim().match(/^(strict)?\s*digraph/)) { return context.set('dot'); } } } catch { // continue regardless of error } } return null; } async open(context) { const decoder = await context.read('text.decoder'); const parser = new dot.Parser(decoder); const graph = parser.parse(); if (graph.kind !== 'digraph') { throw new dot.Error(`Graph type '${graph.type}' is not supported.`); } return new dot.Model(graph); } }; dot.Model = class { constructor(graph) { this.format = 'DOT'; this.modules = [new dot.Graph(graph)]; } }; dot.Graph = class { constructor(graph) { this.name = graph.name || ''; this.nodes = []; this.inputs = []; this.outputs = []; const values = new Map(); values.map = (name, type, tensor, metadata) => { if (typeof name !== 'string') { throw new dot.Error('Invalid value name.'); } if (!values.has(name) || tensor) { values.set(name, new dot.Value(name, type, tensor, metadata)); } return values.get(name); }; const nodes = new Map(); nodes.map = (name) => { if (typeof name !== 'string') { throw new dot.Error('Invalid node name.'); } if (!nodes.has(name)) { const node = { kind: 'node', name: { id: name, key: name }, type: { name }, inputs: [], outputs: [], attributes: new Map(), metadata: new Map() }; nodes.set(name, node); } return nodes.get(name); }; for (const node of graph.statements) { if (node.kind === 'node') { node.inputs = []; node.outputs = []; node.metadata = new Map([...node.defaults, ...node.attributes]); node.attributes = new Map(); delete node.defaults; const metadata = node.metadata; if (metadata.has('label')) { const label = metadata.get('label'); if (label.startsWith('{') && label.endsWith('}')) { const lines = label.substring(1, label.length - 1).split('|'); if (lines.length > 1 && node.name.id === lines[0] && lines[1].startsWith('op_code=')) { const def = lines[1].split('\\l'); const op_code = def[0].split('=').pop(); node.type = { name: op_code }; if (op_code === 'call_module') { node.type = { name: def[1], type: 'function' }; } else if (op_code === 'call_function') { const vals = lines[2].split('\\l'); node.type = { name: vals[0] }; } else if (op_code.startsWith('get_parameter')) { node.attributes.set('type', op_code.substring(13, op_code.length).trim()); node.type = { name: 'get_parameter' }; } if (lines.length > 2) { const attributes = lines[2].split('\\l'); for (const attribute of attributes) { const parts = attribute.split(':'); if (parts.length === 2) { const key = parts[0].trim(); let value = parts[1].trim(); if (value.startsWith('(') && value.endsWith(')')) { value = JSON.parse(`[${value.substring(1, value.length - 1)}]`); } node.attributes.set(key, value); } } } metadata.delete('label'); } else if (lines.length === 1 && lines[0].startsWith('buffer\\l')) { const def = lines[0].split('\\l'); node.type = { name: def[0] }; if (def.length > 1) { node.attributes.set('type', def[1]); } metadata.delete('label'); } } else { const match = label.match(/^name:\s*([A-Za-z][A-Za-z0-9_]*)\stype:\s*([A-Za-z][A-Za-z0-9_]*)$/); if (match && node.name.id === match[1]) { node.type = { name: match[2] }; metadata.delete('label'); } } } if (!node.type) { const lines = node.name.id.split('\\n'); const match = lines[0].match(/^([A-Z][A-Za-z0-9_]*)\/([A-Z][A-Za-z0-9_]*)\s\(op#(\d+)\)$/); if (match) { node.type = { name: match[2] }; } else { const match = lines[0].match(/^([A-Z][A-Za-z0-9_]*)\s\(op#(\d+)\)$/); if (match) { node.type = { name: match[1] }; } else { // debugger; } } } if (!node.type) { node.type = { name: node.name.id }; } nodes.set(node.name.id, node); } } for (const edge of graph.statements) { if (edge.kind === 'edge') { edge.uses = edge.uses || []; const to = nodes.map(edge.to.id); to.inputs.push(edge); edge.uses.push(to); edge.from = nodes.map(edge.name.id); edge.from.outputs.push(edge); } } for (const [key, node] of nodes) { const keys = new Set(['pos', 'height', 'width', 'shape', 'label']); if (node.metadata.get('shape') === 'octagon' && node.metadata.keys().every((key) => keys.has(key)) && node.inputs.length === 1 && node.inputs[0].uses.length === 1 && node.inputs[0].from.outputs.length === 1 && node.inputs[0].from.outputs[0].uses.length === 1 && new Set(node.outputs.map((output) => output.name.id)).size === 1 && node.outputs.every((output) => output.uses.length === 1)) { const [from] = node.inputs[0].from.outputs; for (const e of node.outputs) { const [n] = e.uses; n.inputs = n.inputs.map((edge) => edge === e ? from : edge); } nodes.delete(key); } } for (const [key, node] of nodes) { if ((node.type.name === 'get_parameter' || node.type.name === 'buffer' || node.type.name === 'Constant') && node.inputs.length === 0 && node.outputs.length === 1 && node.outputs[0].uses.length === 1) { node.outputs[0].initializer = node; nodes.delete(key); } } for (const [, obj] of nodes) { const node = new dot.Node(obj, values); this.nodes.push(node); } for (const edge of graph.statements) { if (edge.kind === 'edge') { const value = values.map(edge.name.id); const metadata = new Map([...edge.defaults, ...edge.attributes]); value.metadata = Array.from(metadata).map(([key, value]) => new dot.Argument(key, value)); } } } }; dot.Argument = class { constructor(name, value, type = null) { this.name = name; this.value = value; this.type = type; } }; dot.Value = class { constructor(name, type, initializer, metadata) { this.name = name; this.type = !type && initializer ? initializer.type : type; this.initializer = initializer || null; this.metadata = metadata; } }; dot.Node = class { constructor(node, values) { this.name = node.name.key; this.type = node.type; this.inputs = []; this.outputs = []; this.attributes = []; this.metadata = []; for (let i = 0; i < node.inputs.length; i++) { const edge = node.inputs[i]; const initializer = edge.initializer ? new dot.Tensor(edge.initializer) : null; const value = values.map(edge.name.key, null, initializer); const argument = new dot.Argument(i.toString(), [value]); this.inputs.push(argument); } for (let i = 0; i < node.outputs.length; i++) { const edge = node.outputs[i]; const value = values.map(edge.name.key); const argument = new dot.Argument(i.toString(), [value]); this.outputs.push(argument); } for (const [name, value] of node.attributes) { const argument = new dot.Argument(name, value, 'attribute'); this.attributes.push(argument); } for (const [name, value] of node.metadata) { const argument = new dot.Argument(name, value); this.metadata.push(argument); } } }; dot.TensorType = class { constructor(type) { const index = type.indexOf('['); const dtype = type.substring(0, index); this.dataType = dtype.split('.').pop(); if (index > 0) { const dimensions = JSON.parse(type.substring(index, type.length)); this.shape = new dot.TensorShape(dimensions); } else { this.shape = new dot.TensorShape([]); } } toString() { return this.dataType + this.shape.toString(); } }; dot.TensorShape = class { constructor(dimensions) { this.dimensions = dimensions; } toString() { if (this.dimensions && this.dimensions.length > 0) { return `[${this.dimensions.map((dimension) => dimension.toString()).join(',')}]`; } return ''; } }; dot.Tensor = class { constructor(stmt) { if (stmt.attributes.has('type')) { const type = stmt.attributes.get('type'); this.type = new dot.TensorType(type); } else { this.type = new dot.TensorType('?'); } } }; dot.Parser = class { constructor(decoder) { // https://graphviz.org/doc/info/lang.html this._tokenizer = new dot.Tokenizer(decoder); this._token = this._tokenizer.read(); } parse() { const graph = {}; if (this._eat('id', 'strict')) { graph.strict = true; } let edgeop = ''; if (this._match('id', 'graph')) { graph.kind = this._read(); edgeop = '--'; } else if (this._match('id', 'digraph')) { graph.kind = this._read(); edgeop = '->'; } else { throw new dot.Error('Invalid graph type.'); } if (this._match('id')) { graph.name = this._read(); } const defaults = {}; defaults.graph = new Map(); defaults.node = new Map(); defaults.edge = new Map(); graph.statements = this._parseBlock(defaults, edgeop, 0); graph.defaults = new Map(defaults.graph); return graph; } _parseBlock(defaults, edgeop) { defaults = { graph: new Map(defaults.graph), node: new Map(defaults.node), edge: new Map(defaults.edge) }; const list = []; this._read('{'); while (!this._match('}')) { if (this._eat('id', 'subgraph')) { const stmt = {}; stmt.kind = 'subgraph'; if (this._match('id')) { stmt.name = this._read(); } stmt.statements = this._parseBlock(defaults, edgeop); } else if (this._match('{')) { const stmt = {}; const statements = this._parseBlock(defaults, edgeop); if (this._eat(edgeop)) { if (!statements.every((stmt) => stmt.kind === 'node' && stmt.attributes.size === 0)) { throw new dot.Error('Invalid edge group statement.'); } const sources = statements.map((stmt) => stmt.name); list.push(...this._parseEdges(sources, edgeop, defaults.edge)); } else { stmt.kind = 'subgraph'; stmt.statements = statements; } } else if (this._match('id')) { const name = this._parseNodeId(); if (this._eat('=')) { // attr if (this._match('id')) { const value = this._read(); defaults.graph.set(name, value); } else { throw new dot.Error('Invalid attribute value.'); } } else if (this._eat(edgeop)) { list.push(...this._parseEdges([name], edgeop, defaults.edge)); } else { const attributes = this._parseAttributes(); if (name.key === 'node' || name.key === 'edge' || name.key === 'graph') { for (const [key, value] of attributes) { defaults[name.key].set(key, value); } } else { list.push({ kind: 'node', name, attributes, defaults: new Map(defaults.node) }); } } } if (this._match(';') || this._match(',')) { this._read(); } } this._read('}'); return list; } _parseNodeIds() { const list = []; const open = this._eat('{'); while (!this._match('}')) { const value = this._parseNodeId(); list.push(value); if (this._match(',')) { this._read(); continue; } else if (this._match(';')) { this._read(); if (!open) { break; } } else if (!open) { break; } } if (open) { this._read('}'); } return list; } _parseNodeId() { const name = {}; const list = []; name.id = this._read('id'); list.push(name.id); if (this._eat(':')) { name.port = this._read('id'); list.push(name.port); if (this._eat(':')) { name.compass_pt = this._read('id'); list.push(name.compass_pt); } } name.key = list.join(':'); return name; } _parseAttributes() { const table = new Map(); if (this._eat('[')) { while (this._match('id')) { const name = this._read('id'); this._read('='); const value = this._read('id'); table.set(name, value); if (this._match(';') || this._match(',')) { this._read(); } } this._read(']'); } return table; } _parseEdges(sources, edgeop, defaults) { const list = []; do { const targets = this._parseNodeIds(); for (const name of sources) { for (const to of targets) { list.push({ kind: 'edge', name, to }); } } sources = targets; } while (this._eat(edgeop)); const attributes = this._parseAttributes(); for (const edge of list) { edge.attributes = attributes; edge.defaults = new Map(defaults.edge); } return list; } _match(kind, value) { return (this._token.kind === kind && (!value || this._token.value === value)); } _read(kind, value) { if (kind && this._token.kind !== kind) { throw new dot.Error(`Expected token of type '${kind}', but got '${this._token.kind}' ${this._tokenizer.location()}`); } if (value && this._token.value !== value) { throw new dot.Error(`Expected token with value '${value}', but got '${this._token.value}' ${this._tokenizer.location()}`); } const token = this._token; this._token = this._tokenizer.read(); return token.value; } _eat(kind, value) { if (this._match(kind, value)) { return this._read(); } return null; } }; dot.Tokenizer = class { constructor(decoder) { this._decoder = decoder; this._position = 0; this._char = this._decoder.decode(); } _read() { if (this._char === undefined) { this._unexpected(); } const char = this._char; this._position = this._decoder.position; this._char = this._decoder.decode(); return char; } _peek() { const position = this._decoder.position; const char = this._decoder.decode(); this._decoder.position = position; return char; } read() { while (this._char) { if (/\s/.test(this._char)) { this._skipWhitespace(); continue; } if (this._char === '/' || this._char === '#') { this._skipComment(); continue; } if (/[{}[\]=:;,]/.test(this._char)) { const value = this._read(); return { kind: value, value }; } else if (this._char === '-') { let value = this._read(); if (this._char === '>' || this._char === '-') { value += this._read(); return { kind: value, value }; } throw new dot.Error(`Unexpected character '${value}' ${this.location()}`); } else if (/[a-zA-Z0-9_$"<]/.test(this._char)) { const value = this._identifier(); return { kind: 'id', value }; } else { throw new dot.Error(`Unexpected character '${this._char}' ${this.location()}`); } } return { type: 'eof' }; } _skipWhitespace() { while (this._char !== undefined && /\s/.test(this._char)) { this._read(); } } _skipComment() { if (this._char === '#' || (this._char === '/' && this._peek() === '/')) { while (this._char && this._char !== '\n') { this._read(); } return; } if (this._char === '/' && this._peek() === '*') { while (this._char && (this._char !== '*' || this._peek() !== '/')) { this._read(); } this._read(); this._read(); return; } throw new dot.Error('Invalid comment.'); } _identifier() { let value = ''; if (this._char === '"') { // double quoted string this._read(); while (this._char && this._char !== '"') { value += this._read(); } this._read('"'); } if (this._char === '<') { // HTML String value += this._read(); let depth = 0; while (depth > 0 || this._char !== '>') { const c = this._read(); value += c; if (c === '<') { depth += 1; } else if (c === '>') { depth -= 1; } } value += this._read(); } else { while (/[a-zA-Z0-9_$.*]/.test(this._char)) { value += this._read(); } } return value; } _unexpected() { let c = this._char; if (c === undefined) { throw new dot.Error('Unexpected end of input.'); } else if (c === '"') { c = 'string'; } else if ((c >= '0' && c <= '9') || c === '-') { c = 'number'; } else { if (c < ' ' || c > '\x7F') { const name = Object.keys(this._escape).filter((key) => this._escape[key] === c); c = (name.length === 1) ? `\\${name}` : `\\u${(`000${c.charCodeAt(0).toString(16)}`).slice(-4)}`; } c = `token '${c}'`; } this._throw(`Unexpected ${c}`); } _throw(message) { message = message.replace(/\.$/, ''); throw new dot.Error(`${message} ${this._location()}`); } location() { let line = 1; let column = 1; const position = this._decoder.position; this._decoder.position = 0; let c = ''; do { if (this._decoder.position === this._position) { this._decoder.position = position; return `at ${line}:${column}.`; } c = this._decoder.decode(); if (c === '\n') { line++; column = 1; } else { column++; } } while (c !== undefined); this._decoder.position = position; return `at ${line}:${column}.`; } }; dot.Error = class extends Error { constructor(message) { super(message); this.name = 'Error loadig DOT graph'; } }; export const ModelFactory = dot.ModelFactory;