@tsplc/compiler 0.2.10 → 0.2.11

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.
@@ -111,11 +111,18 @@ export class ModuleDeclarations {
111
111
  block.locals.length ||
112
112
  !matches(block.inputs, expected.inputs) ||
113
113
  !matches(block.outputs, expected.outputs) ||
114
- block.outputs.some((output) =>
115
- output.type === "BOOL"
116
- ? output.initial?.kind !== "bool" || output.initial.value !== false
117
- : output.initial?.kind !== "duration" || output.initial.nanoseconds !== "0",
118
- )
114
+ block.outputs.some(({ initial }) => {
115
+ switch (initial?.kind) {
116
+ case "bool":
117
+ return initial.value !== false;
118
+ case "duration":
119
+ return initial.nanoseconds !== "0";
120
+ case "integer":
121
+ return initial.value !== "0";
122
+ default:
123
+ return true;
124
+ }
125
+ })
119
126
  )
120
127
  reject("Invalid canonical standard block definition.", block.span);
121
128
  }
package/ir/model.ts CHANGED
@@ -158,7 +158,7 @@ export interface PlcFunction {
158
158
  export type MathHelper = "min" | "max" | "clamp";
159
159
 
160
160
  // Native vendor blocks under their TypeScript names. Ports are [name, native name, type];
161
- // every output starts FALSE or T#0s, matching the vendor library blocks.
161
+ // every output starts at its type's default, matching the vendor library blocks.
162
162
  const timer = (input: string, output: string) =>
163
163
  ({
164
164
  inputs: [
@@ -184,6 +184,45 @@ export const standardBlocks = {
184
184
  inputs: [["signal", "CLK", "BOOL"]],
185
185
  outputs: [["fell", "Q", "BOOL"]],
186
186
  },
187
+ UpCounter: {
188
+ native: "CTU",
189
+ inputs: [
190
+ ["countUp", "CU", "BOOL"],
191
+ ["reset", "RESET", "BOOL"],
192
+ ["preset", "PV", "WORD"],
193
+ ],
194
+ outputs: [
195
+ ["reachedPreset", "Q", "BOOL"],
196
+ ["value", "CV", "WORD"],
197
+ ],
198
+ },
199
+ DownCounter: {
200
+ native: "CTD",
201
+ inputs: [
202
+ ["countDown", "CD", "BOOL"],
203
+ ["load", "LOAD", "BOOL"],
204
+ ["preset", "PV", "WORD"],
205
+ ],
206
+ outputs: [
207
+ ["atZero", "Q", "BOOL"],
208
+ ["value", "CV", "WORD"],
209
+ ],
210
+ },
211
+ UpDownCounter: {
212
+ native: "CTUD",
213
+ inputs: [
214
+ ["countUp", "CU", "BOOL"],
215
+ ["countDown", "CD", "BOOL"],
216
+ ["reset", "RESET", "BOOL"],
217
+ ["load", "LOAD", "BOOL"],
218
+ ["preset", "PV", "WORD"],
219
+ ],
220
+ outputs: [
221
+ ["reachedPreset", "QU", "BOOL"],
222
+ ["atZero", "QD", "BOOL"],
223
+ ["value", "CV", "WORD"],
224
+ ],
225
+ },
187
226
  Blinker: {
188
227
  native: "Util.BLINK",
189
228
  inputs: [
package/package.json CHANGED
@@ -17,7 +17,7 @@
17
17
  "url": "git+https://github.com/livingforjesus/tsplc.git",
18
18
  "directory": "packages/compiler"
19
19
  },
20
- "version": "0.2.10",
20
+ "version": "0.2.11",
21
21
  "type": "module",
22
22
  "exports": {
23
23
  ".": {
@@ -90,14 +90,14 @@
90
90
  "bun": ">=1.4.3"
91
91
  },
92
92
  "dependencies": {
93
- "@tsplc/agent": "0.2.10",
94
- "@tsplc/build": "0.2.10",
95
- "@tsplc/config": "0.2.10",
96
- "@tsplc/typegen": "0.2.10",
93
+ "@tsplc/agent": "0.2.11",
94
+ "@tsplc/build": "0.2.11",
95
+ "@tsplc/config": "0.2.11",
96
+ "@tsplc/typegen": "0.2.11",
97
97
  "typescript": "6.0.3",
98
98
  "zod": "4.3.6",
99
99
  "@types/bun": "1.4.2",
100
- "@tsplc/server": "0.2.10"
100
+ "@tsplc/server": "0.2.11"
101
101
  },
102
102
  "bin": {
103
103
  "tsplc": "./dist/cli.js"
package/project/plant.ts CHANGED
@@ -107,6 +107,15 @@ const askCredentials = async (): Promise<Credentials> => ({
107
107
 
108
108
  const progress = (message: string) => console.log(` ${message}`);
109
109
 
110
+ /** Prints progress with the minutes and seconds since it was made, so the slow steps show. */
111
+ const timedProgress = () => {
112
+ const start = Date.now();
113
+ return (message: string) => {
114
+ const seconds = Math.round((Date.now() - start) / 1000);
115
+ progress(`${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")} ${message}`);
116
+ };
117
+ };
118
+
110
119
  const connect = () => {
111
120
  const environment = z
112
121
  .object({
@@ -239,8 +248,10 @@ const deploy = async (
239
248
  if (status.login) throw new Error("A login is open; log out before deploying: tsplc logout");
240
249
  const target = !status.target || flags.select ? await selectTarget(agent) : status.target;
241
250
  console.log(`Building the release for ${target.host}…`);
251
+ const building = timedProgress();
242
252
  const { release, diagnostics } = await prepareRelease(file, loaded, flags);
243
253
  if (!release) return { diagnostics };
254
+ building("Built");
244
255
  if (
245
256
  flags.download &&
246
257
  !(await confirm(
@@ -258,8 +269,10 @@ const deploy = async (
258
269
  console.log(
259
270
  release.manifest.server ? "Deploying the server and the program…" : "Deploying the program…",
260
271
  );
272
+ const deploying = timedProgress();
273
+ deploying("Uploading");
261
274
  try {
262
- return await agent.deploy(release, settings, progress);
275
+ return await agent.deploy(release, settings, deploying);
263
276
  } catch (error) {
264
277
  if (
265
278
  !(error instanceof AgentOperationError && error.downloadRequired) ||
@@ -269,7 +282,7 @@ const deploy = async (
269
282
  ))
270
283
  )
271
284
  throw error;
272
- return agent.deploy(release, { ...settings, download: true }, progress);
285
+ return agent.deploy(release, { ...settings, download: true }, timedProgress());
273
286
  }
274
287
  };
275
288
 
@@ -932,17 +932,22 @@ protected execute(input:{}):void {}
932
932
  const milliseconds =
933
933
  Number(interval.arguments[0]!.text) *
934
934
  (official(interval.expression, "seconds") ? 1000 : 1);
935
+ const taskPriority = Number(priority.text);
935
936
  if (
936
937
  !Number.isSafeInteger(milliseconds) ||
937
938
  milliseconds <= 0 ||
938
939
  milliseconds > 4294967295 ||
939
- Number(priority.text) < 1 ||
940
- Number(priority.text) > 31
940
+ !Number.isInteger(taskPriority) ||
941
+ taskPriority < 1 ||
942
+ taskPriority > 31
941
943
  )
942
- fail(init, "Background interval must be positive and priority must be 1..31.");
944
+ fail(
945
+ init,
946
+ "Background interval must be positive and priority must be an integer in 1..31.",
947
+ );
943
948
  const schedule = {
944
949
  intervalNanoseconds: String(BigInt(milliseconds) * 1000000n),
945
- priority: Number(priority.text),
950
+ priority: taskPriority,
946
951
  mailbox: dec.name.text,
947
952
  worker,
948
953
  };
@@ -1,7 +1,7 @@
1
1
  import { runtimeSources } from "./module-graph.ts";
2
2
  import ts from "typescript";
3
3
  import { createRequire } from "node:module";
4
- import { mkdirSync, writeFileSync } from "node:fs";
4
+ import { mkdirSync, realpathSync, writeFileSync } from "node:fs";
5
5
  import { dirname, join, relative, resolve } from "node:path";
6
6
  import {
7
7
  instancedBlock,
@@ -202,6 +202,24 @@ export const typeCheckOnly: ts.CompilerOptions = {
202
202
  tsBuildInfoFile: undefined,
203
203
  };
204
204
 
205
+ const sdkFor = (project: string, options: ts.CompilerOptions): string => {
206
+ const name = "@tsplc/compiler/globals";
207
+ const resolveSdk = (from: string) => createRequire(from).resolve(name).replaceAll("\\", "/");
208
+ try {
209
+ const installed = resolveSdk(project);
210
+ const resolved = ts.resolveModuleName(name, project, options, ts.sys).resolvedModule;
211
+ // TypeScript may preserve a workspace symlink. A path alias may only stand in
212
+ // for the SDK if it resolves to the same installed declaration.
213
+ if (resolved && realpathSync(resolved.resolvedFileName) === realpathSync(installed))
214
+ return resolved.resolvedFileName;
215
+ return installed;
216
+ } catch (cause) {
217
+ if (!(cause instanceof Error) || !("code" in cause) || cause.code !== "MODULE_NOT_FOUND")
218
+ throw cause;
219
+ return resolveSdk(import.meta.url);
220
+ }
221
+ };
222
+
205
223
  export function analyze(options: FrontendOptions): FrontendResult {
206
224
  const filename = resolve(options.tsconfig);
207
225
  const config = ts.readConfigFile(filename, ts.sys.readFile);
@@ -214,11 +232,6 @@ export function analyze(options: FrontendOptions): FrontendResult {
214
232
  filename,
215
233
  );
216
234
  if (parsed.errors.length) return { diagnostics: parsed.errors.map(tsDiagnostic) };
217
- // TypeScript stores SourceFile paths with forward slashes on every host.
218
- // Normalize require.resolve once so canonical SDK identity also works on Windows.
219
- const sdk = createRequire(import.meta.url)
220
- .resolve("@tsplc/compiler/globals")
221
- .replaceAll("\\", "/");
222
235
  const programFiles = Object.values(options.programs).map((entry) =>
223
236
  resolve(dirname(filename), entry.module),
224
237
  );
@@ -229,7 +242,6 @@ export function analyze(options: FrontendOptions): FrontendResult {
229
242
  (file) => /\.d\.[cm]?ts$/.test(file) && file.startsWith(`${dirname(filename)}/`),
230
243
  );
231
244
  const projectFiles = scope === "project" ? parsed.fileNames : ambient;
232
- const rootNames = [...new Set([...projectFiles, ...programFiles, sdk])];
233
245
  const compilerOptions: ts.CompilerOptions = {
234
246
  ...parsed.options,
235
247
  ...typeCheckOnly,
@@ -251,6 +263,8 @@ export function analyze(options: FrontendOptions): FrontendResult {
251
263
  // Nothing is emitted, and a service client's router type imports modules outside these (TS6059).
252
264
  delete compilerOptions.rootDir;
253
265
  delete compilerOptions.outDir;
266
+ const sdk = sdkFor(filename, compilerOptions);
267
+ const rootNames = [...new Set([...projectFiles, ...programFiles, sdk])];
254
268
  let program = ts.createProgram(rootNames, compilerOptions);
255
269
  const plcFiles = (program: ts.Program) => {
256
270
  const files = new Set(runtimeSources(program));
@@ -24,6 +24,12 @@ declare global {
24
24
  const RisingEdge: typeof import("./index").RisingEdge;
25
25
  type FallingEdge = import("./index").FallingEdge;
26
26
  const FallingEdge: typeof import("./index").FallingEdge;
27
+ type UpCounter = import("./index").UpCounter;
28
+ const UpCounter: typeof import("./index").UpCounter;
29
+ type DownCounter = import("./index").DownCounter;
30
+ const DownCounter: typeof import("./index").DownCounter;
31
+ type UpDownCounter = import("./index").UpDownCounter;
32
+ const UpDownCounter: typeof import("./index").UpDownCounter;
27
33
  const ref: typeof import("./index").ref;
28
34
  const retain: typeof import("./index").retain;
29
35
  const ms: typeof import("./index").ms;
package/types/index.d.ts CHANGED
@@ -68,7 +68,7 @@ export declare abstract class FunctionBlock<I, O = Record<never, never>> {
68
68
  protected abstract execute(input: I): keyof O extends never ? void : O;
69
69
  }
70
70
 
71
- /** Alternates `on` while enabled: off for `offTime`, then on for `onTime`. Keeps its last `on` when disabled. CODESYS Util.BLINK. */
71
+ /** Starts on, then alternates `onTime` and `offTime`. Keeps its last `on` when disabled. CODESYS Util.BLINK. */
72
72
  export declare class Blinker extends FunctionBlock<
73
73
  { enabled: BOOL; offTime: TIME; onTime: TIME },
74
74
  { on: BOOL }
@@ -109,3 +109,39 @@ export declare class RisingEdge extends FunctionBlock<{ signal: BOOL }, { rose:
109
109
  export declare class FallingEdge extends FunctionBlock<{ signal: BOOL }, { fell: BOOL }> {
110
110
  protected execute(input: { signal: BOOL }): { fell: BOOL };
111
111
  }
112
+
113
+ /** Counts rising `countUp` edges. `reset` clears `value`. `reachedPreset` indicates `value >= preset`. CODESYS CTU. */
114
+ export declare class UpCounter extends FunctionBlock<
115
+ { countUp: BOOL; reset: BOOL; preset: WORD },
116
+ { reachedPreset: BOOL; value: WORD }
117
+ > {
118
+ protected execute(input: { countUp: BOOL; reset: BOOL; preset: WORD }): {
119
+ reachedPreset: BOOL;
120
+ value: WORD;
121
+ };
122
+ }
123
+
124
+ /** Counts down on rising `countDown` edges. `load` sets `value` to `preset`. `atZero` indicates `value = 0`. CODESYS CTD. */
125
+ export declare class DownCounter extends FunctionBlock<
126
+ { countDown: BOOL; load: BOOL; preset: WORD },
127
+ { atZero: BOOL; value: WORD }
128
+ > {
129
+ protected execute(input: { countDown: BOOL; load: BOOL; preset: WORD }): {
130
+ atZero: BOOL;
131
+ value: WORD;
132
+ };
133
+ }
134
+
135
+ /** Counts rising edges in either direction. `reset` clears `value`, and `load` sets `value` to `preset`. CODESYS CTUD. */
136
+ export declare class UpDownCounter extends FunctionBlock<
137
+ { countUp: BOOL; countDown: BOOL; reset: BOOL; load: BOOL; preset: WORD },
138
+ { reachedPreset: BOOL; atZero: BOOL; value: WORD }
139
+ > {
140
+ protected execute(input: {
141
+ countUp: BOOL;
142
+ countDown: BOOL;
143
+ reset: BOOL;
144
+ load: BOOL;
145
+ preset: WORD;
146
+ }): { reachedPreset: BOOL; atZero: BOOL; value: WORD };
147
+ }