@tsplc/debug 0.2.16 → 0.2.18

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/src/jobs.ts CHANGED
@@ -6,22 +6,25 @@ const scalar = (iec: string): DebugType => ({ kind: "scalar", iec });
6
6
 
7
7
  /** What the trace said of a job: whether it is running, and when it last started. */
8
8
  export interface TracedJob {
9
- busy: boolean;
10
- startedAt?: string;
9
+ busy: boolean;
10
+ startedAt?: string;
11
11
  }
12
12
 
13
13
  /** The job's `busy` flag, read while a page shows the job and the trace says nothing of it. */
14
14
  export const busyRead = (job: DebugJob): TargetRead => ({
15
- target: `${job.target}.busy`,
16
- type: scalar("BOOL"),
15
+ target: `${job.target}.busy`,
16
+ type: scalar("BOOL"),
17
17
  });
18
18
 
19
19
  /** What a busy job shows: where it waits, since when, and its latest request. */
20
20
  export const detailReads = (job: DebugJob): TargetRead[] => [
21
- { target: `${job.target}.step`, type: scalar("UINT") },
22
- { target: `${job.target}.cancelling`, type: scalar("BOOL") },
23
- ...(job.startedAt ? [{ target: job.startedAt, type: scalar("UDINT") }] : []),
24
- ...job.requests.map(({ sequence }) => ({ target: sequence, type: scalar("UDINT") })),
21
+ { target: `${job.target}.step`, type: scalar("UINT") },
22
+ { target: `${job.target}.cancelling`, type: scalar("BOOL") },
23
+ ...(job.startedAt ? [{ target: job.startedAt, type: scalar("UDINT") }] : []),
24
+ ...job.requests.map(({ sequence }) => ({
25
+ target: sequence,
26
+ type: scalar("UDINT"),
27
+ })),
25
28
  ];
26
29
 
27
30
  /**
@@ -29,16 +32,20 @@ export const detailReads = (job: DebugJob): TargetRead[] => [
29
32
  * stops running it once it finishes, so the trace can miss the finish.
30
33
  */
31
34
  export const busyOf = (
32
- job: DebugJob,
33
- value: (target: string) => TargetSample | undefined,
34
- traced: TracedJob | undefined,
35
+ job: DebugJob,
36
+ value: (target: string) => TargetSample | undefined,
37
+ traced: TracedJob | undefined,
35
38
  ) => {
36
- const read = value(busyRead(job).target);
37
- return read?.quality === "good" ? read.value === true : (traced?.busy ?? null);
39
+ const read = value(busyRead(job).target);
40
+ return read?.quality === "good"
41
+ ? read.value === true
42
+ : (traced?.busy ?? null);
38
43
  };
39
44
 
40
45
  const numberOf = (sample: TargetSample | undefined) =>
41
- sample?.quality === "good" && typeof sample.value === "number" ? sample.value : undefined;
46
+ sample?.quality === "good" && typeof sample.value === "number"
47
+ ? sample.value
48
+ : undefined;
42
49
 
43
50
  /**
44
51
  * A job as the debugger shows it at host time `now`. Details are shown only while it is busy;
@@ -46,70 +53,88 @@ const numberOf = (sample: TargetSample | undefined) =>
46
53
  * clock estimate there is no start time from the PLC's own `startedAt`.
47
54
  */
