skir 0.0.1

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.
Files changed (141) hide show
  1. package/README.md +447 -0
  2. package/dist/casing.d.ts +8 -0
  3. package/dist/casing.d.ts.map +1 -0
  4. package/dist/casing.js +49 -0
  5. package/dist/casing.js.map +1 -0
  6. package/dist/casing.test.d.ts +2 -0
  7. package/dist/casing.test.d.ts.map +1 -0
  8. package/dist/casing.test.js +134 -0
  9. package/dist/casing.test.js.map +1 -0
  10. package/dist/command_line_parser.d.ts +33 -0
  11. package/dist/command_line_parser.d.ts.map +1 -0
  12. package/dist/command_line_parser.js +171 -0
  13. package/dist/command_line_parser.js.map +1 -0
  14. package/dist/command_line_parser.test.d.ts +2 -0
  15. package/dist/command_line_parser.test.d.ts.map +1 -0
  16. package/dist/command_line_parser.test.js +302 -0
  17. package/dist/command_line_parser.test.js.map +1 -0
  18. package/dist/compatibility_checker.d.ts +68 -0
  19. package/dist/compatibility_checker.d.ts.map +1 -0
  20. package/dist/compatibility_checker.js +328 -0
  21. package/dist/compatibility_checker.js.map +1 -0
  22. package/dist/compatibility_checker.test.d.ts +2 -0
  23. package/dist/compatibility_checker.test.d.ts.map +1 -0
  24. package/dist/compatibility_checker.test.js +528 -0
  25. package/dist/compatibility_checker.test.js.map +1 -0
  26. package/dist/compiler.d.ts +3 -0
  27. package/dist/compiler.d.ts.map +1 -0
  28. package/dist/compiler.js +358 -0
  29. package/dist/compiler.js.map +1 -0
  30. package/dist/config.d.ts +47 -0
  31. package/dist/config.d.ts.map +1 -0
  32. package/dist/config.js +23 -0
  33. package/dist/config.js.map +1 -0
  34. package/dist/definition_finder.d.ts +12 -0
  35. package/dist/definition_finder.d.ts.map +1 -0
  36. package/dist/definition_finder.js +180 -0
  37. package/dist/definition_finder.js.map +1 -0
  38. package/dist/definition_finder.test.d.ts +2 -0
  39. package/dist/definition_finder.test.d.ts.map +1 -0
  40. package/dist/definition_finder.test.js +164 -0
  41. package/dist/definition_finder.test.js.map +1 -0
  42. package/dist/encoding.d.ts +2 -0
  43. package/dist/encoding.d.ts.map +1 -0
  44. package/dist/encoding.js +38 -0
  45. package/dist/encoding.js.map +1 -0
  46. package/dist/encoding.test.d.ts +2 -0
  47. package/dist/encoding.test.d.ts.map +1 -0
  48. package/dist/encoding.test.js +23 -0
  49. package/dist/encoding.test.js.map +1 -0
  50. package/dist/error_renderer.d.ts +10 -0
  51. package/dist/error_renderer.d.ts.map +1 -0
  52. package/dist/error_renderer.js +247 -0
  53. package/dist/error_renderer.js.map +1 -0
  54. package/dist/formatter.d.ts +3 -0
  55. package/dist/formatter.d.ts.map +1 -0
  56. package/dist/formatter.js +263 -0
  57. package/dist/formatter.js.map +1 -0
  58. package/dist/formatter.test.d.ts +2 -0
  59. package/dist/formatter.test.d.ts.map +1 -0
  60. package/dist/formatter.test.js +156 -0
  61. package/dist/formatter.test.js.map +1 -0
  62. package/dist/index.d.ts +6 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +5 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/index.test.d.ts +2 -0
  67. package/dist/index.test.d.ts.map +1 -0
  68. package/dist/index.test.js +14 -0
  69. package/dist/index.test.js.map +1 -0
  70. package/dist/io.d.ts +13 -0
  71. package/dist/io.d.ts.map +1 -0
  72. package/dist/io.js +22 -0
  73. package/dist/io.js.map +1 -0
  74. package/dist/language_server.d.ts +15 -0
  75. package/dist/language_server.d.ts.map +1 -0
  76. package/dist/language_server.js +248 -0
  77. package/dist/language_server.js.map +1 -0
  78. package/dist/literals.d.ts +13 -0
  79. package/dist/literals.d.ts.map +1 -0
  80. package/dist/literals.js +100 -0
  81. package/dist/literals.js.map +1 -0
  82. package/dist/literals.test.d.ts +2 -0
  83. package/dist/literals.test.d.ts.map +1 -0
  84. package/dist/literals.test.js +149 -0
  85. package/dist/literals.test.js.map +1 -0
  86. package/dist/module_collector.d.ts +3 -0
  87. package/dist/module_collector.d.ts.map +1 -0
  88. package/dist/module_collector.js +22 -0
  89. package/dist/module_collector.js.map +1 -0
  90. package/dist/module_set.d.ts +44 -0
  91. package/dist/module_set.d.ts.map +1 -0
  92. package/dist/module_set.js +1025 -0
  93. package/dist/module_set.js.map +1 -0
  94. package/dist/module_set.test.d.ts +2 -0
  95. package/dist/module_set.test.d.ts.map +1 -0
  96. package/dist/module_set.test.js +1330 -0
  97. package/dist/module_set.test.js.map +1 -0
  98. package/dist/parser.d.ts +6 -0
  99. package/dist/parser.d.ts.map +1 -0
  100. package/dist/parser.js +971 -0
  101. package/dist/parser.js.map +1 -0
  102. package/dist/parser.test.d.ts +2 -0
  103. package/dist/parser.test.d.ts.map +1 -0
  104. package/dist/parser.test.js +1366 -0
  105. package/dist/parser.test.js.map +1 -0
  106. package/dist/snapshotter.d.ts +6 -0
  107. package/dist/snapshotter.d.ts.map +1 -0
  108. package/dist/snapshotter.js +107 -0
  109. package/dist/snapshotter.js.map +1 -0
  110. package/dist/tokenizer.d.ts +4 -0
  111. package/dist/tokenizer.d.ts.map +1 -0
  112. package/dist/tokenizer.js +192 -0
  113. package/dist/tokenizer.js.map +1 -0
  114. package/dist/tokenizer.test.d.ts +2 -0
  115. package/dist/tokenizer.test.d.ts.map +1 -0
  116. package/dist/tokenizer.test.js +425 -0
  117. package/dist/tokenizer.test.js.map +1 -0
  118. package/dist/types.d.ts +375 -0
  119. package/dist/types.d.ts.map +1 -0
  120. package/dist/types.js +2 -0
  121. package/dist/types.js.map +1 -0
  122. package/package.json +63 -0
  123. package/src/casing.ts +64 -0
  124. package/src/command_line_parser.ts +249 -0
  125. package/src/compatibility_checker.ts +470 -0
  126. package/src/compiler.ts +435 -0
  127. package/src/config.ts +28 -0
  128. package/src/definition_finder.ts +221 -0
  129. package/src/encoding.ts +32 -0
  130. package/src/error_renderer.ts +278 -0
  131. package/src/formatter.ts +274 -0
  132. package/src/index.ts +6 -0
  133. package/src/io.ts +33 -0
  134. package/src/language_server.ts +301 -0
  135. package/src/literals.ts +120 -0
  136. package/src/module_collector.ts +22 -0
  137. package/src/module_set.ts +1175 -0
  138. package/src/parser.ts +1122 -0
  139. package/src/snapshotter.ts +136 -0
  140. package/src/tokenizer.ts +216 -0
  141. package/src/types.ts +518 -0
