@schemd/core 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +19 -11
- package/dist/compiler.d.ts +2 -0
- package/dist/compiler.js +4 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/layout.d.ts +26 -3
- package/dist/layout.js +621 -82
- package/dist/parser.d.ts +1 -1
- package/dist/parser.js +137 -20
- package/dist/renderer.js +96 -88
- package/dist/types.d.ts +4 -0
- package/package.json +8 -2
package/dist/parser.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { type SchematicColor, type SchematicDocument, type SchematicFence } from
|
|
|
12
12
|
/**
|
|
13
13
|
* Assert that a document originated from {@link parseSchematic} in this module.
|
|
14
14
|
*
|
|
15
|
-
* @param document - Candidate AST supplied to
|
|
15
|
+
* @param document - Candidate AST supplied to a parsed-document consumer.
|
|
16
16
|
* @throws {TypeError} When the object was forged, cloned, or parsed by another
|
|
17
17
|
* loaded copy of the package rather than this module instance.
|
|
18
18
|
* @internal
|
package/dist/parser.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { ADDER_TYPES, AMPLIFIER_TYPES, ANALOG_KINDS, BUFFER_TYPES, BUS_TYPES, CLASSICAL_GATE_KINDS, COMPONENT_KINDS, DIGITAL_COMPONENT_KINDS, DIODE_TYPES, ELECTRICAL_COMPONENT_KINDS, FLIPFLOP_TYPES, GROUND_STYLES, LOAD_TYPES, LOGIC_STATES, METER_TYPES, MUX_TYPES, NAMED_QUANTUM_GATE_KINDS, PASSIVE_KINDS, POWER_TYPES, PROTECTION_TYPES, QUANTUM_GATE_KINDS, QUANTUM_SPECIAL_KINDS, RESONATOR_TYPES, SCHEMATIC_ORIENTATIONS, SCHEMATIC_SIGNAL_KINDS, SEMANTIC_COLORS, SCHEMATIC_SIGNAL_MARKERS, SOURCE_TYPES, SWITCH_TYPES, TRANSISTOR_TYPES, UML_COMPONENT_KINDS, UML_RELATION_KINDS, SchematicSyntaxError } from './types.js';
|
|
2
|
-
import { validateDocumentGeometry } from './layout.js';
|
|
2
|
+
import { isClassicalGate, isDigitalComponent, isQuantumSpecial, validateDocumentGeometry } from './layout.js';
|
|
3
3
|
import { mathLabelTextWidth } from './math-label.js';
|
|
4
4
|
import { cacheParsedSchematicRoutes } from './route-cache.js';
|
|
5
5
|
import { MAX_SCHEMATIC_COMPONENTS, MAX_SCHEMATIC_CONNECTIONS, MAX_SCHEMATIC_SOURCE_CHARACTERS } from './limits.js';
|
|
6
6
|
const COMPONENT_PATTERN = /^([A-Za-z][A-Za-z0-9_-]*):([A-Za-z][A-Za-z0-9_-]*)\s+"([^"]+)"\s+at\s+\((-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)\)\s+(.+)$/;
|
|
7
7
|
const CONNECTION_PATTERN = /^([A-Za-z][A-Za-z0-9_-]*)\.([A-Za-z][A-Za-z0-9_+-]*)\s*->\s*([A-Za-z][A-Za-z0-9_-]*)\.([A-Za-z][A-Za-z0-9_+-]*)\s+(.+)$/;
|
|
8
|
-
const SCHEMD_FENCE_PATTERN = /^schemd\s+bounds="(\d+)x(\d+)"(?:\s+title="([^"]+)")?\s*$/i;
|
|
8
|
+
const SCHEMD_FENCE_PATTERN = /^(?:schemd|schematic)\s+bounds="(\d+)x(\d+)"(?:\s+title="([^"]+)")?\s*$/i;
|
|
9
9
|
const NUMBER_PATTERN = /^[+-]?(?:\d+\.?\d*|\.\d+)$/;
|
|
10
10
|
const ANGLE_PATTERN = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:deg|grad|rad|turn)?$/i;
|
|
11
11
|
const ALIAS_PATTERN = /^[a-z][a-z0-9-]{0,63}$/i;
|
|
12
12
|
const PIN_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
13
|
+
const NET_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
13
14
|
const RESERVED_IC_PIN_NAMES = new Set(['in', 'out']);
|
|
14
15
|
const MAX_IC_PINS_PER_SIDE = 64;
|
|
15
16
|
const IC_MINIMUM_WIDTH = 88;
|
|
@@ -58,7 +59,7 @@ function freezeParsedDocument(document) {
|
|
|
58
59
|
}
|
|
59
60
|
export function assertParsedSchematicDocument(document) {
|
|
60
61
|
if (!parsedDocuments.has(document)) {
|
|
61
|
-
throw new TypeError('
|
|
62
|
+
throw new TypeError('This operation requires an immutable document returned by parseSchematic.');
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
function includesValue(values, value) {
|
|
@@ -750,9 +751,10 @@ function parseConnectionOptions(raw, line) {
|
|
|
750
751
|
let label;
|
|
751
752
|
let dashed = false;
|
|
752
753
|
let signalKind;
|
|
754
|
+
let net;
|
|
753
755
|
let width;
|
|
754
756
|
if (raw === undefined || raw.trim() === '') {
|
|
755
|
-
return { curve, markerStart, markerEnd, relation, label, dashed, signalKind, width };
|
|
757
|
+
return { curve, markerStart, markerEnd, relation, label, dashed, signalKind, net, width };
|
|
756
758
|
}
|
|
757
759
|
const seen = new Set();
|
|
758
760
|
for (const token of connectionOptionTokens(raw.trim(), line)) {
|
|
@@ -796,9 +798,9 @@ function parseConnectionOptions(raw, line) {
|
|
|
796
798
|
seen.add('marker-end');
|
|
797
799
|
continue;
|
|
798
800
|
}
|
|
799
|
-
const match = token.match(/^(marker-start|marker-end|relation|label|signal|width)=(.*)$/);
|
|
801
|
+
const match = token.match(/^(marker-start|marker-end|relation|label|signal|net|width)=(.*)$/);
|
|
800
802
|
if (!match) {
|
|
801
|
-
throw new SchematicSyntaxError('Unsupported connection routing, marker, relation, label, or stroke option.', line);
|
|
803
|
+
throw new SchematicSyntaxError('Unsupported connection routing, topology, marker, relation, label, or stroke option.', line);
|
|
802
804
|
}
|
|
803
805
|
const option = match[1];
|
|
804
806
|
if (seen.has(option)) {
|
|
@@ -829,6 +831,12 @@ function parseConnectionOptions(raw, line) {
|
|
|
829
831
|
}
|
|
830
832
|
signalKind = optionValue;
|
|
831
833
|
}
|
|
834
|
+
else if (option === 'net') {
|
|
835
|
+
if (!NET_NAME_PATTERN.test(optionValue)) {
|
|
836
|
+
throw new SchematicSyntaxError('net must begin with a letter and contain at most 64 letters, digits, underscores, or hyphens.', line);
|
|
837
|
+
}
|
|
838
|
+
net = optionValue;
|
|
839
|
+
}
|
|
832
840
|
else if (option === 'width') {
|
|
833
841
|
width = parseWidth(optionValue, 1, line);
|
|
834
842
|
}
|
|
@@ -870,7 +878,7 @@ function parseConnectionOptions(raw, line) {
|
|
|
870
878
|
if (label === undefined && (relation === 'include' || relation === 'extend')) {
|
|
871
879
|
label = `«${relation}»`;
|
|
872
880
|
}
|
|
873
|
-
return { curve, markerStart, markerEnd, relation, label, dashed, signalKind, width };
|
|
881
|
+
return { curve, markerStart, markerEnd, relation, label, dashed, signalKind, net, width };
|
|
874
882
|
}
|
|
875
883
|
function parseConnection(match, line) {
|
|
876
884
|
const tail = splitDeclarationTail(match[5], line);
|
|
@@ -890,6 +898,8 @@ function parseConnection(match, line) {
|
|
|
890
898
|
connection.label = options.label;
|
|
891
899
|
if (options.signalKind !== undefined)
|
|
892
900
|
connection.signalKind = options.signalKind;
|
|
901
|
+
if (options.net !== undefined)
|
|
902
|
+
connection.net = options.net;
|
|
893
903
|
if (options.width !== undefined)
|
|
894
904
|
connection.width = options.width;
|
|
895
905
|
return connection;
|
|
@@ -913,15 +923,6 @@ function validGatePort(component, port) {
|
|
|
913
923
|
? index <= component.inputs && index >= 1
|
|
914
924
|
: index <= component.outputs && index >= 1;
|
|
915
925
|
}
|
|
916
|
-
function isClassicalGate(component) {
|
|
917
|
-
return CLASSICAL_GATE_KINDS.includes(component.kind);
|
|
918
|
-
}
|
|
919
|
-
function isDigitalComponent(component) {
|
|
920
|
-
return DIGITAL_COMPONENT_KINDS.includes(component.kind);
|
|
921
|
-
}
|
|
922
|
-
function isQuantumSpecial(component) {
|
|
923
|
-
return QUANTUM_SPECIAL_KINDS.includes(component.kind);
|
|
924
|
-
}
|
|
925
926
|
function validIndexedPort(port, inputs, outputs) {
|
|
926
927
|
if (port === 'in')
|
|
927
928
|
return inputs > 0;
|
|
@@ -1119,8 +1120,119 @@ function validateConnectionWidth(connection, components) {
|
|
|
1119
1120
|
throw new SchematicSyntaxError(`Connection width ${width} is incompatible with ${sourceWidth}-bit source and ${targetWidth}-bit target ports.`, connection.line);
|
|
1120
1121
|
}
|
|
1121
1122
|
}
|
|
1123
|
+
function assignConnectionNetIds(connections) {
|
|
1124
|
+
const parent = connections.map((_, index) => index);
|
|
1125
|
+
const find = (index) => {
|
|
1126
|
+
let root = index;
|
|
1127
|
+
while (parent[root] !== root)
|
|
1128
|
+
root = parent[root];
|
|
1129
|
+
while (parent[index] !== index) {
|
|
1130
|
+
const next = parent[index];
|
|
1131
|
+
parent[index] = root;
|
|
1132
|
+
index = next;
|
|
1133
|
+
}
|
|
1134
|
+
return root;
|
|
1135
|
+
};
|
|
1136
|
+
const union = (left, right) => {
|
|
1137
|
+
const leftRoot = find(left);
|
|
1138
|
+
const rightRoot = find(right);
|
|
1139
|
+
if (leftRoot !== rightRoot)
|
|
1140
|
+
parent[rightRoot] = leftRoot;
|
|
1141
|
+
};
|
|
1142
|
+
const terminalOwner = new Map();
|
|
1143
|
+
const namedOwner = new Map();
|
|
1144
|
+
for (const [index, connection] of connections.entries()) {
|
|
1145
|
+
if (connection.relation !== 'signal') {
|
|
1146
|
+
if (connection.net !== undefined) {
|
|
1147
|
+
throw new SchematicSyntaxError('Only signal connections may declare a net.', connection.line);
|
|
1148
|
+
}
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
for (const endpoint of [connection.from, connection.to]) {
|
|
1152
|
+
const key = `${endpoint.componentId}.${endpoint.port}`;
|
|
1153
|
+
const owner = terminalOwner.get(key);
|
|
1154
|
+
if (owner === undefined)
|
|
1155
|
+
terminalOwner.set(key, index);
|
|
1156
|
+
else
|
|
1157
|
+
union(index, owner);
|
|
1158
|
+
}
|
|
1159
|
+
if (connection.net !== undefined) {
|
|
1160
|
+
const owner = namedOwner.get(connection.net);
|
|
1161
|
+
if (owner === undefined)
|
|
1162
|
+
namedOwner.set(connection.net, index);
|
|
1163
|
+
else
|
|
1164
|
+
union(index, owner);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
const names = new Map();
|
|
1168
|
+
const contracts = new Map();
|
|
1169
|
+
for (const [index, connection] of connections.entries()) {
|
|
1170
|
+
if (connection.relation !== 'signal')
|
|
1171
|
+
continue;
|
|
1172
|
+
const root = find(index);
|
|
1173
|
+
if (connection.net !== undefined) {
|
|
1174
|
+
const existing = names.get(root);
|
|
1175
|
+
if (existing !== undefined && existing !== connection.net) {
|
|
1176
|
+
throw new SchematicSyntaxError(`Terminal joins conflicting nets ${existing} and ${connection.net}.`, connection.line);
|
|
1177
|
+
}
|
|
1178
|
+
names.set(root, connection.net);
|
|
1179
|
+
}
|
|
1180
|
+
const signalKind = connection.signalKind ?? 'electrical';
|
|
1181
|
+
const width = connection.width ?? 1;
|
|
1182
|
+
const contract = contracts.get(root);
|
|
1183
|
+
if (contract !== undefined &&
|
|
1184
|
+
(contract.signalKind !== signalKind || contract.width !== width)) {
|
|
1185
|
+
throw new SchematicSyntaxError(`Net segments must share one signal kind and width; expected ${contract.signalKind} width ${contract.width}.`, connection.line);
|
|
1186
|
+
}
|
|
1187
|
+
contracts.set(root, { signalKind, width });
|
|
1188
|
+
}
|
|
1189
|
+
const resolved = new Map();
|
|
1190
|
+
let generated = 0;
|
|
1191
|
+
for (const [index, connection] of connections.entries()) {
|
|
1192
|
+
if (connection.relation !== 'signal')
|
|
1193
|
+
continue;
|
|
1194
|
+
const root = find(index);
|
|
1195
|
+
let netId = resolved.get(root);
|
|
1196
|
+
if (netId === undefined) {
|
|
1197
|
+
netId = names.get(root) ?? `$${++generated}`;
|
|
1198
|
+
resolved.set(root, netId);
|
|
1199
|
+
}
|
|
1200
|
+
connection.netId = netId;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
function normalizeParserFence(value) {
|
|
1204
|
+
if (typeof value !== 'object' || value === null) {
|
|
1205
|
+
throw new SchematicSyntaxError('Parser options must be an object.');
|
|
1206
|
+
}
|
|
1207
|
+
const candidate = value;
|
|
1208
|
+
const rawBounds = candidate.bounds;
|
|
1209
|
+
if (typeof rawBounds !== 'object' || rawBounds === null) {
|
|
1210
|
+
throw new SchematicSyntaxError('Parser options require bounds.');
|
|
1211
|
+
}
|
|
1212
|
+
const bounds = rawBounds;
|
|
1213
|
+
const width = bounds.width;
|
|
1214
|
+
const height = bounds.height;
|
|
1215
|
+
const title = candidate.title;
|
|
1216
|
+
if (typeof width !== 'number' ||
|
|
1217
|
+
!Number.isInteger(width) ||
|
|
1218
|
+
typeof height !== 'number' ||
|
|
1219
|
+
!Number.isInteger(height) ||
|
|
1220
|
+
width < 64 ||
|
|
1221
|
+
height < 64 ||
|
|
1222
|
+
width > 4096 ||
|
|
1223
|
+
height > 4096) {
|
|
1224
|
+
throw new SchematicSyntaxError('Schematic bounds must be integers from 64 through 4096.');
|
|
1225
|
+
}
|
|
1226
|
+
if (typeof title !== 'string' || title.trim() === '') {
|
|
1227
|
+
throw new SchematicSyntaxError('Schematic titles cannot be empty.');
|
|
1228
|
+
}
|
|
1229
|
+
if (title.length > MAX_FENCE_TITLE_LENGTH) {
|
|
1230
|
+
throw new SchematicSyntaxError('Schematic titles cannot exceed 512 characters.');
|
|
1231
|
+
}
|
|
1232
|
+
return { bounds: { width, height }, title };
|
|
1233
|
+
}
|
|
1122
1234
|
export function parseSchematicFence(info, defaultTitle = 'Engineering schematic') {
|
|
1123
|
-
if (info === undefined || !/^schemd(?:\s|$)/i.test(info.trim())) {
|
|
1235
|
+
if (info === undefined || !/^(?:schemd|schematic)(?:\s|$)/i.test(info.trim())) {
|
|
1124
1236
|
return undefined;
|
|
1125
1237
|
}
|
|
1126
1238
|
const match = info.trim().match(SCHEMD_FENCE_PATTERN);
|
|
@@ -1142,9 +1254,13 @@ export function parseSchematicFence(info, defaultTitle = 'Engineering schematic'
|
|
|
1142
1254
|
return { bounds: { width, height }, title };
|
|
1143
1255
|
}
|
|
1144
1256
|
export function parseSchematic(source, fence) {
|
|
1257
|
+
if (typeof source !== 'string') {
|
|
1258
|
+
throw new SchematicSyntaxError('Schematic source must be a string.');
|
|
1259
|
+
}
|
|
1145
1260
|
if (source.length > MAX_SCHEMATIC_SOURCE_CHARACTERS) {
|
|
1146
1261
|
throw new SchematicSyntaxError('Schematic source exceeds the 131,072 character limit.');
|
|
1147
1262
|
}
|
|
1263
|
+
const normalizedFence = normalizeParserFence(fence);
|
|
1148
1264
|
const components = [];
|
|
1149
1265
|
const connections = [];
|
|
1150
1266
|
const componentIds = new Set();
|
|
@@ -1162,7 +1278,7 @@ export function parseSchematic(source, fence) {
|
|
|
1162
1278
|
if (componentIds.has(component.id)) {
|
|
1163
1279
|
throw new SchematicSyntaxError(`Duplicate component ID ${component.id}.`, lineNumber);
|
|
1164
1280
|
}
|
|
1165
|
-
validateComponent(component,
|
|
1281
|
+
validateComponent(component, normalizedFence);
|
|
1166
1282
|
componentIds.add(component.id);
|
|
1167
1283
|
components.push(component);
|
|
1168
1284
|
continue;
|
|
@@ -1186,9 +1302,10 @@ export function parseSchematic(source, fence) {
|
|
|
1186
1302
|
validateEndpoint(connection.to, componentsById, connection.line);
|
|
1187
1303
|
validateConnectionWidth(connection, componentsById);
|
|
1188
1304
|
}
|
|
1305
|
+
assignConnectionNetIds(connections);
|
|
1189
1306
|
const document = { components, connections };
|
|
1190
|
-
const routes = validateDocumentGeometry(document,
|
|
1307
|
+
const routes = validateDocumentGeometry(document, normalizedFence);
|
|
1191
1308
|
const parsedDocument = freezeParsedDocument(document);
|
|
1192
|
-
cacheParsedSchematicRoutes(parsedDocument,
|
|
1309
|
+
cacheParsedSchematicRoutes(parsedDocument, normalizedFence.bounds, routes);
|
|
1193
1310
|
return parsedDocument;
|
|
1194
1311
|
}
|