48
55
  export const jobState = (
49
- job: DebugJob,
50
- inputs: {
51
- value: (target: string) => TargetSample | undefined;
52
- traced: TracedJob | undefined;
53
- failure: JobState["failure"];
54
- /** Busy, but its details aren't read for want of room in the poll. */
55
- unread: boolean;
56
- clock: Session["clock"];
57
- now: number;
58
- /** The server's trace of a call, which the PLC tags with its job and the call site's count. */
59
- request: (call: { job: number; service: string; sequence: number }) => RequestTrace | undefined;
60
- },
56
+ job: DebugJob,
57
+ inputs: {
58
+ value: (target: string) => TargetSample | undefined;
59
+ traced: TracedJob | undefined;
60
+ failure: JobState["failure"];
61
+ /** Busy, but its details aren't read for want of room in the poll. */
62
+ unread: boolean;
63
+ clock: Session["clock"];
64
+ now: number;
65
+ /** The server's trace of a call, which the PLC tags with its job and the call site's count. */
66
+ request: (call: {
67
+ job: number;
68
+ service: string;
69
+ sequence: number;
70
+ }) => RequestTrace | undefined;
71
+ },
61
72
  ): JobState => {
62
- const { value, traced, clock, now } = inputs;
63
- const busy = busyOf(job, value, traced);
64
- const startedPlc = job.startedAt === undefined ? undefined : numberOf(value(job.startedAt));
65
- const startedAt =
66
- traced?.startedAt ??
67
- (clock && startedPlc !== undefined
68
- ? new Date(startedPlc - clock.offsetMs).toISOString()
69
- : undefined);
70
- const base: JobState = {
71
- id: job.id,
72
- quality: value(busyRead(job).target)?.quality ?? (traced ? "good" : "unavailable"),
73
- busy,
74
- ...(startedAt && { startedAt }),
75
- ...(inputs.failure && { failure: inputs.failure }),
76
- };
77
- if (!busy) return base;
78
- if (inputs.unread) return { ...base, unread: true };
73
+ const { value, traced, clock, now } = inputs;
74
+ const busy = busyOf(job, value, traced);
75
+ const startedPlc =
76
+ job.startedAt === undefined ? undefined : numberOf(value(job.startedAt));
77
+ const startedAt =
78
+ traced?.startedAt ??
79
+ (clock && startedPlc !== undefined
80
+ ? new Date(startedPlc - clock.offsetMs).toISOString()
81
+ : undefined);
82
+ const base: JobState = {
83
+ id: job.id,
84
+ quality:
85
+ value(busyRead(job).target)?.quality ?? (traced ? "good" : "unavailable"),
86
+ busy,
87
+ ...(startedAt && { startedAt }),
88
+ ...(inputs.failure && { failure: inputs.failure }),
89
+ };
90
+ if (!busy) {
91
+ return base;
92
+ }
93
+ if (inputs.unread) {
94
+ return { ...base, unread: true };
95
+ }
79
96
 
80
- const step = numberOf(value(`${job.target}.step`));
81
- const current = job.steps.find((candidate) => candidate.step === step);
82
- // Each call site counts its own requests, so the latest is the one the server saw start last,
83
- // or the one counted furthest while the server has yet to report them.
84
- const calls = job.requests
85
- .map((call) => {
86
- const sequence = numberOf(value(call.sequence)) ?? 0;
87
- const trace = inputs.request({ job: call.job ?? job.id, service: call.service, sequence });
88
- return { service: call.service, sequence, trace };
89
- })
90
- .filter(({ sequence }) => sequence > 0);
91
- const later = (call: (typeof calls)[number], than: (typeof calls)[number]) =>
92
- call.trace && than.trace
93
- ? call.trace.startedAt > than.trace.startedAt
94
- : call.sequence > than.sequence;
95
- const latest = calls.reduce<(typeof calls)[number] | undefined>(
96
- (latest, call) => (latest && !later(call, latest) ? latest : call),
97
- undefined,
98
- );
99
- const cancelling = value(`${job.target}.cancelling`);
97
+ const step = numberOf(value(`${job.target}.step`));
98
+ const current = job.steps.find((candidate) => candidate.step === step);
99
+ // Each call site counts its own requests, so the latest is the one the server saw start last,
100
+ // or the one counted furthest while the server has yet to report them.
101
+ const calls = job.requests
102
+ .map((call) => {
103
+ const sequence = numberOf(value(call.sequence)) ?? 0;
104
+ const trace = inputs.request({
105
+ job: call.job ?? job.id,
106
+ service: call.service,
107
+ sequence,
108
+ });
109
+ return { service: call.service, sequence, trace };
110
+ })
111
+ .filter(({ sequence }) => sequence > 0);
112
+ const later = (call: (typeof calls)[number], than: (typeof calls)[number]) =>
113
+ call.trace && than.trace
114
+ ? call.trace.startedAt > than.trace.startedAt
115
+ : call.sequence > than.sequence;
116
+ const latest = calls.reduce<(typeof calls)[number] | undefined>(
117
+ (latest, call) => (latest && !later(call, latest) ? latest : call),
118
+ undefined,
119
+ );
120
+ const cancelling = value(`${job.target}.cancelling`);
100
121
 
101
- return {
102
- ...base,
103
- ...(cancelling?.quality === "good" && { cancelling: cancelling.value === true }),
104
- ...(step !== undefined && { step }),
105
- ...(current && { current: { kind: current.kind, range: current.range, text: current.text } }),
106
- ...(startedAt && { elapsedMs: Math.max(0, now - Date.parse(startedAt)) }),
107
- ...(latest && {
108
- request: {
109
- service: latest.service,
110
- sequence: latest.sequence,
111
- ...(latest.trace && { trace: latest.trace }),
112
- },
113
- }),
114
- };
122
+ return {
123
+ ...base,
124
+ ...(cancelling?.quality === "good" && {
125
+ cancelling: cancelling.value === true,
126
+ }),
127
+ ...(step !== undefined && { step }),
128
+ ...(current && {
129
+ current: { kind: current.kind, range: current.range, text: current.text },
130
+ }),
131
+ ...(startedAt && { elapsedMs: Math.max(0, now - Date.parse(startedAt)) }),
132
+ ...(latest && {
133
+ request: {
134
+ service: latest.service,
135
+ sequence: latest.sequence,
136
+ ...(latest.trace && { trace: latest.trace }),
137
+ },
138
+ }),
139
+ };
115
140
  };