@@ -0,0 +1,301 @@
1
+ import * as yaml from "yaml";
2
+ import { fromZodError } from "zod-validation-error";
3
+ import { SkirConfig } from "./config.js";
4
+ import { ModuleParser, ModuleSet } from "./module_set.js";
5
+ import { parseModule } from "./parser.js";
6
+ import { tokenizeModule } from "./tokenizer.js";
7
+ import type {
8
+ Module,
9
+ MutableModule,
10
+ RecordKey,
11
+ RecordLocation,
12
+ Result,
13
+ SkirError,
14
+ } from "./types.js";
15
+
16
+ export class LanguageServerModuleSet {
17
+ constructor(private readonly rootPath: string) {}
18
+
19
+ setFileContent(uri: string, content: string): void {
20
+ this.deleteFile(uri);
21
+ const fileType = getFileType(uri);
22
+ switch (fileType) {
23
+ case "skir.yml": {
24
+ const workspace = this.parseSkirConfig(content, uri);
25
+ if (workspace) {
26
+ this.workspaces.set(uri, workspace);
27
+ this.reassignModulesToWorkspaces();
28
+ }
29
+ break;
30
+ }
31
+ case "*.skir": {
32
+ const moduleWorkspace = this.findModuleWorkspace(uri);
33
+ const moduleBundle = this.parseSkirModule(content, uri);
34
+ this.moduleBundles.set(uri, moduleBundle);
35
+ if (moduleWorkspace) {
36
+ Workspace.addModule(moduleBundle, moduleWorkspace);
37
+ }
38
+ break;
39
+ }
40
+ default: {
41
+ const _: null = fileType;
42
+ }
43
+ }
44
+ }
45
+
46
+ deleteFile(uri: string): void {
47
+ const fileType = getFileType(uri);
48
+ switch (fileType) {
49
+ case "skir.yml": {
50
+ if (this.workspaces.delete(uri)) {
51
+ this.reassignModulesToWorkspaces();
52
+ }
53
+ break;
54
+ }
55
+ case "*.skir": {
56
+ const moduleBundle = this.moduleBundles.get(uri);
57
+ if (moduleBundle) {
58
+ Workspace.removeModule(moduleBundle);
59
+ this.moduleBundles.delete(uri);
60
+ }
61
+ break;
62
+ }
63
+ default: {
64
+ const _: null = fileType;
65
+ }
66
+ }
67
+ }
68
+
69
+ private reassignModulesToWorkspaces(): void {
70
+ if (this.reassigneModulesTimeout) {
71
+ // Already scheduled, do nothing.
72
+ return;
73
+ }
74
+ this.reassigneModulesTimeout = setTimeout(() => {
75
+ for (const [moduleUri, moduleBundle] of this.moduleBundles.entries()) {
76
+ Workspace.removeModule(moduleBundle);
77
+ const newWorkspace = this.findModuleWorkspace(moduleUri);
78
+ if (newWorkspace) {
79
+ Workspace.addModule(moduleBundle, newWorkspace);
80
+ }
81
+ }
82
+ for (const workspace of this.workspaces.values()) {
83
+ workspace.scheduleResolution();
84
+ }
85
+ this.reassigneModulesTimeout = undefined;
86
+ });
87
+ }
88
+
89
+ private parseSkirConfig(content: string, uri: string): Workspace | null {
90
+ let skirConfig: SkirConfig;
91
+ {
92
+ // `yaml.parse` fail with a helpful error message, no need to add context.
93
+ const parseResult = SkirConfig.safeParse(yaml.parse(content));
94
+ if (parseResult.success) {
95
+ skirConfig = parseResult.data;
96
+ } else {
97
+ const validationError = fromZodError(parseResult.error);
98
+ console.error(
99
+ `Error parsing skir.yml at ${uri}:`,
100
+ validationError.message,
101
+ );
102
+ return null;
103
+ }
104
+ }
105
+
106
+ let rootUri = new URL(skirConfig.srcDir || ".", uri).href;
107
+ if (!rootUri.endsWith("/")) {
108
+ rootUri += "/";
109
+ }
110
+ return new Workspace(rootUri);
111
+ }
112
+
113
+ private parseSkirModule(content: string, uri: string): ModuleBundle {
114
+ let astTree: Result<Module | null>;
115
+ {
116
+ const tokens = tokenizeModule(content, uri);
117
+ if (tokens.errors.length !== 0) {
118
+ astTree = {
119
+ result: null,
120
+ errors: tokens.errors,
121
+ };
122
+ } else {
123
+ astTree = parseModule(tokens.result, uri, content);
124
+ }
125
+ }
126
+ return {
127
+ astTree,
128
+ errors: [],
129
+ };
130
+ }
131
+
132
+ /** Finds the workspace which contains the given module URI. */
133
+ private findModuleWorkspace(moduleUri: string): ModuleWorkspace | undefined {
134
+ let match: Workspace | undefined;
135
+ const leftIsBetter = (
136
+ left: Workspace,
137
+ right: Workspace | undefined,
138
+ ): boolean => {
139
+ if (right === undefined || left.rootUri.length < right.rootUri.length) {
140
+ return true;
141
+ }
142
+ if (left.rootUri.length === right.rootUri.length) {
143
+ // Completely arbitrary, just to have a consistent order.
144
+ return left.rootUri < right.rootUri;
145
+ }
146
+ return false;
147
+ };
148
+ for (const workspace of this.workspaces.values()) {
149
+ const { rootUri } = workspace;
150
+ if (moduleUri.startsWith(rootUri) && leftIsBetter(workspace, match)) {
151
+ match = workspace;
152
+ }
153
+ }
154
+ if (!match) {
155
+ return undefined;
156
+ }
157
+ return {
158
+ workspace: match,
159
+ modulePath: moduleUri.substring(match.rootUri.length),
160
+ };
161
+ }
162
+
163
+ private reassigneModulesTimeout?: NodeJS.Timeout;
164
+ private readonly moduleBundles = new Map<string, ModuleBundle>(); // key: file URI
165
+ private readonly workspaces = new Map<string, Workspace>(); // key: file URI
166
+ }
167
+
168
+ function errorToDiagnostic(error: SkirError): Diagnostic {
169
+ const { token, message, expected } = error;
170
+ return {
171
+ range: {
172
+ start: token.position,
173
+ end: token.position + token.text.length,
174
+ },
175
+ message: message ? message : `expected: ${expected}`,
176
+ };
177
+ }
178
+
179
+ function getFileType(uri: string): "skir.yml" | "*.skir" | null {
180
+ if (uri.endsWith("/skir.yml")) {
181
+ return "skir.yml";
182
+ } else if (uri.endsWith(".skir")) {
183
+ return "*.skir";
184
+ }
185
+ return null;
186
+ }
187
+
188
+ interface Diagnostic {
189
+ readonly range?: {
190
+ readonly start: number;
191
+ readonly end: number;
192
+ };
193
+ readonly message: string;
194
+ }
195
+
196
+ interface ModuleWorkspace {
197
+ readonly workspace: Workspace;
198
+ readonly modulePath: string;
199
+ }
200
+
201
+ interface ModuleBundle {
202
+ readonly astTree: Result<Module | null>;
203
+ moduleWorkspace?: ModuleWorkspace;
204
+ errors: Diagnostic[];
205
+ }
206
+
207
+ class Workspace implements ModuleParser {
208
+ constructor(readonly rootUri: string) {}
209
+
210
+ private readonly mutableRecordMap = new Map<RecordKey, RecordLocation>();
211
+ // key: module path
212
+ private readonly modules = new Map<string, ModuleBundle>();
213
+ private scheduledResolution?: {
214
+ timeout: NodeJS.Timeout;
215
+ promise: Promise<void>;
216
+ callback: () => void;
217
+ };
218
+
219
+ static addModule(
220
+ moduleBundle: ModuleBundle,
221
+ moduleWorkspace: ModuleWorkspace,
222
+ ): void {
223
+ // If the module was already in a workspace, remove it from the old workspace.
224
+ Workspace.removeModule(moduleBundle);
225
+ const { workspace } = moduleWorkspace;
226
+ moduleBundle.moduleWorkspace = moduleWorkspace;
227
+ workspace.modules.set(moduleWorkspace.modulePath, moduleBundle);
228
+ for (const record of moduleBundle.astTree.result?.records || []) {
229
+ workspace.mutableRecordMap.set(record.record.key, record);
230
+ }
231
+ workspace.scheduleResolution();
232
+ }
233
+
234
+ static removeModule(moduleBundle: ModuleBundle): void {
235
+ const { moduleWorkspace } = moduleBundle;
236
+ if (!moduleWorkspace) {
237
+ return;
238
+ }
239
+ const { workspace } = moduleWorkspace;
240
+ workspace.modules.delete(moduleWorkspace.modulePath);
241
+ for (const record of moduleBundle.astTree.result?.records || []) {
242
+ workspace.mutableRecordMap.delete(record.record.key);
243
+ }
244
+ moduleBundle.moduleWorkspace = undefined;
245
+ workspace.scheduleResolution();
246
+ }
247
+
248
+ parseModule(modulePath: string): Result<MutableModule | null> {
249
+ const moduleBundle = this.modules.get(modulePath);
250
+ if (!moduleBundle) {
251
+ return {
252
+ result: null,
253
+ errors: [],
254
+ };
255
+ }
256
+ return moduleBundle.astTree;
257
+ }
258
+
259
+ scheduleResolution(): void {
260
+ if (this.scheduledResolution) {
261
+ clearTimeout(this.scheduledResolution.timeout);
262
+ }
263
+ const delayMilliseconds = 500;
264
+ const timeout = setTimeout(() => {
265
+ this.scheduledResolution = undefined;
266
+ this.resolve();
267
+ }, delayMilliseconds);
268
+ const scheduledResolution = {
269
+ timeout,
270
+ promise: new Promise<void>((resolve) => {
271
+ scheduledResolution.callback = resolve;
272
+ }),
273
+ callback: (() => {
274
+ throw new Error("callback not set");
275
+ }) as () => void,
276
+ };
277
+ this.scheduledResolution = scheduledResolution;
278
+ }
279
+
280
+ get resolutionDone(): Promise<void> {
281
+ if (this.scheduledResolution) {
282
+ return this.scheduledResolution.promise;
283
+ }
284
+ return Promise.resolve();
285
+ }
286
+
287
+ /**
288
+ * Synchronously performs type resolution (and validation).
289
+ * Stores the errors in every module bundle.
290
+ */
291
+ private resolve(): void {
292
+ const moduleSet = new ModuleSet(this);
293
+ for (const [modulePath, moduleBundle] of this.modules.entries()) {
294
+ const parseResult = moduleSet.parseAndResolve(modulePath);
295
+ moduleBundle.errors = parseResult.errors.map(errorToDiagnostic);
296
+ }
297
+ if (this.scheduledResolution) {
298
+ this.scheduledResolution.callback();
299
+ }
300
+ }
301
+ }
@@ -0,0 +1,120 @@
1
+ import type { DenseJson, Primitive } from "./types.js";
2
+
3
+ export function unquoteAndUnescape(stringLiteral: string): string {
4
+ const unquoted = stringLiteral.slice(1, -1);
5
+ const parts = unquoted.split("\\\\");
6
+ for (let i = 0; i < parts.length; ++i) {
7
+ const part = parts[i]!.replace(/\\(\r\n|\n|\r)/g, "\\n")
8
+ // Escape unescaped double quotes which can appear in a single-quoted
9
+ // string literal.
10
+ .replace(/(?<=^|[^\\])"/g, '\\"');
11
+ parts[i] = JSON.parse(`"${part}"`);
12
+ }
13
+ return parts.join("\\");
14
+ }
15
+
16
+ export function valueHasPrimitiveType(
17
+ token: string,
18
+ expectedType: Primitive,
19
+ ): boolean {
20
+ switch (expectedType) {
21
+ case "bool":
22
+ return token === "false" || token === "true";
23
+ case "bytes":
24
+ return (
25
+ isStringLiteral(token) &&
26
+ /^hex:([0-9A-Fa-f]{2})*$/.test(unquoteAndUnescape(token))
27
+ );
28
+ case "timestamp": {
29
+ if (!isStringLiteral(token)) {
30
+ return false;
31
+ }
32
+ const dateTime = unquoteAndUnescape(token);
33
+ const timestamp = Date.parse(dateTime).valueOf();
34
+ return (
35
+ // A timezone is required.
36
+ /(Z|[+-]\d\d:\d\d)$/.test(dateTime) &&
37
+ -8640000000000000 <= timestamp &&
38
+ timestamp <= 8640000000000000 &&
39
+ timestamp === Math.round(timestamp)
40
+ );
41
+ }
42
+ case "int32":
43
+ return isIntLiteral(token, BigInt(-2147483648), BigInt(2147483647));
44
+ case "int64":
45
+ return isIntLiteral(
46
+ token,
47
+ BigInt("-9223372036854775808"),
48
+ BigInt("9223372036854775807"),
49
+ );
50
+ case "uint64":
51
+ return isIntLiteral(token, BigInt(0), BigInt("18446744073709551615"));
52
+ case "float32":
53
+ case "float64": {
54
+ if (isStringLiteral(token)) {
55
+ const stringLiteral = unquoteAndUnescape(token);
56
+ return (
57
+ stringLiteral === "NaN" ||
58
+ stringLiteral === "Infinity" ||
59
+ stringLiteral === "-Infinity"
60
+ );
61
+ } else {
62
+ return /^[-0-9]/.test(token);
63
+ }
64
+ }
65
+ case "string":
66
+ return isStringLiteral(token);
67
+ }
68
+ }
69
+
70
+ export function isStringLiteral(token: string): boolean {
71
+ return /^['"]/.test(token);
72
+ }
73
+
74
+ function isIntLiteral(token: string, min: bigint, max: bigint): boolean {
75
+ if (!/^-?[0-9]+$/.test(token)) {
76
+ return false;
77
+ }
78
+ const value = BigInt(token);
79
+ return min <= value && value <= max;
80
+ }
81
+
82
+ export function literalValueToDenseJson(
83
+ token: string,
84
+ type: Primitive,
85
+ ): DenseJson {
86
+ switch (type) {
87
+ case "bool":
88
+ return token === "true" ? 1 : 0;
89
+ case "bytes": {
90
+ const string = unquoteAndUnescape(token);
91
+ const buffer = Buffer.from(string.substring(4), "hex");
92
+ return buffer.toString("base64");
93
+ }
94
+ case "timestamp": {
95
+ const dateTime = unquoteAndUnescape(token);
96
+ return Math.round(new Date(dateTime).valueOf());
97
+ }
98
+ case "int32":
99
+ case "float32":
100
+ case "float64": {
101
+ return isStringLiteral(token) ? unquoteAndUnescape(token) : Number(token);
102
+ }
103
+ case "int64":
104
+ case "uint64":
105
+ return String(BigInt(token));
106
+ case "string":
107
+ return unquoteAndUnescape(token);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Assuming `token` is a literal value of primitive type, returns a string which
113
+ * uniquely identifies the value within the primitive type. The behavior is
114
+ * undefined if `token` does not match `type`.
115
+ * Use this function to check if two literal values are actually equal.
116
+ */
117
+ export function literalValueToIdentity(token: string, type: Primitive): string {
118
+ const denseJson = literalValueToDenseJson(token, type);
119
+ return typeof denseJson === "string" ? denseJson : JSON.stringify(denseJson);
120
+ }
@@ -0,0 +1,22 @@
1
+ import { glob } from "glob";
2
+ import * as paths from "path";
3
+ import { REAL_FILE_SYSTEM } from "./io.js";
4
+ import { ModuleSet } from "./module_set.js";
5
+
6
+ export async function collectModules(srcDir: string): Promise<ModuleSet> {
7
+ const modules = ModuleSet.create(REAL_FILE_SYSTEM, srcDir);
8
+ const skirFiles = await glob(paths.join(srcDir, "**/*.skir"), {
9
+ stat: true,
10
+ withFileTypes: true,
11
+ });
12
+ for await (const skirFile of skirFiles) {
13
+ if (!skirFile.isFile) {
14
+ continue;
15
+ }
16
+ const relativePath = paths
17
+ .relative(srcDir, skirFile.fullpath())
18
+ .replace(/\\/g, "/");
19
+ modules.parseAndResolve(relativePath);
20
+ }
21
+ return modules;
22
+ }