layout-data 0.2.0 → 0.3.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.
@@ -0,0 +1,238 @@
1
+ // Expansion and validation of data block structures (doc/spec.md §3,
2
+ // doc/design.md §3.1). Part of the data layer.
3
+ import { FLOAT32_MAX } from "./codecs.js";
4
+ import { is_reserved_name } from "./reserved.js";
5
+
6
+ /** @import { Layout_Item } from "./typedefs.js" */
7
+ /** @import { Data_Struct } from "./internal_types.js" */
8
+
9
+ function add_item(data_struct, item, base_offset, name_prefix) {
10
+ const items = data_struct.items;
11
+ // every entry that is not a pure inclusion is a tag, whatever its name: duplicated
12
+ // and invalid names are kept and reported by validate_structure
13
+ if (item.includes === undefined || item.name !== undefined) items.push(item);
14
+ if (name_prefix && typeof item.name === "string") item.name = name_prefix + item.name;
15
+
16
+ item.offset = item.offset + base_offset;
17
+ const length = item.offset + item.length;
18
+ if (data_struct.length < length) data_struct.length = length;
19
+
20
+ const includes = item.includes;
21
+ if (includes && Array.isArray(includes)) {
22
+ for (const _item of includes) {
23
+ add_item(data_struct, { ..._item }, item.offset, item.name_prefix);
24
+ }
25
+ // The "includes" and "coupling" attributes can only be chosen one at a time.
26
+ return;
27
+ }
28
+
29
+ const coupling = [];
30
+ const _coupling = item.coupling;
31
+ if (_coupling && Array.isArray(_coupling)) {
32
+ for (const _cp of _coupling) {
33
+ const cp = { ..._cp };
34
+ add_item(data_struct, cp, item.offset, item.name_prefix);
35
+ cp.coupling ??= [];
36
+ cp.coupling.push(item.name);
37
+ coupling.push(cp);
38
+ }
39
+ share_init_value(item, coupling);
40
+ }
41
+ item.coupling = coupling.map((item) => item.name);
42
+ }
43
+
44
+ // Items whose init_value is given both on the word and on some of its bits,
45
+ // with the names of those bits; reported by validate_structure.
46
+ const init_conflicts = new WeakMap();
47
+
48
+ /**
49
+ * Only one side of a combined word gives the init values (doc/spec.md §3):
50
+ * either the word, whose bits then take theirs from it, or the bits, from
51
+ * which the word is composed (a bit without init_value is false).
52
+ */
53
+ function share_init_value(word, bits) {
54
+ const given = bits.filter((bit) => bit.init_value !== undefined);
55
+ if (word.init_value !== undefined && given.length > 0) {
56
+ // keep the given values (the bits are written after the word) and
57
+ // report the conflict; the other bits still take theirs from the word
58
+ init_conflicts.set(word, given.map((bit) => bit.name));
59
+ }
60
+ const size = word.length >> 3;
61
+ // big-endian word: byte k of the word holds bits (size - 1 - k) * 8 ..
62
+ // a bit outside the word has no value in it (reported by validate_structure)
63
+ const bit_value = (bit) => {
64
+ const relative = bit.offset - word.offset;
65
+ if (!(relative >= 0 && relative < word.length)) return 0;
66
+ return 2 ** ((size - 1 - (relative >> 3)) * 8 + relative % 8);
67
+ };
68
+ if (word.init_value === undefined) {
69
+ word.init_value = 0;
70
+ for (const bit of bits) {
71
+ bit.init_value ??= false;
72
+ if (bit.init_value === true) word.init_value += bit_value(bit);
73
+ }
74
+ return;
75
+ }
76
+ if (typeof word.init_value !== "number") return;
77
+ for (const bit of bits) {
78
+ if (bit.init_value !== undefined) continue;
79
+ const value = bit_value(bit);
80
+ bit.init_value = value > 0 && Math.floor(word.init_value / value) % 2 === 1;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Expands a list of items (resolving `includes` and `coupling`, making offsets
86
+ * absolute) into the given structure. Internal. It changes the items it is given.
87
+ *
88
+ * @param {{name: string}} data_struct - The structure to fill in; only the name is given.
89
+ * @param {Layout_Item[]} items
90
+ * @returns {Data_Struct} The same object, now complete.
91
+ */
92
+ export function build_structure(data_struct, items) {
93
+ data_struct.length = 0;
94
+ data_struct.items = [];
95
+ if (!items) return data_struct;
96
+ for (const item of items) {
97
+ add_item(data_struct, item, 0);
98
+ }
99
+ return data_struct;
100
+ }
101
+
102
+ /**
103
+ * Bit length of each tag type (lower case names).
104
+ *
105
+ * @type {Record<string, number>}
106
+ */
107
+ export const TYPE_BITS = {
108
+ bool: 1,
109
+ byte: 8, sint: 8, usint: 8,
110
+ int: 16, uint: 16, word: 16,
111
+ dint: 32, udint: 32, dword: 32, real: 32,
112
+ };
113
+
114
+ /**
115
+ * A tag name: letters, digits and `_`, not starting with a digit, an
116
+ * identifier in every language (doc/spec-data-api.md §6.2).
117
+ */
118
+ const TAG_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
119
+
120
+ // value range of each integer type
121
+ const INTEGER_RANGES = {
122
+ byte: [0, 0xff], usint: [0, 0xff], sint: [-0x80, 0x7f],
123
+ int: [-0x8000, 0x7fff], uint: [0, 0xffff], word: [0, 0xffff],
124
+ dint: [-0x80000000, 0x7fffffff], udint: [0, 0xffffffff], dword: [0, 0xffffffff],
125
+ };
126
+
127
+ function check_init_value(item, type) {
128
+ const value = item.init_value;
129
+ if (type === "bool") return typeof value === "boolean";
130
+ if (typeof value !== "number") return false;
131
+ if (type === "real") return Number.isFinite(value) && Math.abs(value) <= FLOAT32_MAX;
132
+ const [min, max] = INTEGER_RANGES[type];
133
+ return Number.isInteger(value) && value >= min && value <= max;
134
+ }
135
+
136
+ /**
137
+ * Validates a structure built by build_structure against the layout rules
138
+ * (doc/spec.md §3, doc/design.md §3.1). Internal.
139
+ *
140
+ * @param {Data_Struct} data_struct
141
+ * @returns {string[]} descriptions of all violations, empty when the layout is valid
142
+ */
143
+ export function validate_structure(data_struct) {
144
+ const errors = [];
145
+ const struct_name = data_struct.name;
146
+ const report = (item, message) => errors.push(`${struct_name}.${item.name}: ${message}`);
147
+ const items = data_struct.items;
148
+
149
+ // names
150
+ const by_name = new Map();
151
+ for (const item of items) {
152
+ if (by_name.has(item.name)) report(item, "duplicated name");
153
+ else by_name.set(item.name, item);
154
+ if (typeof item.name !== "string" || !TAG_NAME.test(item.name)) {
155
+ report(item, "name is not an identifier (letters, digits and \"_\", not starting with a digit)");
156
+ } else if (is_reserved_name(item.name)) {
157
+ report(item, "name is a reserved name");
158
+ }
159
+ }
160
+
161
+ // type, length, alignment and init_value
162
+ const valid_items = [];
163
+ for (const item of items) {
164
+ const type = item.type?.toLowerCase();
165
+ if (!(type in TYPE_BITS)) {
166
+ report(item, `unknown type ${item.type}`);
167
+ continue;
168
+ }
169
+ if (item.length !== TYPE_BITS[type]) {
170
+ report(item, `length ${item.length} does not match type ${item.type}`);
171
+ }
172
+ if (type !== "bool") {
173
+ if (item.offset % 8 !== 0) {
174
+ report(item, `offset ${item.offset} is not on a byte boundary`);
175
+ } else if (TYPE_BITS[type] > 8 && (item.offset >> 3) % 2 !== 0) {
176
+ report(item, `byte offset ${item.offset >> 3} is not even`);
177
+ }
178
+ }
179
+ const conflict = init_conflicts.get(item);
180
+ if (conflict) {
181
+ report(item, `init_value is given both on the word and on its bits ${conflict.join(", ")}`);
182
+ }
183
+ if (!check_init_value(item, type)) {
184
+ report(item, `init_value ${item.init_value} is not valid for type ${item.type}`);
185
+ }
186
+ valid_items.push({ item, type, end: item.offset + TYPE_BITS[type] });
187
+ }
188
+
189
+ // non-Bool tags must not overlap
190
+ const words = valid_items
191
+ .filter(({ type }) => type !== "bool")
192
+ .sort((a, b) => a.item.offset - b.item.offset);
193
+ let last = null;
194
+ for (const word of words) {
195
+ if (last && word.item.offset < last.end) {
196
+ report(word.item, `overlaps ${last.item.name}`);
197
+ }
198
+ if (!last || word.end > last.end) last = word;
199
+ }
200
+
201
+ // each bit belongs to at most one Bool, and a Bool inside a non-Bool tag
202
+ // must be one of its coupled bits
203
+ const bits = new Map();
204
+ for (const { item, type } of valid_items) {
205
+ if (type !== "bool") continue;
206
+ const other = bits.get(item.offset);
207
+ if (other) report(item, `shares bit ${item.offset} with ${other.name}`);
208
+ else bits.set(item.offset, item);
209
+ for (const word of words) {
210
+ if (item.offset < word.item.offset || item.offset >= word.end) continue;
211
+ if (!item.coupling?.includes(word.item.name)) {
212
+ report(item, `lies inside ${word.item.name} but is not one of its coupled bits`);
213
+ }
214
+ }
215
+ }
216
+
217
+ // coupled bits of a combined tag must lie inside it
218
+ for (const { item, type, end } of valid_items) {
219
+ if (type === "bool" || !item.coupling?.length) continue;
220
+ for (const bit_name of item.coupling) {
221
+ const bit = by_name.get(bit_name);
222
+ if (!bit) {
223
+ report(item, `coupled bit ${bit_name} does not exist`);
224
+ } else if (bit.type?.toLowerCase() !== "bool") {
225
+ report(item, `coupled tag ${bit_name} is not a Bool`);
226
+ } else if (bit.offset < item.offset || bit.offset >= end) {
227
+ report(item, `coupled bit ${bit_name} lies outside it`);
228
+ }
229
+ }
230
+ }
231
+
232
+ // whole registers
233
+ if (data_struct.length % 16 !== 0) {
234
+ errors.push(`${struct_name}: length ${data_struct.length} bits is not a whole number of registers`);
235
+ }
236
+
237
+ return errors;
238
+ }
@@ -0,0 +1,143 @@
1
+ // Type definitions of the public interface. This file has no run-time content:
2
+ // the JSDoc here is the source of the generated declaration file (dist/types).
3
+ // The types of the internals are in internal_types.js and are not exported.
4
+
5
+ /**
6
+ * An entry of a layout description that defines a tag (doc/spec-data-api.md §6.2).
7
+ *
8
+ * @typedef {object} Tag_Item
9
+ * @property {string} name - Tag name.
10
+ * @property {string} type - Tag type, case-insensitive (see Tag_Type).
11
+ * @property {number} offset - Bit offset, relative to the parent entry until expanded.
12
+ * @property {number} [length] - Length in bits, filled in from the type when omitted.
13
+ * @property {number | boolean} [init_value] - Initial value.
14
+ * @property {string} [comment] - Description, rendered into the generated types.
15
+ * @property {boolean} [is_combined] - Descriptive mark of a combined word; the linkage is decided by `coupling`.
16
+ * @property {Tag_Item[] | string} [coupling] - The bits of a combined word, as an inline list or the name of a fragment.
17
+ * @property {never} [includes] - A tag entry does not include entries.
18
+ */
19
+
20
+ /**
21
+ * An entry of a layout description that splices in other entries (doc/spec-data-api.md §6.2).
22
+ *
23
+ * @typedef {object} Include_Item
24
+ * @property {Layout_Item[] | string} includes - The entries to include, as an inline list or the name of a fragment.
25
+ * @property {number} offset - Bit offset by which the included entries are shifted as a whole.
26
+ * @property {string} [name_prefix] - Prefix of the names of the included entries.
27
+ * @property {never} [name] - An inclusion defines no tag.
28
+ * @property {never} [type] - An inclusion defines no tag.
29
+ */
30
+
31
+ /**
32
+ * One entry of a layout description, as written by the user in a layout file
33
+ * or given to register_layout (doc/spec-data-api.md §6).
34
+ *
35
+ * @typedef {Tag_Item | Include_Item} Layout_Item
36
+ */
37
+
38
+ /**
39
+ * A layout description: `layouts` maps layout names to lists of entries, and
40
+ * every other key is a fragment, a list of entries that can be named
41
+ * (doc/spec-data-api.md §6.1).
42
+ *
43
+ * @typedef {{ layouts?: Record<string, Layout_Item[]> } & Record<string, Layout_Item[] | Record<string, Layout_Item[]> | undefined>} Layout_Description
44
+ */
45
+
46
+ /**
47
+ * One tag of a compiled layout (doc/spec-data-api.md §6.6), read-only:
48
+ * `name`; `type`, as written in the description; `offset`, the absolute bit
49
+ * offset; `length` in bits; `init_value`; `comment`, if given; `coupling`,
50
+ * the tag names of the bits of a combined word, or of the combined words a
51
+ * bit belongs to.
52
+ *
53
+ * @typedef {Readonly<{
54
+ * name: string,
55
+ * type: string,
56
+ * offset: number,
57
+ * length: number,
58
+ * init_value: number | boolean,
59
+ * comment?: string,
60
+ * coupling: readonly string[],
61
+ * }>} Layout_Tag
62
+ */
63
+
64
+ /**
65
+ * A compiled layout (doc/spec-data-api.md §6.6), read-only and deeply frozen:
66
+ * `name`; `length` in bits; `size` in bytes; `items`, the tags in layout
67
+ * order (a combined word before its bits).
68
+ *
69
+ * @typedef {Readonly<{
70
+ * name: string,
71
+ * length: number,
72
+ * size: number,
73
+ * items: readonly Layout_Tag[],
74
+ * }>} Layout
75
+ */
76
+
77
+ /**
78
+ * Device byte order of an IO buffer (doc/spec-data-api.md §2.9).
79
+ *
80
+ * @typedef {"BE" | "LE" | "BEBS" | "LEBS"} Endian
81
+ */
82
+
83
+ /**
84
+ * A source of layouts: the path of a YAML 1.2 layout file, or a description
85
+ * that is already parsed (the same structure as the file, doc/spec-data-api.md §6.1).
86
+ *
87
+ * @typedef {string | Layout_Description} Layout_Source
88
+ */
89
+
90
+ /**
91
+ * The result of registering a layout description.
92
+ *
93
+ * @typedef {object} Registered_Layouts
94
+ * @property {Record<string, Layout>} layouts
95
+ * @property {Record<string, Layout_Item[]>} fragments
96
+ */
97
+
98
+ /**
99
+ * Type of a tag, in lower case: the type of `Ltag.type` (doc/spec-data-api.md §1).
100
+ *
101
+ * @typedef {"bool" | "byte" | "usint" | "sint" | "int" | "uint" | "word" | "dint" | "udint" | "dword" | "real"} Tag_Type
102
+ */
103
+
104
+ /**
105
+ * The listeners of the events of an Ldata that the library emits.
106
+ *
107
+ * @typedef {object} Data_Events
108
+ * @property {(name: string, old_value: number | boolean, new_value: number | boolean) => void} change
109
+ * @property {(data: import("./Ldata.js").Ldata) => void} write
110
+ * @property {(kind: "read" | "write", tags: import("./Ltag.js").Ltag[]) => void} access
111
+ */
112
+
113
+ /**
114
+ * A class-level access listener: called for the accesses to every Ldata
115
+ * (doc/spec-data-api.md §2.8).
116
+ *
117
+ * @typedef {(data: import("./Ldata.js").Ldata, kind: "read" | "write", tags: import("./Ltag.js").Ltag[]) => void} Class_Access_Listener
118
+ */
119
+
120
+ /**
121
+ * The listener of an event of an Ldata: typed for `change` and `write`, any
122
+ * listener for another name (doc/spec-data-api.md §2.7).
123
+ *
124
+ * @template {string | symbol} E
125
+ * @typedef {E extends keyof Data_Events ? Data_Events[E] : (...args: any[]) => void} Data_Listener
126
+ */
127
+
128
+ /**
129
+ * The listeners of the events of an Ltag that the library emits.
130
+ *
131
+ * @typedef {object} Tag_Events
132
+ * @property {(old_value: number | boolean, new_value: number | boolean) => void} change
133
+ */
134
+
135
+ /**
136
+ * The listener of an event of an Ltag: typed for `change`, any listener for
137
+ * another name (doc/spec-data-api.md §3).
138
+ *
139
+ * @template {string | symbol} E
140
+ * @typedef {E extends keyof Tag_Events ? Tag_Events[E] : (...args: any[]) => void} Tag_Listener
141
+ */
142
+
143
+ export {};
package/src/types.js ADDED
@@ -0,0 +1,74 @@
1
+ // Renders TypeScript declarations of registered layouts, for editor completion
2
+ // of the tag accessors (doc/spec-data-api.md §6.7, doc/design.md §4).
3
+
4
+ /** A name that can be part of a type name: an identifier of JavaScript, in any script. */
5
+ const IDENTIFIER = /^[\p{ID_Start}_$][\p{ID_Continue}$‌‍]*$/u;
6
+
7
+ /**
8
+ * Renders a text as a doc comment, one line or several, with the `*` and `/`
9
+ * pair that would end the comment early escaped.
10
+ *
11
+ * @param {string} text
12
+ * @param {string} indent
13
+ * @returns {string[]}
14
+ */
15
+ function doc_comment(text, indent) {
16
+ const lines = String(text).replaceAll("*/", "*\\/").split(/\r?\n/);
17
+ if (lines.length === 1) return [`${indent}/** ${lines[0]} */`];
18
+ return [`${indent}/**`, ...lines.map((line) => `${indent} * ${line}`.trimEnd()), `${indent} */`];
19
+ }
20
+
21
+ /**
22
+ * Renders a declaration file with, for each layout, an interface of its tags
23
+ * (`<Layout>_Tags`, one property per tag) and the type of its data block
24
+ * (`<Layout>_Ldata`). The layout name is used as it is, with the case it has,
25
+ * so that the type names are those of `new Ldata("<Layout>", ...)` and layouts
26
+ * with different names never get the same type name.
27
+ *
28
+ * In JavaScript a JSDoc cast names the type of the value, which an editor
29
+ * uses for completion and, with `checkJs`, to check the tags:
30
+ *
31
+ * ```js
32
+ * const pump = /** @type {import("./layout_types.js").Pump_Ldata} *\/ (new Ldata("Pump", "pump_1"));
33
+ * ```
34
+ *
35
+ * @param {Record<string, import("./typedefs.js").Layout>} layouts - Registered layouts by name.
36
+ * @param {object} [options]
37
+ * @param {string} [options.source] - The layout file, named in the header; the header names none when it is omitted.
38
+ * @param {string} [options.ldata_module] - Module specifier of Ldata, relative to the output file; "layout-data" by default.
39
+ * @returns {string}
40
+ * @throws {Error} With `errors`, when the name of a layout is not an identifier, which a type name needs.
41
+ */
42
+ export function render_types(layouts, { source, ldata_module = "layout-data" } = {}) {
43
+ const names = Object.keys(layouts);
44
+ const errors = names
45
+ .filter((name) => !IDENTIFIER.test(name))
46
+ .map((name) => `layout ${JSON.stringify(name)}: the name is not an identifier, so it cannot be part of a type name`);
47
+ if (errors.length > 0) {
48
+ const error = new Error(`cannot render the types of the layouts:\n${errors.join("\n")}`);
49
+ error.errors = errors;
50
+ throw error;
51
+ }
52
+
53
+ const lines = [
54
+ source === undefined ? "// Generated by render_types. Do not edit." : `// Generated from ${source}. Do not edit.`,
55
+ `import type { Ldata } from ${JSON.stringify(ldata_module)};`,
56
+ ];
57
+ if (names.length > 0) {
58
+ lines.push(
59
+ "//",
60
+ "// In JavaScript, a JSDoc cast names the type of the value:",
61
+ `// const data = /** @type {import("<this file>").${names[0]}_Ldata} */ (new Ldata("${names[0]}", "data_1"));`,
62
+ );
63
+ }
64
+ for (const [name, layout] of Object.entries(layouts)) {
65
+ lines.push("", `/** Tags of the ${name} data block. */`, `export interface ${name}_Tags {`);
66
+ for (const item of layout.items) {
67
+ if (item.comment) lines.push(...doc_comment(item.comment, "\t"));
68
+ const ts_type = item.type.toLowerCase() === "bool" ? "boolean" : "number";
69
+ lines.push(`\t${item.name}: ${ts_type};`);
70
+ }
71
+ lines.push("}", "", `/** A ${name} data block, with its tags as properties. */`, `export type ${name}_Ldata = Ldata & ${name}_Tags;`);
72
+ }
73
+ return `${lines.join("\n")}\n`;
74
+ }