@aryacode/dsh-engineering 0.5.0 → 0.7.0
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 +52 -0
- package/README.md +9 -8
- package/cordis.patch.yml +1 -1
- package/entries/netlogic.js +266 -0
- package/entries/optix.js +13 -0
- package/package.json +10 -10
- package/vendor/l5x/SKILL.md +27 -7
- package/vendor/l5x/lib/engine/change-set.d.ts +9 -1
- package/vendor/l5x/lib/engine/change-set.js +32 -7
- package/vendor/l5x/lib/engine/documentation.d.ts +58 -0
- package/vendor/l5x/lib/engine/documentation.js +237 -0
- package/vendor/l5x/lib/engine/instructions.d.ts +50 -0
- package/vendor/l5x/lib/engine/instructions.js +107 -0
- package/vendor/l5x/lib/engine/ladder.d.ts +74 -0
- package/vendor/l5x/lib/engine/ladder.js +209 -0
- package/vendor/l5x/lib/index.d.ts +1 -0
- package/vendor/l5x/lib/index.js +3 -1
- package/vendor/l5x/lib/model/routines.js +6 -1
- package/vendor/l5x/lib/session.d.ts +20 -0
- package/vendor/l5x/lib/session.js +36 -0
- package/vendor/l5x/lib/tools/document.d.ts +18 -0
- package/vendor/l5x/lib/tools/document.js +119 -0
- package/vendor/l5x/lib/tools/drafts.js +109 -3
- package/vendor/optix/SKILL.md +68 -0
- package/vendor/optix/netlogic/SOURCE.json +12 -0
- package/vendor/optix/netlogic/index.json +1987 -0
- package/vendor/optix/netlogic/pages/accessing-project-nodes.md +113 -0
- package/vendor/optix/netlogic/pages/advanced-svg.md +124 -0
- package/vendor/optix/netlogic/pages/alarming.md +489 -0
- package/vendor/optix/netlogic/pages/async-tasks.md +236 -0
- package/vendor/optix/netlogic/pages/audit-signature.md +91 -0
- package/vendor/optix/netlogic/pages/colors.md +122 -0
- package/vendor/optix/netlogic/pages/companion-specs.md +77 -0
- package/vendor/optix/netlogic/pages/creating-objects.md +370 -0
- package/vendor/optix/netlogic/pages/database-interaction.md +437 -0
- package/vendor/optix/netlogic/pages/datagrids.md +123 -0
- package/vendor/optix/netlogic/pages/debugging-tips.md +605 -0
- package/vendor/optix/netlogic/pages/designtime-tags-import.md +531 -0
- package/vendor/optix/netlogic/pages/dialog-boxes.md +150 -0
- package/vendor/optix/netlogic/pages/dispatch-events.md +200 -0
- package/vendor/optix/netlogic/pages/dynamic-links.md +825 -0
- package/vendor/optix/netlogic/pages/element-access.md +1208 -0
- package/vendor/optix/netlogic/pages/engineering-units.md +298 -0
- package/vendor/optix/netlogic/pages/events.md +340 -0
- package/vendor/optix/netlogic/pages/execute-command.md +202 -0
- package/vendor/optix/netlogic/pages/fto-studio-cli.md +99 -0
- package/vendor/optix/netlogic/pages/good-practices.md +50 -0
- package/vendor/optix/netlogic/pages/information-model.md +89 -0
- package/vendor/optix/netlogic/pages/log-output.md +22 -0
- package/vendor/optix/netlogic/pages/managing-aliases.md +22 -0
- package/vendor/optix/netlogic/pages/manipulate-nodes.md +72 -0
- package/vendor/optix/netlogic/pages/mqtt.md +240 -0
- package/vendor/optix/netlogic/pages/netlogic-lifecycle.md +270 -0
- package/vendor/optix/netlogic/pages/netlogic-overview.md +155 -0
- package/vendor/optix/netlogic/pages/opcua.md +325 -0
- package/vendor/optix/netlogic/pages/plc-tags.md +219 -0
- package/vendor/optix/netlogic/pages/queries.md +1297 -0
- package/vendor/optix/netlogic/pages/random-generation.md +90 -0
- package/vendor/optix/netlogic/pages/recipes.md +375 -0
- package/vendor/optix/netlogic/pages/recipex.md +2383 -0
- package/vendor/optix/netlogic/pages/regex.md +15 -0
- package/vendor/optix/netlogic/pages/register-observers.md +346 -0
- package/vendor/optix/netlogic/pages/reports.md +141 -0
- package/vendor/optix/netlogic/pages/resource-uri.md +308 -0
- package/vendor/optix/netlogic/pages/runtime-tags-import.md +280 -0
- package/vendor/optix/netlogic/pages/serial-port.md +357 -0
- package/vendor/optix/netlogic/pages/sessions.md +58 -0
- package/vendor/optix/netlogic/pages/system-node.md +153 -0
- package/vendor/optix/netlogic/pages/template-library.md +127 -0
- package/vendor/optix/netlogic/pages/threading.md +236 -0
- package/vendor/optix/netlogic/pages/translations.md +417 -0
- package/vendor/optix/netlogic/pages/trends.md +128 -0
- package/vendor/optix/netlogic/pages/ui-animations.md +115 -0
- package/vendor/optix/netlogic/pages/users-groups.md +416 -0
- package/vendor/optix/netlogic/pages/variables-formatting.md +16 -0
- package/vendor/optix/netlogic/pages/variables-interaction.md +557 -0
- package/vendor/optix/netlogic/pages/variables-structure.md +155 -0
|
@@ -9,12 +9,13 @@ import { uniqueRegion, xmlRegions } from "./xml-regions.js";
|
|
|
9
9
|
/** Scalar types the plugin can create with a literal initial value. */
|
|
10
10
|
const SCALAR_TYPES = new Set(['BOOL', 'SINT', 'INT', 'DINT', 'LINT', 'USINT', 'UINT', 'UDINT', 'ULINT', 'REAL', 'LREAL']);
|
|
11
11
|
/** Operation names accepted by a version-1 change set. */
|
|
12
|
-
const SUPPORTED_OPERATIONS = ['replace_rung', 'insert_rung', 'delete_rung', 'replace_st', 'add_routine', 'add_tag', 'set_tag_description', 'add_udt_member', 'set_udt_description', 'set_udt_member_description'];
|
|
12
|
+
const SUPPORTED_OPERATIONS = ['replace_rung', 'insert_rung', 'delete_rung', 'set_rung_comment', 'replace_st', 'add_routine', 'add_tag', 'set_tag_description', 'add_udt_member', 'set_udt_description', 'set_udt_member_description'];
|
|
13
13
|
/** Compact contract returned with validation errors and embedded in tool descriptions. */
|
|
14
14
|
export const CHANGE_SET_HELP = `Operations (each also needs "operation"):
|
|
15
|
-
replace_rung {program,routine,rung,text}
|
|
16
|
-
insert_rung {program,routine,rung,text,before_rung?}
|
|
15
|
+
replace_rung {program,routine,rung,text,comment?}
|
|
16
|
+
insert_rung {program,routine,rung,text,before_rung?,comment?}
|
|
17
17
|
delete_rung {program,routine,rung}
|
|
18
|
+
set_rung_comment {program,routine,rung,comment}
|
|
18
19
|
replace_st {program,routine,code}
|
|
19
20
|
add_routine {program,name,language:"RLL"|"ST",code}
|
|
20
21
|
add_tag {scope,name,data_type,description,value? scalar, dimensions? "4 2", external_access? "Read/Write"|"Read Only"|"None"}
|
|
@@ -104,17 +105,21 @@ function parseOne(value, declaredTypes) {
|
|
|
104
105
|
case 'replace_rung':
|
|
105
106
|
case 'insert_rung':
|
|
106
107
|
case 'delete_rung': {
|
|
107
|
-
fields(row, row.operation === 'delete_rung' ? ['operation', 'program', 'routine', 'rung'] : row.operation === 'insert_rung' ? ['operation', 'program', 'routine', 'rung', 'text', 'before_rung'] : ['operation', 'program', 'routine', 'rung', 'text']);
|
|
108
|
+
fields(row, row.operation === 'delete_rung' ? ['operation', 'program', 'routine', 'rung'] : row.operation === 'insert_rung' ? ['operation', 'program', 'routine', 'rung', 'text', 'before_rung', 'comment'] : ['operation', 'program', 'routine', 'rung', 'text', 'comment']);
|
|
108
109
|
const selected = { program: identifier(row, 'program'), routine: identifier(row, 'routine'), rung: integer(row, 'rung') };
|
|
109
110
|
if (row.operation === 'delete_rung')
|
|
110
111
|
return { operation: row.operation, ...selected };
|
|
111
112
|
const source = text(row, 'text');
|
|
112
113
|
if (parseLadder(source).issues.length > 0)
|
|
113
114
|
throw new EngineeringError('INVALID_LADDER', 'Rung contains malformed or unsupported NeutralText structure');
|
|
115
|
+
const comment = row.comment === undefined ? {} : { comment: text(row, 'comment') };
|
|
114
116
|
if (row.operation === 'replace_rung')
|
|
115
|
-
return { operation: row.operation, ...selected, text: source };
|
|
116
|
-
return { operation: row.operation, ...selected, text: source, ...(row.before_rung === undefined ? {} : { before_rung: integer(row, 'before_rung') }) };
|
|
117
|
+
return { operation: row.operation, ...selected, text: source, ...comment };
|
|
118
|
+
return { operation: row.operation, ...selected, text: source, ...comment, ...(row.before_rung === undefined ? {} : { before_rung: integer(row, 'before_rung') }) };
|
|
117
119
|
}
|
|
120
|
+
case 'set_rung_comment':
|
|
121
|
+
fields(row, ['operation', 'program', 'routine', 'rung', 'comment']);
|
|
122
|
+
return { operation: row.operation, program: identifier(row, 'program'), routine: identifier(row, 'routine'), rung: integer(row, 'rung'), comment: text(row, 'comment') };
|
|
118
123
|
case 'replace_st':
|
|
119
124
|
fields(row, ['operation', 'program', 'routine', 'code']);
|
|
120
125
|
return { operation: row.operation, program: identifier(row, 'program'), routine: identifier(row, 'routine'), code: text(row, 'code') };
|
|
@@ -314,7 +319,7 @@ function applyOperation(xml, operation, context) {
|
|
|
314
319
|
if (operation.operation === 'insert_rung') {
|
|
315
320
|
if (targets.length !== 0)
|
|
316
321
|
throw new EngineeringError('NAME_COLLISION', 'Rung number already exists; no automatic renumbering is performed');
|
|
317
|
-
const newRung = `<Rung Number="${operation.rung}" Type="N"
|
|
322
|
+
const newRung = `<Rung Number="${operation.rung}" Type="N">${commentXml(operation.comment)}<Text>${cdata(operation.text)}</Text></Rung>`;
|
|
318
323
|
if (operation.before_rung === undefined)
|
|
319
324
|
return insert(xml, content, newRung);
|
|
320
325
|
const before = rungs.filter(rung => Number(rung.attributes.Number) === operation.before_rung);
|
|
@@ -327,9 +332,29 @@ function applyOperation(xml, operation, context) {
|
|
|
327
332
|
const target = targets[0];
|
|
328
333
|
if (operation.operation === 'delete_rung')
|
|
329
334
|
return replace(xml, target.start, target.end, '');
|
|
335
|
+
if (operation.operation === 'set_rung_comment') {
|
|
336
|
+
return replace(xml, target.start, target.end, rungWith(target, retainedText(xml, target), operation.comment));
|
|
337
|
+
}
|
|
338
|
+
if (operation.comment !== undefined) {
|
|
339
|
+
return replace(xml, target.start, target.end, rungWith(target, `<Text>${cdata(operation.text)}</Text>`, operation.comment));
|
|
340
|
+
}
|
|
330
341
|
const contentText = uniqueRegion(target.children, 'Text');
|
|
331
342
|
return replace(xml, contentText.start, contentText.end, `<Text>${cdata(operation.text)}</Text>`);
|
|
332
343
|
}
|
|
344
|
+
/** A comment element, or nothing when the comment is cleared. */
|
|
345
|
+
function commentXml(comment) {
|
|
346
|
+
return comment === undefined || comment === '' ? '' : `<Comment>${cdata(comment)}</Comment>`;
|
|
347
|
+
}
|
|
348
|
+
/** The rung's existing `<Text>` element, verbatim, for a comment-only edit. */
|
|
349
|
+
function retainedText(xml, rung) {
|
|
350
|
+
const text = rung.children.find(child => child.kind === 'Text');
|
|
351
|
+
return text === undefined ? '<Text><![CDATA[]]></Text>' : xml.slice(text.start, text.end);
|
|
352
|
+
}
|
|
353
|
+
/** Rebuild one rung element with a comment and a text element. */
|
|
354
|
+
function rungWith(rung, text, comment) {
|
|
355
|
+
const type = rung.attributes.Type ?? 'N';
|
|
356
|
+
return `<Rung Number="${rung.attributes.Number}" Type="${type}">${commentXml(comment)}${text}</Rung>`;
|
|
357
|
+
}
|
|
333
358
|
/** Every type name a change may reference: scalars plus declared types. */
|
|
334
359
|
function declaredTypeNames(project) {
|
|
335
360
|
const names = new Set([...SCALAR_TYPES].map(name => name.toLowerCase()));
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the handover documentation bundle for one export.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is derived from the parsed project: the generator provides the
|
|
5
|
+
* structure and the citations, and the narrative prose stays the caller's job.
|
|
6
|
+
* That split is deliberate — a generated narrative would be an unsourced claim,
|
|
7
|
+
* while a rung with its number, comment, text and referenced tags is evidence a
|
|
8
|
+
* reviewer can check in one click.
|
|
9
|
+
*
|
|
10
|
+
* The builder is pure: it returns a map of relative path to text and writes
|
|
11
|
+
* nothing. The tool decides where the folder goes.
|
|
12
|
+
*
|
|
13
|
+
* @module dsh-engineering-l5x/engine/documentation
|
|
14
|
+
*/
|
|
15
|
+
import type { ProjectSnapshot } from '../session.ts';
|
|
16
|
+
/** One documentation part a caller can ask for. */
|
|
17
|
+
export type DocumentPart = 'project' | 'io' | 'tags' | 'rungs';
|
|
18
|
+
/** Every part, in the order the files are produced. */
|
|
19
|
+
export declare const DOCUMENT_PARTS: readonly DocumentPart[];
|
|
20
|
+
/** What one generation produced. */
|
|
21
|
+
export interface DocumentBundle {
|
|
22
|
+
/** Relative path to file text. */
|
|
23
|
+
readonly files: ReadonlyMap<string, string>;
|
|
24
|
+
/** Counts a caller can compare with project_overview. */
|
|
25
|
+
counts: {
|
|
26
|
+
programs: number;
|
|
27
|
+
tasks: number;
|
|
28
|
+
routines: number;
|
|
29
|
+
rungs: number;
|
|
30
|
+
comments: number;
|
|
31
|
+
tags: number;
|
|
32
|
+
modules: number;
|
|
33
|
+
};
|
|
34
|
+
/** Programs and routines the filters excluded. */
|
|
35
|
+
readonly excluded: {
|
|
36
|
+
readonly programs: readonly string[];
|
|
37
|
+
readonly routines: readonly string[];
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Options for one generation. */
|
|
41
|
+
export interface DocumentOptions {
|
|
42
|
+
/** Parts to produce; defaults to all. */
|
|
43
|
+
readonly include?: readonly DocumentPart[];
|
|
44
|
+
/** Restrict to these programs. */
|
|
45
|
+
readonly programs?: readonly string[];
|
|
46
|
+
/** Restrict to these routine names. */
|
|
47
|
+
readonly routines?: readonly string[];
|
|
48
|
+
/** Most rungs per generated file before a continuation file is written. */
|
|
49
|
+
readonly rungsPerFile?: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Build the documentation bundle.
|
|
53
|
+
*
|
|
54
|
+
* @param project - Open export snapshot.
|
|
55
|
+
* @param options - Parts, filters and file split.
|
|
56
|
+
* @returns Files to write, counts, and what the filters excluded.
|
|
57
|
+
*/
|
|
58
|
+
export declare function buildDocumentation(project: ProjectSnapshot, options?: DocumentOptions): DocumentBundle;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the handover documentation bundle for one export.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is derived from the parsed project: the generator provides the
|
|
5
|
+
* structure and the citations, and the narrative prose stays the caller's job.
|
|
6
|
+
* That split is deliberate — a generated narrative would be an unsourced claim,
|
|
7
|
+
* while a rung with its number, comment, text and referenced tags is evidence a
|
|
8
|
+
* reviewer can check in one click.
|
|
9
|
+
*
|
|
10
|
+
* The builder is pure: it returns a map of relative path to text and writes
|
|
11
|
+
* nothing. The tool decides where the folder goes.
|
|
12
|
+
*
|
|
13
|
+
* @module dsh-engineering-l5x/engine/documentation
|
|
14
|
+
*/
|
|
15
|
+
import { L5xError } from "../core/errors.js";
|
|
16
|
+
import { getRoutine, listRoutines } from "../model/routines.js";
|
|
17
|
+
import { aliasesOf } from "./configuration.js";
|
|
18
|
+
import { parseLadder } from "./logic-syntax.js";
|
|
19
|
+
import { resolveTag } from "./tag-resolution.js";
|
|
20
|
+
/** Every part, in the order the files are produced. */
|
|
21
|
+
export const DOCUMENT_PARTS = ['project', 'io', 'tags', 'rungs'];
|
|
22
|
+
/** CSV cell with quoting when the value needs it. */
|
|
23
|
+
function cell(value) {
|
|
24
|
+
const text = value === undefined || value === null ? '' : String(value);
|
|
25
|
+
return /[",\n\r]/u.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
|
|
26
|
+
}
|
|
27
|
+
/** One CSV line. */
|
|
28
|
+
function line(...values) {
|
|
29
|
+
return `${values.map(cell).join(',')}\n`;
|
|
30
|
+
}
|
|
31
|
+
/** A markdown table row. */
|
|
32
|
+
function mdRow(...values) {
|
|
33
|
+
return `| ${values.map(value => String(value ?? '').replaceAll('|', '\\|').replaceAll('\n', ' ')).join(' | ')} |\n`;
|
|
34
|
+
}
|
|
35
|
+
/** Every instruction name in one rung, branches included. */
|
|
36
|
+
function instructionsOf(nodes, out = new Set()) {
|
|
37
|
+
for (const node of nodes) {
|
|
38
|
+
if (node.kind === 'instruction')
|
|
39
|
+
out.add(node.name.toUpperCase());
|
|
40
|
+
else
|
|
41
|
+
for (const path of node.paths)
|
|
42
|
+
instructionsOf(path, out);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
/** Tags a rung mentions, resolved through the export; instruction names excluded. */
|
|
47
|
+
function tagsOf(document, rung) {
|
|
48
|
+
const parsed = parseLadder(rung.text);
|
|
49
|
+
const instructions = instructionsOf(parsed.nodes);
|
|
50
|
+
for (const aoi of document.aois)
|
|
51
|
+
instructions.add(aoi.name.toUpperCase());
|
|
52
|
+
const found = new Set();
|
|
53
|
+
for (const match of rung.text.matchAll(/[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z0-9_]+|\[[^\]]*\])*/gu)) {
|
|
54
|
+
const text = match[0];
|
|
55
|
+
if (instructions.has(text.toUpperCase()))
|
|
56
|
+
continue;
|
|
57
|
+
const resolution = resolveTag(document, 'controller', text);
|
|
58
|
+
if (resolution.resolved)
|
|
59
|
+
found.add(resolution.canonical ?? text);
|
|
60
|
+
}
|
|
61
|
+
return [...found];
|
|
62
|
+
}
|
|
63
|
+
/** A tag's alias target, for the CSV only. */
|
|
64
|
+
function aliasTarget(document, tag) {
|
|
65
|
+
return aliasesOf(document).get(`${tag.scope}::${tag.name}`) ?? '';
|
|
66
|
+
}
|
|
67
|
+
/** The tags CSV every declared tag appears in. */
|
|
68
|
+
function tagsCsv(document) {
|
|
69
|
+
let text = line('scope', 'name', 'data_type', 'description', 'alias_for', 'dimensions', 'external_access');
|
|
70
|
+
for (const tag of document.tags) {
|
|
71
|
+
const record = tag;
|
|
72
|
+
text += line(tag.scope, tag.name, tag.dataType, tag.description ?? '', aliasTarget(document, tag), record.dimensions ?? '', tag.externalAccess ?? '');
|
|
73
|
+
}
|
|
74
|
+
return text;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The Studio import shape: `Tag Name,Description`.
|
|
78
|
+
*
|
|
79
|
+
* A program-scoped tag is written `Program:Name` so a duplicate name across two
|
|
80
|
+
* programs cannot silently overwrite one of them on import. Verify the template
|
|
81
|
+
* against the installed Studio help before relying on it for a bulk import.
|
|
82
|
+
*/
|
|
83
|
+
function tagsStudioCsv(document) {
|
|
84
|
+
let text = line('Tag Name', 'Description');
|
|
85
|
+
for (const tag of document.tags) {
|
|
86
|
+
const name = tag.scope === 'controller' ? tag.name : `${tag.scope}:${tag.name}`;
|
|
87
|
+
text += line(name, tag.description ?? '');
|
|
88
|
+
}
|
|
89
|
+
return text;
|
|
90
|
+
}
|
|
91
|
+
/** The I/O list: modules and the tags that look like module data. */
|
|
92
|
+
function ioFiles(document) {
|
|
93
|
+
const io = document.tags.filter(tag => tag.name.includes(':'));
|
|
94
|
+
let markdown = '# I/O list\n\n';
|
|
95
|
+
markdown += `${document.modules.length} module(s); ${io.length} tag(s) look like module data (a colon in the name).\n\n`;
|
|
96
|
+
markdown += '| Module | Catalog | Slot | Parent | Inhibited | Description |\n|---|---|---|---|---|---|\n';
|
|
97
|
+
for (const module of document.modules) {
|
|
98
|
+
markdown += mdRow(module.name, module.catalogNumber, module.slot ?? '', module.parentModule, module.inhibited === true, module.description ?? '');
|
|
99
|
+
}
|
|
100
|
+
markdown += `\nModule-data tags: ${io.map(tag => tag.name).join(', ') || 'none'}.\n`;
|
|
101
|
+
let csv = line('name', 'catalog_number', 'slot', 'parent_module', 'inhibited', 'description');
|
|
102
|
+
for (const module of document.modules)
|
|
103
|
+
csv += line(module.name, module.catalogNumber, module.slot ?? '', module.parentModule, module.inhibited === true, module.description ?? '');
|
|
104
|
+
return { markdown, csv };
|
|
105
|
+
}
|
|
106
|
+
/** The project summary: identity, tasks, programs, counts, coverage. */
|
|
107
|
+
function projectMarkdown(project, counts) {
|
|
108
|
+
const document = project.document;
|
|
109
|
+
const { controller } = document;
|
|
110
|
+
let text = `# ${controller.name} project documentation\n\n`;
|
|
111
|
+
text += `Generated from \`${project.path}\` (sha256 \`${project.sha256.slice(0, 16)}…\`) at ${new Date().toISOString()}.\n\n`;
|
|
112
|
+
text += `- Processor: ${controller.processorType}\n- Revision: ${document.controller.majorRev}.${document.controller.minorRev}\n`;
|
|
113
|
+
text += `- Export: ${project.exportKind}; parser verification: ${project.verification}\n`;
|
|
114
|
+
text += `- Programs: ${counts.programs}; tasks: ${counts.tasks}; routines: ${counts.routines}; rungs: ${counts.rungs} (${counts.comments} with comments); tags: ${counts.tags}; modules: ${counts.modules}\n\n`;
|
|
115
|
+
text += '## Tasks\n\n| Task | Type | Rate (ms) | Priority | Programs |\n|---|---|---|---|---|\n';
|
|
116
|
+
for (const task of document.tasks)
|
|
117
|
+
text += mdRow(task.name, task.type, task.rate, task.priority, task.programs.join(', '));
|
|
118
|
+
text += '\n## Programs\n\n| Program | Main routine | Routines | Description |\n|---|---|---|---|\n';
|
|
119
|
+
for (const program of document.programs) {
|
|
120
|
+
const routines = listRoutines(document, program.name);
|
|
121
|
+
text += mdRow(program.name, program.mainRoutine, routines.length, program.description);
|
|
122
|
+
}
|
|
123
|
+
text += '\n## Coverage\n\nThis document is generated from the saved export. It does not prove construction of the program,';
|
|
124
|
+
text += ' compiled behaviour, runtime behaviour, or anything outside the export (HMI, produced/consumed tags, driver consumers).\n';
|
|
125
|
+
return text;
|
|
126
|
+
}
|
|
127
|
+
/** Rung skeletons, one markdown file per program (and continuation). */
|
|
128
|
+
function rungFiles(project, options) {
|
|
129
|
+
const files = new Map();
|
|
130
|
+
const document = project.document;
|
|
131
|
+
const programs = options.programs ?? document.programs.map(program => program.name);
|
|
132
|
+
for (const programName of programs) {
|
|
133
|
+
const program = document.programs.find(entry => entry.name === programName);
|
|
134
|
+
if (program === undefined)
|
|
135
|
+
continue;
|
|
136
|
+
let section = `# ${programName} — rung documentation\n\nProgram main routine: ${program.mainRoutine || 'not declared'}. ${program.description || 'No program description.'}\n`;
|
|
137
|
+
let part = 1;
|
|
138
|
+
let inFile = 0;
|
|
139
|
+
let headingWritten = false;
|
|
140
|
+
for (const info of listRoutines(document, programName)) {
|
|
141
|
+
if (options.routines !== undefined && !options.routines.includes(info.name))
|
|
142
|
+
continue;
|
|
143
|
+
const routine = getRoutine(document, programName, info.name);
|
|
144
|
+
if (routine === undefined || routine.body.type !== 'RLL')
|
|
145
|
+
continue;
|
|
146
|
+
section += `\n## ${info.name}\n\n${info.description || 'No routine description.'}\n`;
|
|
147
|
+
for (const rung of routine.body.rungs) {
|
|
148
|
+
const instructions = [...instructionsOf(parseLadder(rung.text).nodes)].join(', ');
|
|
149
|
+
const tags = tagsOf(document, rung);
|
|
150
|
+
section += `\n### Rung ${rung.number}\n\n`;
|
|
151
|
+
if (rung.comment !== undefined)
|
|
152
|
+
section += `**Comment:** ${rung.comment.replaceAll('\n', '\n> ')}\n\n`;
|
|
153
|
+
section += `\`\`\`\n${rung.text}\n\`\`\`\n\n`;
|
|
154
|
+
section += `Instructions: ${instructions || 'none parsed'}.\n`;
|
|
155
|
+
if (tags.length > 0)
|
|
156
|
+
section += `Tags: ${tags.join(', ')}.\n`;
|
|
157
|
+
inFile += 1;
|
|
158
|
+
if (inFile >= options.perFile) {
|
|
159
|
+
files.set(part === 1 ? `rungs/${programName}.md` : `rungs/${programName}-${part}.md`, section);
|
|
160
|
+
part += 1;
|
|
161
|
+
inFile = 0;
|
|
162
|
+
headingWritten = true;
|
|
163
|
+
section = `# ${programName} — rung documentation (continued)\n`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (inFile > 0 || !headingWritten)
|
|
168
|
+
files.set(part === 1 ? `rungs/${programName}.md` : `rungs/${programName}-${part}.md`, section);
|
|
169
|
+
}
|
|
170
|
+
return files;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Build the documentation bundle.
|
|
174
|
+
*
|
|
175
|
+
* @param project - Open export snapshot.
|
|
176
|
+
* @param options - Parts, filters and file split.
|
|
177
|
+
* @returns Files to write, counts, and what the filters excluded.
|
|
178
|
+
*/
|
|
179
|
+
export function buildDocumentation(project, options = {}) {
|
|
180
|
+
const document = project.document;
|
|
181
|
+
const include = new Set(options.include ?? DOCUMENT_PARTS);
|
|
182
|
+
const programs = options.programs === undefined ? undefined : [...options.programs];
|
|
183
|
+
const routines = options.routines === undefined ? undefined : [...options.routines];
|
|
184
|
+
if (programs !== undefined) {
|
|
185
|
+
const unknown = programs.filter(name => !document.programs.some(program => program.name === name));
|
|
186
|
+
if (unknown.length > 0)
|
|
187
|
+
throw new L5xError('COMPONENT_NOT_FOUND', `No program named ${unknown.join(', ')}.`, 'Call list_components kind="program" for the declared names.', document.programs.map(program => program.name).slice(0, 5));
|
|
188
|
+
}
|
|
189
|
+
const counts = {
|
|
190
|
+
programs: programs?.length ?? document.programs.length,
|
|
191
|
+
tasks: document.tasks.length,
|
|
192
|
+
routines: 0,
|
|
193
|
+
rungs: 0,
|
|
194
|
+
comments: 0,
|
|
195
|
+
tags: document.tags.length,
|
|
196
|
+
modules: document.modules.length,
|
|
197
|
+
};
|
|
198
|
+
for (const program of document.programs) {
|
|
199
|
+
if (programs !== undefined && !programs.includes(program.name))
|
|
200
|
+
continue;
|
|
201
|
+
for (const routine of listRoutines(document, program.name)) {
|
|
202
|
+
if (routines !== undefined && !routines.includes(routine.name))
|
|
203
|
+
continue;
|
|
204
|
+
counts.routines += 1;
|
|
205
|
+
const body = getRoutine(document, program.name, routine.name);
|
|
206
|
+
if (body !== undefined && body.body.type === 'RLL') {
|
|
207
|
+
counts.rungs += body.body.rungs.length;
|
|
208
|
+
counts.comments += body.body.rungs.filter(rung => rung.comment !== undefined).length;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const files = new Map();
|
|
213
|
+
if (include.has('project'))
|
|
214
|
+
files.set('PROJECT.md', projectMarkdown(project, counts));
|
|
215
|
+
if (include.has('io')) {
|
|
216
|
+
const io = ioFiles(document);
|
|
217
|
+
files.set('io.md', io.markdown);
|
|
218
|
+
files.set('io.csv', io.csv);
|
|
219
|
+
}
|
|
220
|
+
if (include.has('tags')) {
|
|
221
|
+
files.set('tags.csv', tagsCsv(document));
|
|
222
|
+
files.set('tags-studio.csv', tagsStudioCsv(document));
|
|
223
|
+
}
|
|
224
|
+
if (include.has('rungs')) {
|
|
225
|
+
const perFile = Math.max(1, options.rungsPerFile ?? 200);
|
|
226
|
+
for (const [path, text] of rungFiles(project, { programs, routines, perFile }))
|
|
227
|
+
files.set(path, text);
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
files,
|
|
231
|
+
counts,
|
|
232
|
+
excluded: {
|
|
233
|
+
programs: programs === undefined ? [] : document.programs.map(program => program.name).filter(name => !programs.includes(name)),
|
|
234
|
+
routines: document.routines.map(routine => routine.name).filter(name => routines !== undefined && !routines.includes(name)),
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The instruction catalog the ladder composer validates against.
|
|
3
|
+
*
|
|
4
|
+
* Logix has no instruction dictionary inside an L5X export, so a generated rung
|
|
5
|
+
* could previously name any identifier and still parse. This table is the
|
|
6
|
+
* contract: an instruction, its operand slots and the kind each slot accepts.
|
|
7
|
+
*
|
|
8
|
+
* The set covers what engineering programs actually use — measured against a
|
|
9
|
+
* 1279-tag, 860-rung project — rather than every instruction the vendor ships.
|
|
10
|
+
* An unknown name is refused with the nearest catalog names; an AOI declared by
|
|
11
|
+
* the project is accepted separately, because its pins live in the export.
|
|
12
|
+
*
|
|
13
|
+
* @module dsh-engineering-l5x/engine/instructions
|
|
14
|
+
*/
|
|
15
|
+
/** What one operand slot accepts. */
|
|
16
|
+
export type OperandKind = 'BOOL' | 'NUMBER' | 'ANY' | 'TIMER' | 'COUNTER';
|
|
17
|
+
/** One operand slot. */
|
|
18
|
+
export interface InstructionOperand {
|
|
19
|
+
/** Slot name as the programming manual presents it. */
|
|
20
|
+
readonly name: string;
|
|
21
|
+
/** What the slot accepts. */
|
|
22
|
+
readonly kind: OperandKind;
|
|
23
|
+
/** True when the instruction accepts the slot but does not require it. */
|
|
24
|
+
readonly optional?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** One catalogued instruction. */
|
|
27
|
+
export interface InstructionInfo {
|
|
28
|
+
/** Instruction mnemonic, upper case. */
|
|
29
|
+
readonly name: string;
|
|
30
|
+
/** Grouping shown by the listing tool. */
|
|
31
|
+
readonly category: string;
|
|
32
|
+
/** One line on what the instruction does. */
|
|
33
|
+
readonly summary: string;
|
|
34
|
+
/** Operand slots in written order. */
|
|
35
|
+
readonly operands: readonly InstructionOperand[];
|
|
36
|
+
}
|
|
37
|
+
/** Scalar data types the composer treats as numbers. */
|
|
38
|
+
export declare const NUMERIC_TYPES: ReadonlySet<string>;
|
|
39
|
+
/** Timer data types. */
|
|
40
|
+
export declare const TIMER_TYPES: ReadonlySet<string>;
|
|
41
|
+
/** Counter data types. */
|
|
42
|
+
export declare const COUNTER_TYPES: ReadonlySet<string>;
|
|
43
|
+
/** The catalog, in category order. */
|
|
44
|
+
export declare const INSTRUCTIONS: readonly InstructionInfo[];
|
|
45
|
+
/** The catalog entry for one mnemonic, following the alias table. */
|
|
46
|
+
export declare function instructionByName(name: string): InstructionInfo | undefined;
|
|
47
|
+
/** The catalog name a caller should use for a mnemonic. */
|
|
48
|
+
export declare function canonicalInstruction(name: string): string;
|
|
49
|
+
/** Up to `limit` catalog names closest to one unknown mnemonic. */
|
|
50
|
+
export declare function nearestInstructions(name: string, limit?: number): readonly string[];
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The instruction catalog the ladder composer validates against.
|
|
3
|
+
*
|
|
4
|
+
* Logix has no instruction dictionary inside an L5X export, so a generated rung
|
|
5
|
+
* could previously name any identifier and still parse. This table is the
|
|
6
|
+
* contract: an instruction, its operand slots and the kind each slot accepts.
|
|
7
|
+
*
|
|
8
|
+
* The set covers what engineering programs actually use — measured against a
|
|
9
|
+
* 1279-tag, 860-rung project — rather than every instruction the vendor ships.
|
|
10
|
+
* An unknown name is refused with the nearest catalog names; an AOI declared by
|
|
11
|
+
* the project is accepted separately, because its pins live in the export.
|
|
12
|
+
*
|
|
13
|
+
* @module dsh-engineering-l5x/engine/instructions
|
|
14
|
+
*/
|
|
15
|
+
/** Scalar data types the composer treats as numbers. */
|
|
16
|
+
export const NUMERIC_TYPES = new Set(['SINT', 'INT', 'DINT', 'LINT', 'USINT', 'UINT', 'UDINT', 'ULINT', 'REAL', 'LREAL']);
|
|
17
|
+
/** Timer data types. */
|
|
18
|
+
export const TIMER_TYPES = new Set(['TIMER', 'TON', 'TOF', 'RTO']);
|
|
19
|
+
/** Counter data types. */
|
|
20
|
+
export const COUNTER_TYPES = new Set(['COUNTER', 'CTU', 'CTD', 'CTUD']);
|
|
21
|
+
/** Slot helpers kept terse: the table reads as data, not code. */
|
|
22
|
+
const bool = (name = 'Tag') => ({ name, kind: 'BOOL' });
|
|
23
|
+
const num = (name = 'Source') => ({ name, kind: 'NUMBER' });
|
|
24
|
+
/** The catalog, in category order. */
|
|
25
|
+
export const INSTRUCTIONS = [
|
|
26
|
+
// Contacts and coils
|
|
27
|
+
{ name: 'XIC', category: 'Contacts and coils', summary: 'Examine if closed: true when the BOOL tag is true.', operands: [bool()] },
|
|
28
|
+
{ name: 'XIO', category: 'Contacts and coils', summary: 'Examine if open: true when the BOOL tag is false.', operands: [bool()] },
|
|
29
|
+
{ name: 'OTE', category: 'Contacts and coils', summary: 'Output energize: sets the BOOL tag to the rung condition.', operands: [bool()] },
|
|
30
|
+
{ name: 'OTL', category: 'Contacts and coils', summary: 'Output latch: sets the BOOL tag when the rung is true and holds it.', operands: [bool()] },
|
|
31
|
+
{ name: 'OTU', category: 'Contacts and coils', summary: 'Output unlatch: clears a latched BOOL tag when the rung is true.', operands: [bool()] },
|
|
32
|
+
{ name: 'ONS', category: 'Contacts and coils', summary: 'One shot: true for one scan when the rung goes true.', operands: [{ name: 'Storage', kind: 'BOOL' }] },
|
|
33
|
+
{ name: 'AFI', category: 'Contacts and coils', summary: 'Always false instruction: disables the rung and its outputs.', operands: [] },
|
|
34
|
+
// Comparison
|
|
35
|
+
{ name: 'EQU', category: 'Comparison', summary: 'Equal: true when source A equals source B.', operands: [num('Source A'), num('Source B')] },
|
|
36
|
+
{ name: 'NEQ', category: 'Comparison', summary: 'Not equal.', operands: [num('Source A'), num('Source B')] },
|
|
37
|
+
{ name: 'GRT', category: 'Comparison', summary: 'Greater than.', operands: [num('Source A'), num('Source B')] },
|
|
38
|
+
{ name: 'GEQ', category: 'Comparison', summary: 'Greater than or equal.', operands: [num('Source A'), num('Source B')] },
|
|
39
|
+
{ name: 'LES', category: 'Comparison', summary: 'Less than.', operands: [num('Source A'), num('Source B')] },
|
|
40
|
+
{ name: 'LEQ', category: 'Comparison', summary: 'Less than or equal.', operands: [num('Source A'), num('Source B')] },
|
|
41
|
+
{ name: 'LIM', category: 'Comparison', summary: 'Limit test: true when the test value lies between the limits.', operands: [num('Low limit'), num('Test'), num('High limit')] },
|
|
42
|
+
{ name: 'CMP', category: 'Comparison', summary: 'Compare: true when the expression is true.', operands: [{ name: 'Expression', kind: 'ANY' }] },
|
|
43
|
+
// Math
|
|
44
|
+
{ name: 'ADD', category: 'Math', summary: 'Add source A and source B into the destination.', operands: [num('Source A'), num('Source B'), num('Destination')] },
|
|
45
|
+
{ name: 'SUB', category: 'Math', summary: 'Subtract source B from source A into the destination.', operands: [num('Source A'), num('Source B'), num('Destination')] },
|
|
46
|
+
{ name: 'MUL', category: 'Math', summary: 'Multiply source A by source B into the destination.', operands: [num('Source A'), num('Source B'), num('Destination')] },
|
|
47
|
+
{ name: 'DIV', category: 'Math', summary: 'Divide source A by source B into the destination.', operands: [num('Source A'), num('Source B'), num('Destination')] },
|
|
48
|
+
{ name: 'MOD', category: 'Math', summary: 'Remainder of source A divided by source B.', operands: [num('Source A'), num('Source B'), num('Destination')] },
|
|
49
|
+
{ name: 'SQR', category: 'Math', summary: 'Square root of the source into the destination.', operands: [num('Source'), num('Destination')] },
|
|
50
|
+
{ name: 'CPT', category: 'Math', summary: 'Compute an expression into the destination.', operands: [{ name: 'Expression', kind: 'ANY' }, num('Destination')] },
|
|
51
|
+
// Move
|
|
52
|
+
{ name: 'MOV', category: 'Move', summary: 'Move the source value into the destination.', operands: [num('Source'), num('Destination')] },
|
|
53
|
+
{ name: 'COP', category: 'Move', summary: 'Copy elements from the source to the destination.', operands: [{ name: 'Source', kind: 'ANY' }, { name: 'Destination', kind: 'ANY' }, num('Length')] },
|
|
54
|
+
{ name: 'CLR', category: 'Move', summary: 'Clear the destination to zero.', operands: [num('Destination')] },
|
|
55
|
+
// Timer and counter
|
|
56
|
+
{ name: 'TON', category: 'Timer and counter', summary: 'Timer on delay: accumulates while the rung is true.', operands: [{ name: 'Timer', kind: 'TIMER' }, num('Preset')] },
|
|
57
|
+
{ name: 'TOF', category: 'Timer and counter', summary: 'Timer off delay: accumulates after the rung goes false.', operands: [{ name: 'Timer', kind: 'TIMER' }, num('Preset')] },
|
|
58
|
+
{ name: 'RTO', category: 'Timer and counter', summary: 'Retentive timer on: accumulates and holds its value.', operands: [{ name: 'Timer', kind: 'TIMER' }, num('Preset')] },
|
|
59
|
+
{ name: 'RES', category: 'Timer and counter', summary: 'Reset a timer or counter structure.', operands: [{ name: 'Structure', kind: 'ANY' }] },
|
|
60
|
+
{ name: 'CTU', category: 'Timer and counter', summary: 'Count up on each false-to-true rung transition.', operands: [{ name: 'Counter', kind: 'COUNTER' }, num('Preset')] },
|
|
61
|
+
{ name: 'CTD', category: 'Timer and counter', summary: 'Count down on each false-to-true rung transition.', operands: [{ name: 'Counter', kind: 'COUNTER' }, num('Preset')] },
|
|
62
|
+
// Program control
|
|
63
|
+
{ name: 'JSR', category: 'Program control', summary: 'Jump to a subroutine; parameters follow the routine name.', operands: [{ name: 'Routine', kind: 'ANY' }, { name: 'Parameter', kind: 'ANY', optional: true }] },
|
|
64
|
+
{ name: 'RET', category: 'Program control', summary: 'Return from a subroutine.', operands: [] },
|
|
65
|
+
{ name: 'JMP', category: 'Program control', summary: 'Jump to a label in the same routine.', operands: [{ name: 'Label', kind: 'ANY' }] },
|
|
66
|
+
{ name: 'LBL', category: 'Program control', summary: 'Label target for a JMP.', operands: [{ name: 'Label', kind: 'ANY' }] },
|
|
67
|
+
{ name: 'MCR', category: 'Program control', summary: 'Master control relay zone boundary.', operands: [] },
|
|
68
|
+
// Bit
|
|
69
|
+
{ name: 'AND', category: 'Bit', summary: 'Bitwise AND of two sources into the destination.', operands: [num('Source A'), num('Source B'), num('Destination')] },
|
|
70
|
+
{ name: 'OR', category: 'Bit', summary: 'Bitwise OR of two sources into the destination.', operands: [num('Source A'), num('Source B'), num('Destination')] },
|
|
71
|
+
{ name: 'XOR', category: 'Bit', summary: 'Bitwise exclusive OR of two sources into the destination.', operands: [num('Source A'), num('Source B'), num('Destination')] },
|
|
72
|
+
{ name: 'NOT', category: 'Bit', summary: 'Bitwise complement of the source into the destination.', operands: [num('Source'), num('Destination')] },
|
|
73
|
+
{ name: 'BSL', category: 'Bit', summary: 'Bit shift left through an array.', operands: [{ name: 'Array', kind: 'ANY' }, { name: 'Control', kind: 'ANY' }, num('Length')] },
|
|
74
|
+
{ name: 'BSR', category: 'Bit', summary: 'Bit shift right through an array.', operands: [{ name: 'Array', kind: 'ANY' }, { name: 'Control', kind: 'ANY' }, num('Length')] },
|
|
75
|
+
];
|
|
76
|
+
/** Alias spellings accepted for a catalog name. */
|
|
77
|
+
const ALIASES = { EQ: 'EQU', NE: 'NEQ', GT: 'GRT', GE: 'GEQ', LT: 'LES', LE: 'LEQ', LIMIT: 'LIM', MOVE: 'MOV' };
|
|
78
|
+
/** The catalog entry for one mnemonic, following the alias table. */
|
|
79
|
+
export function instructionByName(name) {
|
|
80
|
+
const upper = name.toUpperCase();
|
|
81
|
+
const canonical = ALIASES[upper] ?? upper;
|
|
82
|
+
return INSTRUCTIONS.find(instruction => instruction.name === canonical);
|
|
83
|
+
}
|
|
84
|
+
/** The catalog name a caller should use for a mnemonic. */
|
|
85
|
+
export function canonicalInstruction(name) {
|
|
86
|
+
const upper = name.toUpperCase();
|
|
87
|
+
return ALIASES[upper] ?? upper;
|
|
88
|
+
}
|
|
89
|
+
/** Up to `limit` catalog names closest to one unknown mnemonic. */
|
|
90
|
+
export function nearestInstructions(name, limit = 5) {
|
|
91
|
+
const upper = name.toUpperCase();
|
|
92
|
+
const scored = INSTRUCTIONS.map(instruction => {
|
|
93
|
+
let score = 0;
|
|
94
|
+
const candidate = instruction.name;
|
|
95
|
+
for (let index = 0; index < Math.min(candidate.length, upper.length); index += 1) {
|
|
96
|
+
if (candidate[index] === upper[index])
|
|
97
|
+
score += 1;
|
|
98
|
+
else
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
for (const letter of new Set(upper))
|
|
102
|
+
if (candidate.includes(letter))
|
|
103
|
+
score += 0.1;
|
|
104
|
+
return { name: candidate, score };
|
|
105
|
+
});
|
|
106
|
+
return scored.sort((left, right) => right.score - left.score).slice(0, limit).map(row => row.name);
|
|
107
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compose one ladder rung from a structure, never from prose.
|
|
3
|
+
*
|
|
4
|
+
* The composer is the generation half of the pair: the caller decides the logic,
|
|
5
|
+
* this module decides the NeutralText. Every instruction is checked against the
|
|
6
|
+
* catalog (or a declared AOI), every operand against the export, and the emitted
|
|
7
|
+
* text is re-parsed by the same parser `draft_rung` uses. A rung that fails any
|
|
8
|
+
* of those never reaches a draft.
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-engineering-l5x/engine/ladder
|
|
11
|
+
*/
|
|
12
|
+
import type { ProjectSnapshot } from '../session.ts';
|
|
13
|
+
/** One instruction node. */
|
|
14
|
+
export interface InstructionNode {
|
|
15
|
+
/** Instruction mnemonic, for example `XIC`. */
|
|
16
|
+
readonly instr: string;
|
|
17
|
+
/** Operands in written order. */
|
|
18
|
+
readonly args?: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
/** A left-to-right sequence of nodes. */
|
|
21
|
+
export interface SeriesNode {
|
|
22
|
+
/** Nodes evaluated in series. */
|
|
23
|
+
readonly series: readonly LadderNodeSpec[];
|
|
24
|
+
}
|
|
25
|
+
/** Parallel paths, each a left-to-right sequence. */
|
|
26
|
+
export interface ParallelNode {
|
|
27
|
+
/** One entry per path. */
|
|
28
|
+
readonly parallel: readonly (readonly LadderNodeSpec[])[];
|
|
29
|
+
}
|
|
30
|
+
/** One node of the composition tree. */
|
|
31
|
+
export type LadderNodeSpec = InstructionNode | SeriesNode | ParallelNode;
|
|
32
|
+
/** One operand, with what the export says about it. */
|
|
33
|
+
export interface ComposedOperand {
|
|
34
|
+
/** Operand text as emitted. */
|
|
35
|
+
readonly text: string;
|
|
36
|
+
/** Whether the export declares it (false for numeric literals). */
|
|
37
|
+
readonly resolved: boolean;
|
|
38
|
+
/** Canonical declaration when it resolves. */
|
|
39
|
+
readonly canonical?: string;
|
|
40
|
+
/** Declared data type when it resolves. */
|
|
41
|
+
readonly data_type?: string;
|
|
42
|
+
}
|
|
43
|
+
/** One composed rung. */
|
|
44
|
+
export interface ComposedRung {
|
|
45
|
+
/** NeutralText, ready for `draft_rung`. */
|
|
46
|
+
readonly neutral_text: string;
|
|
47
|
+
/** Instruction count. */
|
|
48
|
+
readonly instructions: number;
|
|
49
|
+
/** Operands in written order. */
|
|
50
|
+
readonly operands: readonly ComposedOperand[];
|
|
51
|
+
/** Where the instruction grammar came from. */
|
|
52
|
+
readonly grounding: 'catalog' | 'aoi' | 'mixed';
|
|
53
|
+
}
|
|
54
|
+
/** A composition result: one rung, or a pattern's pair. */
|
|
55
|
+
export interface ComposedRungs {
|
|
56
|
+
/** Rungs in evaluation order. */
|
|
57
|
+
readonly rungs: readonly ComposedRung[];
|
|
58
|
+
/** Pattern name, when a pattern produced them. */
|
|
59
|
+
readonly pattern?: string;
|
|
60
|
+
}
|
|
61
|
+
/** Named patterns the composer can expand. */
|
|
62
|
+
export declare const PATTERNS: readonly ["start_stop_latch", "fault_latch", "ton_delay", "interlock"];
|
|
63
|
+
/** One pattern name. */
|
|
64
|
+
export type PatternName = (typeof PATTERNS)[number];
|
|
65
|
+
/**
|
|
66
|
+
* Compose one rung from a structure tree.
|
|
67
|
+
*
|
|
68
|
+
* @param project - Fresh export snapshot the operands are resolved against.
|
|
69
|
+
* @param spec - The composition tree.
|
|
70
|
+
* @returns The validated rung.
|
|
71
|
+
*/
|
|
72
|
+
export declare function composeRung(project: ProjectSnapshot, spec: LadderNodeSpec): ComposedRung;
|
|
73
|
+
/** A pattern's named tags, expanded to one or two rungs. */
|
|
74
|
+
export declare function composePattern(project: ProjectSnapshot, pattern: PatternName, args: Readonly<Record<string, unknown>>): ComposedRungs;
|