@@ -0,0 +1,165 @@
1
+ import { elementaryType } from "@tsplc/iec";
2
+ import type { DebugType } from "./manifest.ts";
3
+
4
+ /** CODESYS's `TRUE`/`FALSE`, and the digits a BOOL may also read as. */
5
+ const booleans: Record<string, boolean> = {
6
+ TRUE: true,
7
+ FALSE: false,
8
+ "1": true,
9
+ "0": false,
10
+ };
11
+
12
+ const radixPrefixes: Record<string, string> = { 2: "0b", 8: "0o", 16: "0x" };
13
+
14
+ const timeUnits: Record<string, number> = {
15
+ d: 86_400_000,
16
+ h: 3_600_000,
17
+ m: 60_000,
18
+ s: 1000,
19
+ ms: 1,
20
+ us: 0.001,
21
+ ns: 0.000_001,
22
+ };
23
+
24
+ /** IEC's `$` escapes by letter; `$$`, `$'` and `$"` stand for themselves. */
25
+ const stringEscapes: Record<string, string> = {
26
+ l: "\n",
27
+ n: "\n",
28
+ p: "\f",
29
+ r: "\r",
30
+ t: "\t",
31
+ };
32
+
33
+ /** Typed literals such as `INT#5`, without their type; TIME's own `T#` prefix is kept. */
34
+ const withoutTypePrefix = (text: string) =>
35
+ text.replace(/^(?!L?T(?:IME)?#)[a-z_]+#(?=.)/i, "");
36
+
37
+ /** `16#FF`, `2#1010`, `-42`, with IEC's digit separators, as a BigInt literal. */
38
+ const bigIntLiteral = (text: string) => {
39
+ const [, radix, digits] = /^(2|8|16)#([0-9a-f_]+)$/i.exec(text) ?? [];
40
+ if (radix !== undefined && digits !== undefined) {
41
+ return `${radixPrefixes[radix]}${digits.replaceAll("_", "")}`;
42
+ }
43
+ const decimal = text.replaceAll("_", "");
44
+ return /^[+-]?\d+$/.test(decimal) ? decimal : undefined;
45
+ };
46
+
47
+ /** An integer literal; beyond doubles' exact range, a string. */
48
+ const integer = (text: string) => {
49
+ const literal = bigIntLiteral(text);
50
+ if (literal === undefined) {
51
+ return undefined;
52
+ }
53
+ const value = BigInt(literal);
54
+ return Number.isSafeInteger(Number(value)) ? Number(value) : String(value);
55
+ };
56
+
57
+ const real = (text: string) => {
58
+ const number = Number(text);
59
+ return text !== "" && !Number.isNaN(number) ? number : undefined;
60
+ };
61
+
62
+ /** `T#1h2m3s4ms`, `TIME#-1.5s`, `LTIME#5us`, in milliseconds. */
63
+ const duration = (text: string) => {
64
+ const [, sign, body] = /^L?T(?:IME)?#(-?)(.+)$/i.exec(text) ?? [];
65
+ if (body === undefined) {
66
+ return undefined;
67
+ }
68
+ const units = body.replaceAll("_", "");
69
+ const parts = [...units.matchAll(/(\d+(?:\.\d+)?)(ms|us|ns|d|h|m|s)/gi)];
70
+ if (parts.map(([part]) => part).join("") !== units) {
71
+ return undefined;
72
+ }
73
+ const ms = parts.reduce(
74
+ (total, [, amount, unit]) =>
75
+ total + Number(amount) * timeUnits[unit!.toLowerCase()]!,
76
+ 0,
77
+ );
78
+ return sign ? -ms : ms;
79
+ };
80
+
81
+ /** `'it$'s'` or `"wide"`, with IEC's `$` escapes. */
82
+ const quoted = (text: string) => {
83
+ const [, , body] = /^(['"])(.*)\1$/s.exec(text) ?? [];
84
+ return body?.replace(/\$([0-9a-f]{2}|[$'"lnprt])/gi, (_, code: string) =>
85
+ code.length === 2
86
+ ? String.fromCharCode(parseInt(code, 16))
87
+ : (stringEscapes[code.toLowerCase()] ?? code),
88
+ );
89
+ };
90
+
91
+ /** A member's number, or the name the login gave when the manifest doesn't list it. */
92
+ const enumValue = (
93
+ type: Extract<DebugType, { kind: "enum" }>,
94
+ text: string,
95
+ ) => {
96
+ const number = integer(text);
97
+ if (typeof number === "number") {
98
+ return number;
99
+ }
100
+ // `E_Bin.Present`, `Present`, or a member the manifest lacks with its number: `Missing (7)`.
101
+ const [, name, value] =
102
+ /^(?:\w+\.)*(\w+)(?:\s*\((-?\d+)\))?$/.exec(text) ?? [];
103
+ if (name === undefined) {
104
+ return undefined;
105
+ }
106
+ return type.members[name] ?? (value === undefined ? name : Number(value));
107
+ };
108
+
109
+ /** Only raw assembly bytes are read whole; their elements are scalars named by IEC type. */
110
+ const arrayValue = (
111
+ type: Extract<DebugType, { kind: "array" }>,
112
+ text: string,
113
+ ) => {
114
+ const items = text.replace(/^\[/, "").replace(/\]$/, "").trim();
115
+ if (!items) {
116
+ return [];
117
+ }
118
+ const values = items
119
+ .split(/\s*,\s*/)
120
+ .map((item) => scalarValue(type.element, item));
121
+ return values.length === type.length &&
122
+ values.every((value) => value !== undefined)
123
+ ? values
124
+ : undefined;
125
+ };
126
+
127
+ const scalarValue = (iec: string, text: string): unknown => {
128
+ const untyped = withoutTypePrefix(text);
129
+ const name = iec.toUpperCase();
130
+ switch (elementaryType(name)?.family) {
131
+ case "bool":
132
+ return booleans[untyped.toUpperCase()];
133
+ case "integer":
134
+ case "bits":
135
+ return integer(untyped);
136
+ case "real":
137
+ return real(untyped);
138
+ case "duration":
139
+ return duration(text);
140
+ }
141
+ if (/^W?STRING\b/.test(name)) {
142
+ return quoted(text) ?? text;
143
+ }
144
+ return text;
145
+ };
146
+
147
+ /**
148
+ * A value as `read_value` returns it, in the form the debugger decodes: BOOL as a boolean,
149
+ * numbers and TIME (in ms) as numbers, enums as their member's number (or its name when the
150
+ * manifest doesn't list it), STRING unquoted, and arrays read whole as arrays. Undefined when the
151
+ * text doesn't fit the type.
152
+ */
153
+ export const parseLoginValue = (type: DebugType, text: string): unknown => {
154
+ const trimmed = text.trim();
155
+ switch (type.kind) {
156
+ case "enum":
157
+ return enumValue(type, withoutTypePrefix(trimmed));
158
+ case "array":
159
+ return arrayValue(type, withoutTypePrefix(trimmed));
160
+ case "record":
161
+ return undefined;
162
+ case "scalar":
163
+ return scalarValue(type.iec, trimmed);
164
+ }
165
+ };