layout-data 0.0.0-stage → 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.
- package/CHANGELOG.md +65 -0
- package/LICENSE +373 -0
- package/README.md +123 -2
- package/dist/types/Ldata.d.ts +180 -0
- package/dist/types/Ltag.d.ts +74 -0
- package/dist/types/codecs.d.ts +11 -0
- package/dist/types/dispatch.d.ts +11 -0
- package/dist/types/endian.d.ts +65 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/internal_types.d.ts +65 -0
- package/dist/types/layout.d.ts +54 -0
- package/dist/types/observer.d.ts +43 -0
- package/dist/types/registry.d.ts +47 -0
- package/dist/types/reserved.d.ts +9 -0
- package/dist/types/structure.d.ts +27 -0
- package/dist/types/typedefs.d.ts +155 -0
- package/dist/types/types.d.ts +25 -0
- package/package.json +49 -3
- package/src/Ldata.js +541 -0
- package/src/Ltag.js +87 -0
- package/src/codecs.js +64 -0
- package/src/dispatch.js +23 -0
- package/src/endian.js +197 -0
- package/src/index.js +8 -0
- package/src/internal_types.js +47 -0
- package/src/layout.js +303 -0
- package/src/observer.js +147 -0
- package/src/registry.js +83 -0
- package/src/reserved.js +35 -0
- package/src/structure.js +238 -0
- package/src/typedefs.js +143 -0
- package/src/types.js +74 -0
package/src/observer.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
|
|
3
|
+
/** @import { Compiled_Layout, Layout_Entry } from "./internal_types.js" */
|
|
4
|
+
|
|
5
|
+
// a change made by a change handler is detected in a further round
|
|
6
|
+
const MAX_DIFF_ROUNDS = 16;
|
|
7
|
+
|
|
8
|
+
const same_value = (a, b) => a === b || (Number.isNaN(a) && Number.isNaN(b));
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Detects the changes of an Ldata buffer by comparing it with a snapshot
|
|
12
|
+
* (doc/design.md §5.1) and reports each changed tag once.
|
|
13
|
+
*
|
|
14
|
+
* Every write path of the Ldata registers the byte range it wrote with
|
|
15
|
+
* touch(), then calls diff() (in a transaction: at its commit). Only the
|
|
16
|
+
* registered range is compared, the other bytes equal the snapshot by
|
|
17
|
+
* construction. A diff() called while the changes are being reported, by a
|
|
18
|
+
* handler writing a tag, is only recorded; the outer diff() runs another
|
|
19
|
+
* round once the current changes are reported.
|
|
20
|
+
*/
|
|
21
|
+
export class Observer {
|
|
22
|
+
/** @type {Buffer} */
|
|
23
|
+
#buffer;
|
|
24
|
+
/** @type {string} */
|
|
25
|
+
#name;
|
|
26
|
+
/** @type {Compiled_Layout} */
|
|
27
|
+
#compiled;
|
|
28
|
+
/** @type {Buffer} */
|
|
29
|
+
#snapshot;
|
|
30
|
+
/** @type {(entry: Layout_Entry, old_value: any, new_value: any, errors: unknown[]) => void} */
|
|
31
|
+
#report;
|
|
32
|
+
#diffing = false;
|
|
33
|
+
#pending = false;
|
|
34
|
+
/** The byte range [#start, #end) written since the last collection. */
|
|
35
|
+
#start = Number.POSITIVE_INFINITY;
|
|
36
|
+
#end = 0;
|
|
37
|
+
/** @type {Uint32Array} */
|
|
38
|
+
#seen;
|
|
39
|
+
#mark = 0;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {Buffer} buffer - The buffer of the Ldata.
|
|
43
|
+
* @param {string} name - The layout name, for errors.
|
|
44
|
+
* @param {Compiled_Layout} compiled - The compiled data of the layout of the Ldata.
|
|
45
|
+
* @param {(entry: Layout_Entry, old_value: any, new_value: any, errors: unknown[]) => void} report - Called for each change; what a handler throws goes to `errors`.
|
|
46
|
+
*/
|
|
47
|
+
constructor(buffer, name, compiled, report) {
|
|
48
|
+
this.#buffer = buffer;
|
|
49
|
+
this.#name = name;
|
|
50
|
+
this.#compiled = compiled;
|
|
51
|
+
this.#report = report;
|
|
52
|
+
this.#snapshot = Buffer.from(buffer);
|
|
53
|
+
this.#seen = new Uint32Array(compiled.entries.size);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Registers the byte range [start, end) as written.
|
|
58
|
+
*
|
|
59
|
+
* @param {number} start
|
|
60
|
+
* @param {number} end - Exclusive.
|
|
61
|
+
*/
|
|
62
|
+
touch(start, end) {
|
|
63
|
+
if (start >= end) return;
|
|
64
|
+
if (start < this.#start) this.#start = start;
|
|
65
|
+
if (end > this.#end) this.#end = end;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Forgets the registered range: the buffer was restored to the snapshot.
|
|
70
|
+
*/
|
|
71
|
+
discard() {
|
|
72
|
+
this.#start = Number.POSITIVE_INFINITY;
|
|
73
|
+
this.#end = 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Reports every tag changed since the last diff. Nothing is thrown: what
|
|
78
|
+
* the handlers throw, and the exception of changes that do not settle, are
|
|
79
|
+
* pushed to `errors` after every change has been reported (doc/spec-data-api.md §4.4).
|
|
80
|
+
*
|
|
81
|
+
* @param {unknown[]} errors
|
|
82
|
+
*/
|
|
83
|
+
diff(errors) {
|
|
84
|
+
if (this.#diffing) {
|
|
85
|
+
this.#pending = true;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this.#diffing = true;
|
|
89
|
+
try {
|
|
90
|
+
let rounds = 0;
|
|
91
|
+
do {
|
|
92
|
+
this.#pending = false;
|
|
93
|
+
if (++rounds > MAX_DIFF_ROUNDS) {
|
|
94
|
+
errors.push(new Error(`${this.#name}: changes do not settle after ${MAX_DIFF_ROUNDS} rounds`));
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
for (const [entry, old_value, new_value] of this.#collect()) {
|
|
98
|
+
this.#report(entry, old_value, new_value, errors);
|
|
99
|
+
}
|
|
100
|
+
} while (this.#pending);
|
|
101
|
+
} finally {
|
|
102
|
+
this.#diffing = false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Collects the changed tags, in layout order, and updates the snapshot.
|
|
108
|
+
* Old values are decoded from the snapshot, so no value is cached.
|
|
109
|
+
*/
|
|
110
|
+
#collect() {
|
|
111
|
+
const buffer = this.#buffer;
|
|
112
|
+
const snapshot = this.#snapshot;
|
|
113
|
+
const tags_at_byte = this.#compiled.tags_at_byte;
|
|
114
|
+
// entries already checked in this collection, by entry index
|
|
115
|
+
const seen = this.#seen;
|
|
116
|
+
if (this.#mark === 0xffffffff) {
|
|
117
|
+
// the marks wrap around: clear them
|
|
118
|
+
seen.fill(0);
|
|
119
|
+
this.#mark = 0;
|
|
120
|
+
}
|
|
121
|
+
const mark = ++this.#mark;
|
|
122
|
+
const changes = [];
|
|
123
|
+
let in_order = true;
|
|
124
|
+
let last_index = -1;
|
|
125
|
+
// a write during the reporting registers its range for the next round
|
|
126
|
+
const start = this.#start;
|
|
127
|
+
const end = Math.min(this.#end, buffer.length);
|
|
128
|
+
this.discard();
|
|
129
|
+
for (let i = start; i < end; i++) {
|
|
130
|
+
if (buffer[i] === snapshot[i]) continue;
|
|
131
|
+
for (const entry of tags_at_byte[i]) {
|
|
132
|
+
if (seen[entry.index] === mark) continue;
|
|
133
|
+
seen[entry.index] = mark;
|
|
134
|
+
const { codec, byte, mask } = entry;
|
|
135
|
+
const old_value = codec.get(snapshot, byte, mask);
|
|
136
|
+
const new_value = codec.get(buffer, byte, mask);
|
|
137
|
+
if (same_value(old_value, new_value)) continue;
|
|
138
|
+
changes.push([entry, old_value, new_value]);
|
|
139
|
+
if (entry.index < last_index) in_order = false;
|
|
140
|
+
last_index = entry.index;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (start < end) buffer.copy(snapshot, start, start, end);
|
|
144
|
+
if (!in_order) changes.sort((a, b) => a[0].index - b[0].index);
|
|
145
|
+
return changes;
|
|
146
|
+
}
|
|
147
|
+
}
|
package/src/registry.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// The registry of the compiled layouts, by layout name (doc/design.md §3).
|
|
2
|
+
// A name identifies one layout: it is the only source of the layouts an Ldata
|
|
3
|
+
// is created from. Written by register_structure (layout.js), read by Ldata.
|
|
4
|
+
|
|
5
|
+
/** @import { Layout } from "./typedefs.js" */
|
|
6
|
+
/** @import { Compiled_Layout } from "./internal_types.js" */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {object} Registered
|
|
10
|
+
* @property {string} key - The canonical text of the structure, to tell the same layout from another one.
|
|
11
|
+
* @property {Layout} layout
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** @type {Map<string, Registered>} */
|
|
15
|
+
const registered = new Map();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The compiled data of the layouts (doc/design.md §3), kept here so that the
|
|
19
|
+
* public Layout holds none of it.
|
|
20
|
+
*
|
|
21
|
+
* @type {WeakMap<Layout, Compiled_Layout>}
|
|
22
|
+
*/
|
|
23
|
+
const compiled = new WeakMap();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {Layout} layout
|
|
27
|
+
* @param {Compiled_Layout} data
|
|
28
|
+
*/
|
|
29
|
+
export function set_compiled(layout, data) {
|
|
30
|
+
compiled.set(layout, data);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The compiled data of a layout made by this package.
|
|
35
|
+
*
|
|
36
|
+
* @param {Layout} layout
|
|
37
|
+
* @returns {Compiled_Layout}
|
|
38
|
+
* @throws {Error} For an object that is not a compiled layout.
|
|
39
|
+
*/
|
|
40
|
+
export function compiled_of(layout) {
|
|
41
|
+
const data = compiled.get(layout);
|
|
42
|
+
if (!data) throw new Error(`${layout?.name} is not a registered layout`);
|
|
43
|
+
return data;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {string} name
|
|
48
|
+
* @returns {Registered | undefined}
|
|
49
|
+
*/
|
|
50
|
+
export function registered_of(name) {
|
|
51
|
+
return registered.get(name);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} name
|
|
56
|
+
* @param {Registered} entry
|
|
57
|
+
*/
|
|
58
|
+
export function register(name, entry) {
|
|
59
|
+
registered.set(name, entry);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The registered layout of a name, undefined when there is none.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} name
|
|
66
|
+
* @returns {Layout | undefined}
|
|
67
|
+
*/
|
|
68
|
+
export function find_layout(name) {
|
|
69
|
+
return registered.get(name)?.layout;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The registered layout of a name (doc/spec-data-api.md §6.5).
|
|
74
|
+
*
|
|
75
|
+
* @param {string} name
|
|
76
|
+
* @returns {Layout}
|
|
77
|
+
* @throws {Error} When no layout of that name is registered.
|
|
78
|
+
*/
|
|
79
|
+
export function get_layout(name) {
|
|
80
|
+
const layout = find_layout(name);
|
|
81
|
+
if (!layout) throw new Error(`unknown layout ${name}`);
|
|
82
|
+
return layout;
|
|
83
|
+
}
|
package/src/reserved.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// The reserved names (doc/spec-data-api.md §2.10, doc/design.md §4). Internal:
|
|
2
|
+
// index.js does not export this file.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Names a tag must not have: the members of Ldata, the method and field names
|
|
6
|
+
* of EventEmitter in Node.js 20.11 and the names of Object.prototype. A static
|
|
7
|
+
* list, so that it does not depend on the Node.js version that runs the program.
|
|
8
|
+
*/
|
|
9
|
+
const RESERVED_NAMES = new Set([
|
|
10
|
+
// members of Ldata
|
|
11
|
+
"name", "layout", "size", "get", "transaction",
|
|
12
|
+
"read_bytes", "write_bytes", "read_bit", "write_bit", "load_tag", "store_tag",
|
|
13
|
+
// EventEmitter
|
|
14
|
+
"addListener", "emit", "eventNames", "getMaxListeners", "listenerCount", "listeners",
|
|
15
|
+
"off", "on", "once", "prependListener", "prependOnceListener", "rawListeners",
|
|
16
|
+
"removeAllListeners", "removeListener", "setMaxListeners",
|
|
17
|
+
"_events", "_eventsCount", "_maxListeners",
|
|
18
|
+
// Object
|
|
19
|
+
"constructor", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable",
|
|
20
|
+
"toLocaleString", "toString", "valueOf", "__proto__",
|
|
21
|
+
"__defineGetter__", "__defineSetter__", "__lookupGetter__", "__lookupSetter__",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
/** Event names of the library; a user does not emit them or use them for its own events. */
|
|
25
|
+
export const RESERVED_EVENTS = Object.freeze(["change", "write", "access", "read"]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Whether a tag name would shadow a member of a data block.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} name
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
export function is_reserved_name(name) {
|
|
34
|
+
return RESERVED_NAMES.has(name);
|
|
35
|
+
}
|
package/src/structure.js
ADDED
|
@@ -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
|
+
}
|
package/src/typedefs.js
ADDED
|
@@ -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 {};
|