@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/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 the renderer.
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('renderSchematic requires an immutable document returned by parseSchematic.');
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, fence);
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, fence);
1307
+ const routes = validateDocumentGeometry(document, normalizedFence);
1191
1308
  const parsedDocument = freezeParsedDocument(document);
1192
- cacheParsedSchematicRoutes(parsedDocument, fence.bounds, routes);
1309
+ cacheParsedSchematicRoutes(parsedDocument, normalizedFence.bounds, routes);
1193
1310
  return parsedDocument;
1194
1311
  }