semantifold 0.1.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.
Files changed (111) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +71 -0
  3. package/build/index.d.ts +54 -0
  4. package/build/index.d.ts.map +1 -0
  5. package/build/index.js +45 -0
  6. package/build/src/backends/identifiers.d.ts +19 -0
  7. package/build/src/backends/identifiers.d.ts.map +1 -0
  8. package/build/src/backends/identifiers.js +93 -0
  9. package/build/src/backends/index.d.ts +35 -0
  10. package/build/src/backends/index.d.ts.map +1 -0
  11. package/build/src/backends/index.js +149 -0
  12. package/build/src/backends/java.d.ts +8 -0
  13. package/build/src/backends/java.d.ts.map +1 -0
  14. package/build/src/backends/java.js +172 -0
  15. package/build/src/backends/javascript.d.ts +8 -0
  16. package/build/src/backends/javascript.d.ts.map +1 -0
  17. package/build/src/backends/javascript.js +173 -0
  18. package/build/src/backends/php.d.ts +8 -0
  19. package/build/src/backends/php.d.ts.map +1 -0
  20. package/build/src/backends/php.js +175 -0
  21. package/build/src/backends/ruby.d.ts +8 -0
  22. package/build/src/backends/ruby.d.ts.map +1 -0
  23. package/build/src/backends/ruby.js +155 -0
  24. package/build/src/backends/scalars.d.ts +15 -0
  25. package/build/src/backends/scalars.d.ts.map +1 -0
  26. package/build/src/backends/scalars.js +138 -0
  27. package/build/src/backends/shared.d.ts +26 -0
  28. package/build/src/backends/shared.d.ts.map +1 -0
  29. package/build/src/backends/shared.js +424 -0
  30. package/build/src/backends/typescript.d.ts +8 -0
  31. package/build/src/backends/typescript.d.ts.map +1 -0
  32. package/build/src/backends/typescript.js +160 -0
  33. package/build/src/backends/writer.d.ts +87 -0
  34. package/build/src/backends/writer.d.ts.map +1 -0
  35. package/build/src/backends/writer.js +207 -0
  36. package/build/src/diagnostic.d.ts +67 -0
  37. package/build/src/diagnostic.d.ts.map +1 -0
  38. package/build/src/diagnostic.js +96 -0
  39. package/build/src/frontends/index.d.ts +14 -0
  40. package/build/src/frontends/index.d.ts.map +1 -0
  41. package/build/src/frontends/index.js +36 -0
  42. package/build/src/frontends/java.d.ts +12 -0
  43. package/build/src/frontends/java.d.ts.map +1 -0
  44. package/build/src/frontends/java.js +630 -0
  45. package/build/src/frontends/javascript-typescript.d.ts +19 -0
  46. package/build/src/frontends/javascript-typescript.d.ts.map +1 -0
  47. package/build/src/frontends/javascript-typescript.js +633 -0
  48. package/build/src/frontends/php.d.ts +12 -0
  49. package/build/src/frontends/php.d.ts.map +1 -0
  50. package/build/src/frontends/php.js +523 -0
  51. package/build/src/frontends/ruby.d.ts +12 -0
  52. package/build/src/frontends/ruby.d.ts.map +1 -0
  53. package/build/src/frontends/ruby.js +515 -0
  54. package/build/src/frontends/scalars.d.ts +19 -0
  55. package/build/src/frontends/scalars.d.ts.map +1 -0
  56. package/build/src/frontends/scalars.js +71 -0
  57. package/build/src/mapping.d.ts +160 -0
  58. package/build/src/mapping.d.ts.map +1 -0
  59. package/build/src/mapping.js +1120 -0
  60. package/build/src/semantic/location.d.ts +61 -0
  61. package/build/src/semantic/location.d.ts.map +1 -0
  62. package/build/src/semantic/location.js +142 -0
  63. package/build/src/semantic/operators.d.ts +16 -0
  64. package/build/src/semantic/operators.d.ts.map +1 -0
  65. package/build/src/semantic/operators.js +24 -0
  66. package/build/src/semantic/provenance.d.ts +74 -0
  67. package/build/src/semantic/provenance.d.ts.map +1 -0
  68. package/build/src/semantic/provenance.js +577 -0
  69. package/build/src/semantic/scalars.d.ts +19 -0
  70. package/build/src/semantic/scalars.d.ts.map +1 -0
  71. package/build/src/semantic/scalars.js +40 -0
  72. package/build/src/semantic/types.d.ts +1003 -0
  73. package/build/src/semantic/types.d.ts.map +1 -0
  74. package/build/src/semantic/types.js +279 -0
  75. package/build/src/semantic/validate.d.ts +60 -0
  76. package/build/src/semantic/validate.d.ts.map +1 -0
  77. package/build/src/semantic/validate.js +472 -0
  78. package/docs/architecture.md +39 -0
  79. package/docs/coding-standards.md +7 -0
  80. package/docs/goals.md +7 -0
  81. package/docs/language-support.md +69 -0
  82. package/docs/plans/2026-09-02-initial-toolchain.md +54 -0
  83. package/docs/source-maps.md +134 -0
  84. package/docs/testing.md +25 -0
  85. package/index.js +63 -0
  86. package/package.json +69 -0
  87. package/src/backends/identifiers.js +101 -0
  88. package/src/backends/index.js +159 -0
  89. package/src/backends/java.js +182 -0
  90. package/src/backends/javascript.js +184 -0
  91. package/src/backends/php.js +184 -0
  92. package/src/backends/ruby.js +163 -0
  93. package/src/backends/scalars.js +120 -0
  94. package/src/backends/shared.js +456 -0
  95. package/src/backends/typescript.js +168 -0
  96. package/src/backends/writer.js +224 -0
  97. package/src/diagnostic.js +103 -0
  98. package/src/frontends/index.js +36 -0
  99. package/src/frontends/java.js +735 -0
  100. package/src/frontends/javascript-typescript.js +718 -0
  101. package/src/frontends/php.js +593 -0
  102. package/src/frontends/ruby.js +622 -0
  103. package/src/frontends/scalars.js +77 -0
  104. package/src/mapping.js +1260 -0
  105. package/src/semantic/location.js +154 -0
  106. package/src/semantic/operators.js +28 -0
  107. package/src/semantic/provenance.js +629 -0
  108. package/src/semantic/scalars.js +44 -0
  109. package/src/semantic/types.js +319 -0
  110. package/src/semantic/validate.js +529 -0
  111. package/tsconfig.json +16 -0
@@ -0,0 +1,529 @@
1
+ // @ts-check
2
+
3
+ import {semanticFailure, unsupportedCapability, unsupportedSyntax} from "../diagnostic.js"
4
+ import {hasOnlyUnicodeScalars, isScalarTypeName} from "./scalars.js"
5
+ import {adaptedOperationFor} from "./operators.js"
6
+
7
+ /**
8
+ * @typedef Binding
9
+ * @property {boolean} mutable - Whether assignment is allowed.
10
+ * @property {import("./types.js").SemanticTypeName} type - Binding type.
11
+ */
12
+
13
+ /**
14
+ * @typedef Scope
15
+ * @property {Map<string, Binding>} bindings - Bindings declared directly in this scope.
16
+ * @property {Set<string>} pending - Names declared later in this scope.
17
+ * @property {Scope | undefined} parent - Enclosing lexical scope.
18
+ * @property {Set<string>} usedNames - All names used by the enclosing function or entry point.
19
+ */
20
+
21
+ /**
22
+ * @typedef SemanticFail
23
+ * @type {(code: string, detail: string, location: import("./types.js").SourceLocation) => never}
24
+ */
25
+
26
+ /**
27
+ * Enforces the coherent release-candidate semantic subset after adaptation.
28
+ * @param {import("./types.js").SemanticModule} module - Adapted semantic module.
29
+ * @param {import("./types.js").SemanticLanguage} language - Source language.
30
+ * @returns {import("./types.js").SemanticModule} Validated module.
31
+ */
32
+ export function validateParsedModule(module, language) {
33
+ validateModuleShape(module, (detail, location) => unsupportedSyntax(language, detail, location))
34
+ validateModuleTypes(module, (code, detail, location) => semanticFailure(language, code, detail, location), true)
35
+
36
+ return module
37
+ }
38
+
39
+ /**
40
+ * Validates scalar types and bindings for a caller-supplied semantic module before emission.
41
+ * @param {import("./types.js").SemanticModule} module - Semantic module.
42
+ * @param {import("./types.js").SemanticLanguage} language - Backend language.
43
+ * @returns {void}
44
+ */
45
+ export function validateBackendTypes(module, language) {
46
+ validateModuleTypes(module, (_code, detail, location) => unsupportedCapability(language, detail, location), false)
47
+ }
48
+
49
+ /**
50
+ * Checks parser-authored block and statement layouts before semantic validation.
51
+ * @param {import("./types.js").SemanticModule} module - Semantic module.
52
+ * @param {(detail: string, location: import("./types.js").SourceLocation) => never} fail - Shape failure.
53
+ * @returns {void}
54
+ */
55
+ function validateModuleShape(module, fail) {
56
+ for (const functionDeclaration of module.functions) {
57
+ if (functionDeclaration.parameters.length != 2) {
58
+ fail("function parameter count other than two", functionDeclaration.location)
59
+ }
60
+
61
+ validateBlockShape(functionDeclaration.body, "function body", fail)
62
+ }
63
+
64
+ validateBlockShape(module.entryPoint.body, "entry point", fail)
65
+ }
66
+
67
+ /**
68
+ * Checks one recursively nested semantic block produced by a frontend.
69
+ * @param {import("./types.js").Block} block - Semantic block.
70
+ * @param {string} detail - Diagnostic detail.
71
+ * @param {(detail: string, location: import("./types.js").SourceLocation) => never} fail - Shape failure.
72
+ * @returns {void}
73
+ */
74
+ function validateBlockShape(block, detail, fail) {
75
+ if (!block || block.kind != "Block" || !Array.isArray(block.statements)) {
76
+ fail(`${detail} without Block`, block?.location ?? /** @type {never} */ (undefined))
77
+ }
78
+
79
+ for (const statement of block.statements) {
80
+ if (!["AssignmentStatement", "IfStatement", "LocalDeclaration", "PrintStatement", "ReturnStatement"].includes(statement.kind)) {
81
+ fail(`${detail} statement ${statement.kind}`, statement.location)
82
+ }
83
+ if (statement.kind == "IfStatement") {
84
+ validateBlockShape(statement.consequent, "if consequent", fail)
85
+ if (statement.alternate) validateBlockShape(statement.alternate, "if alternate", fail)
86
+ }
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Validates declarations and expression types within lexical scopes.
92
+ * @param {import("./types.js").SemanticModule} module - Semantic module.
93
+ * @param {SemanticFail} fail - Diagnostic callback.
94
+ * @param {boolean} normalizeOperations - Whether to replace transient frontend operation intent.
95
+ * @returns {void}
96
+ */
97
+ function validateModuleTypes(module, fail, normalizeOperations) {
98
+ /** @type {Map<string, import("./types.js").FunctionDeclaration>} */
99
+ const functions = new Map()
100
+
101
+ for (const functionDeclaration of module.functions) {
102
+ if (functions.has(functionDeclaration.name)) {
103
+ fail("DUPLICATE_BINDING", `duplicate function '${functionDeclaration.name}'.`, functionDeclaration.location)
104
+ }
105
+ functions.set(functionDeclaration.name, functionDeclaration)
106
+ }
107
+
108
+ for (const functionDeclaration of module.functions) validateFunction(functionDeclaration, functions, fail, normalizeOperations)
109
+
110
+ const callableNames = new Set(functions.keys())
111
+
112
+ const entryScope = createScope(undefined, module.entryPoint.body.statements, callableNames)
113
+
114
+ validateBlock(module.entryPoint.body, entryScope, undefined, functions, fail, normalizeOperations)
115
+ }
116
+
117
+ /**
118
+ * Validates one non-void function and its complete control flow.
119
+ * @param {import("./types.js").FunctionDeclaration} declaration - Function declaration.
120
+ * @param {Map<string, import("./types.js").FunctionDeclaration>} functions - Function signatures.
121
+ * @param {SemanticFail} fail - Diagnostic callback.
122
+ * @param {boolean} normalizeOperations - Whether to replace transient frontend operation intent.
123
+ * @returns {void}
124
+ */
125
+ function validateFunction(declaration, functions, fail, normalizeOperations) {
126
+ const scope = createScope(undefined, declaration.body.statements, new Set(functions.keys()))
127
+
128
+ for (const parameter of declaration.parameters) {
129
+ const type = validateTypeReference(parameter.type, parameter.location, fail)
130
+
131
+ declareBinding(parameter.name, {mutable: false, type}, parameter.location, scope, fail)
132
+ }
133
+
134
+ const returnType = validateTypeReference(declaration.returnType, declaration.location, fail)
135
+ const returns = validateBlock(declaration.body, scope, returnType, functions, fail, normalizeOperations)
136
+
137
+ if (!returns) fail("MISSING_RETURN", `Function '${declaration.name}' does not return on every reachable path.`, declaration.location)
138
+ }
139
+
140
+ /**
141
+ * Validates one ordered block and reports whether every path returns.
142
+ * @param {import("./types.js").Block} block - Semantic block.
143
+ * @param {Scope} scope - Scope belonging to this block.
144
+ * @param {import("./types.js").SemanticTypeName | undefined} returnType - Function return type, absent for entry points.
145
+ * @param {Map<string, import("./types.js").FunctionDeclaration>} functions - Function signatures.
146
+ * @param {SemanticFail} fail - Diagnostic callback.
147
+ * @param {boolean} normalizeOperations - Whether to replace transient frontend operation intent.
148
+ * @returns {boolean} Whether every path through the block returns.
149
+ */
150
+ function validateBlock(block, scope, returnType, functions, fail, normalizeOperations) {
151
+ let alwaysReturns = false
152
+
153
+ for (const statement of block.statements) {
154
+ if (alwaysReturns) fail("UNREACHABLE_STATEMENT", "Statement is unreachable.", statement.location)
155
+
156
+ if (statement.kind == "LocalDeclaration") {
157
+ const declaredType = validateTypeReference(statement.type, statement.location, fail)
158
+ const initializerType = inferExpressionType(statement.initializer, scope, functions, fail, normalizeOperations)
159
+
160
+ scope.pending.delete(statement.name)
161
+ if (initializerType != declaredType) {
162
+ fail("TYPE_MISMATCH", `Initializer type ${initializerType}; expected ${declaredType}.`, statement.initializer.location)
163
+ }
164
+ declareBinding(statement.name, {mutable: statement.mutable, type: declaredType}, statement.location, scope, fail)
165
+ continue
166
+ }
167
+ if (statement.kind == "AssignmentStatement") {
168
+ const expressionType = inferExpressionType(statement.expression, scope, functions, fail, normalizeOperations)
169
+ const binding = resolveBinding(statement.target.name, statement.target.location, scope, fail)
170
+
171
+ if (!binding.mutable) {
172
+ fail("IMMUTABLE_ASSIGNMENT", `Cannot assign to immutable binding '${statement.target.name}'.`, statement.target.location)
173
+ }
174
+ if (expressionType != binding.type) {
175
+ fail("TYPE_MISMATCH", `Assignment type ${expressionType}; expected ${binding.type}.`, statement.expression.location)
176
+ }
177
+ continue
178
+ }
179
+ if (statement.kind == "PrintStatement") {
180
+ inferExpressionType(statement.expression, scope, functions, fail, normalizeOperations)
181
+ continue
182
+ }
183
+ if (statement.kind == "ReturnStatement") {
184
+ if (!returnType) fail("ILLEGAL_RETURN_CONTEXT", "Return statement outside a function.", statement.location)
185
+ const actualType = inferExpressionType(statement.expression, scope, functions, fail, normalizeOperations)
186
+
187
+ if (actualType != returnType) {
188
+ fail("TYPE_MISMATCH", `Return type ${actualType}; expected ${returnType}.`, statement.expression.location)
189
+ }
190
+ alwaysReturns = true
191
+ continue
192
+ }
193
+ if (statement.kind == "IfStatement") {
194
+ const conditionType = inferExpressionType(statement.condition, scope, functions, fail, normalizeOperations)
195
+
196
+ if (conditionType != "boolean") {
197
+ fail("NON_BOOLEAN_CONDITION", `If condition type ${conditionType}; expected boolean.`, statement.condition.location)
198
+ }
199
+ const consequentScope = createScope(scope, statement.consequent.statements)
200
+ const consequentReturns = validateBlock(statement.consequent, consequentScope, returnType, functions, fail, normalizeOperations)
201
+ let alternateReturns = false
202
+
203
+ if (statement.alternate) {
204
+ const alternateScope = createScope(scope, statement.alternate.statements)
205
+
206
+ alternateReturns = validateBlock(statement.alternate, alternateScope, returnType, functions, fail, normalizeOperations)
207
+ }
208
+ alwaysReturns = consequentReturns && alternateReturns
209
+ continue
210
+ }
211
+
212
+ const unexpected = /** @type {{kind: string, location: import("./types.js").SourceLocation}} */ (statement)
213
+
214
+ fail("UNSUPPORTED_STATEMENT", unexpected.kind, unexpected.location)
215
+ }
216
+
217
+ return alwaysReturns
218
+ }
219
+
220
+ /**
221
+ * Creates a lexical scope with declarations marked pending until visited.
222
+ * @param {Scope | undefined} parent - Enclosing scope.
223
+ * @param {{kind: string, name?: string}[]} statements - Scope statements.
224
+ * @param {Set<string>} [reservedNames] - Names unavailable to root bindings.
225
+ * @returns {Scope} Scope.
226
+ */
227
+ function createScope(parent, statements, reservedNames = new Set()) {
228
+ const pending = new Set(statements.filter((statement) => statement.kind == "LocalDeclaration")
229
+ .map((statement) => /** @type {{name: string}} */ (statement).name))
230
+
231
+ return {bindings: new Map(), parent, pending, usedNames: parent?.usedNames ?? new Set(reservedNames)}
232
+ }
233
+
234
+ /**
235
+ * Adds one binding while rejecting duplicates and shadowing.
236
+ * @param {string} name - Binding name.
237
+ * @param {Binding} binding - Binding metadata.
238
+ * @param {import("./types.js").SourceLocation} location - Declaration location.
239
+ * @param {Scope} scope - Current scope.
240
+ * @param {SemanticFail} fail - Diagnostic callback.
241
+ * @returns {void}
242
+ */
243
+ function declareBinding(name, binding, location, scope, fail) {
244
+ if (scope.usedNames.has(name)) fail("DUPLICATE_BINDING", `Duplicate or shadowed binding '${name}'.`, location)
245
+
246
+ scope.bindings.set(name, binding)
247
+ scope.usedNames.add(name)
248
+ }
249
+
250
+ /**
251
+ * Resolves one identifier and distinguishes use-before-declaration.
252
+ * @param {string} name - Binding name.
253
+ * @param {import("./types.js").SourceLocation} location - Reference location.
254
+ * @param {Scope} scope - Current scope.
255
+ * @param {SemanticFail} fail - Diagnostic callback.
256
+ * @returns {Binding} Binding.
257
+ */
258
+ function resolveBinding(name, location, scope, fail) {
259
+ for (let current = /** @type {Scope | undefined} */ (scope); current; current = current.parent) {
260
+ const binding = current.bindings.get(name)
261
+
262
+ if (binding) return binding
263
+ if (current.pending.has(name)) {
264
+ return fail("USE_BEFORE_DECLARATION", `Binding '${name}' is used before its declaration.`, location)
265
+ }
266
+ }
267
+
268
+ return fail("UNRESOLVED_BINDING", `Unknown binding '${name}'.`, location)
269
+ }
270
+
271
+ /**
272
+ * Validates one semantic type reference.
273
+ * @param {unknown} type - Candidate semantic type reference.
274
+ * @param {import("./types.js").SourceLocation} location - Owning source location.
275
+ * @param {SemanticFail} fail - Diagnostic callback.
276
+ * @returns {import("./types.js").SemanticTypeName} Validated scalar name.
277
+ */
278
+ function validateTypeReference(type, location, fail) {
279
+ if (!type || typeof type != "object" || Array.isArray(type)) {
280
+ return fail("TYPE_MISMATCH", "Unsupported scalar type.", location)
281
+ }
282
+
283
+ const candidate = /** @type {import("./types.js").TypeReference} */ (type)
284
+
285
+ if (candidate.kind != "TypeReference" || !isScalarTypeName(candidate.name)) {
286
+ fail("TYPE_MISMATCH", "Unsupported scalar type.", location)
287
+ }
288
+
289
+ return candidate.name
290
+ }
291
+
292
+ /**
293
+ * Infers and validates one expression.
294
+ * @param {import("./types.js").Expression} expression - Semantic expression.
295
+ * @param {Scope} scope - Visible lexical scope.
296
+ * @param {Map<string, import("./types.js").FunctionDeclaration>} functions - Module function signatures.
297
+ * @param {SemanticFail} fail - Diagnostic callback.
298
+ * @param {boolean} normalizeOperations - Whether to replace transient frontend operation intent.
299
+ * @returns {import("./types.js").SemanticTypeName} Expression type.
300
+ */
301
+ function inferExpressionType(expression, scope, functions, fail, normalizeOperations) {
302
+ if (expression.kind == "IdentifierExpression") return resolveBinding(expression.name, expression.location, scope, fail).type
303
+
304
+ if (expression.kind == "IntegerLiteral") {
305
+ if (!Number.isSafeInteger(expression.value)) fail("TYPE_MISMATCH", "Non-safe integer literal.", expression.location)
306
+ return "integer"
307
+ }
308
+ if (expression.kind == "BooleanLiteral") {
309
+ if (typeof Reflect.get(expression, "value") != "boolean") {
310
+ fail("TYPE_MISMATCH", "Invalid boolean literal value.", expression.location)
311
+ }
312
+ return "boolean"
313
+ }
314
+ if (expression.kind == "StringLiteral") {
315
+ const value = Reflect.get(expression, "value")
316
+
317
+ if (typeof value != "string" || !hasOnlyUnicodeScalars(value)) {
318
+ fail("TYPE_MISMATCH", "Invalid Unicode string literal.", expression.location)
319
+ }
320
+ return "string"
321
+ }
322
+ if (expression.kind == "CallExpression") {
323
+ const functionDeclaration = functions.get(expression.callee)
324
+
325
+ if (!functionDeclaration) return fail("UNRESOLVED_BINDING", `Unknown function '${expression.callee}'.`, expression.location)
326
+ if (expression.arguments.length != functionDeclaration.parameters.length) {
327
+ return fail("TYPE_MISMATCH", `Call argument count for '${expression.callee}'.`, expression.location)
328
+ }
329
+ for (let index = 0; index < expression.arguments.length; index++) {
330
+ const argument = expression.arguments[index]
331
+ const actualType = inferExpressionType(argument, scope, functions, fail, normalizeOperations)
332
+ const expectedType = validateTypeReference(functionDeclaration.parameters[index].type, functionDeclaration.parameters[index].location, fail)
333
+
334
+ if (actualType != expectedType) {
335
+ fail("TYPE_MISMATCH", `Call argument type ${actualType}; expected ${expectedType}.`, argument.location)
336
+ }
337
+ }
338
+ return validateTypeReference(functionDeclaration.returnType, functionDeclaration.location, fail)
339
+ }
340
+ if (expression.kind == "UnaryExpression") {
341
+ const operandType = inferExpressionType(expression.operand, scope, functions, fail, normalizeOperations)
342
+ const operation = normalizeOperations
343
+ ? normalizeUnaryOperation(adaptedOperationFor(expression), operandType, expression.operand.location, fail)
344
+ : expression.operation
345
+
346
+ if (normalizeOperations) setNormalizedOperation(expression, operation, unaryOperationSignatures[operation].result)
347
+
348
+ const signature = unaryOperationSignatures[operation]
349
+
350
+ if (!signature) return fail("TYPE_MISMATCH", `Unknown unary operation ${String(operation)}.`, expression.location)
351
+ if (operandType != signature.operand) {
352
+ return fail("INVALID_OPERAND_TYPE", `${operation} requires ${signature.operand}; received ${operandType}.`, expression.operand.location)
353
+ }
354
+ if (expression.type != signature.result) {
355
+ return fail("TYPE_MISMATCH", `${operation} result type ${String(expression.type)}; expected ${signature.result}.`, expression.location)
356
+ }
357
+ return signature.result
358
+ }
359
+ if (expression.kind == "BinaryExpression") {
360
+ const leftType = inferExpressionType(expression.left, scope, functions, fail, normalizeOperations)
361
+ const rightType = inferExpressionType(expression.right, scope, functions, fail, normalizeOperations)
362
+ const operation = normalizeOperations
363
+ ? normalizeBinaryOperation(adaptedOperationFor(expression), leftType, rightType, expression.left.location, expression.right.location, expression.location, fail)
364
+ : expression.operation
365
+
366
+ if (normalizeOperations) setNormalizedOperation(expression, operation, binaryOperationSignatures[operation].result)
367
+
368
+ const signature = binaryOperationSignatures[operation]
369
+
370
+ if (!signature) return fail("TYPE_MISMATCH", `Unknown binary operation ${String(operation)}.`, expression.location)
371
+ if (leftType != signature.left) {
372
+ return fail("INVALID_OPERAND_TYPE", `${operation} requires ${signature.left} left operand; received ${leftType}.`, expression.left.location)
373
+ }
374
+ if (rightType != signature.right) {
375
+ return fail("INVALID_OPERAND_TYPE", `${operation} requires ${signature.right} right operand; received ${rightType}.`, expression.right.location)
376
+ }
377
+ if (expression.type != signature.result) {
378
+ return fail("TYPE_MISMATCH", `${operation} result type ${String(expression.type)}; expected ${signature.result}.`, expression.location)
379
+ }
380
+ return signature.result
381
+ }
382
+
383
+ const unexpected = /** @type {{kind: string, location: import("./types.js").SourceLocation}} */ (expression)
384
+
385
+ return fail("TYPE_MISMATCH", unexpected.kind, unexpected.location)
386
+ }
387
+
388
+ /** @type {Readonly<Record<import("./types.js").SemanticUnaryOperation, {operand: import("./types.js").SemanticTypeName, result: import("./types.js").SemanticTypeName}>>} */
389
+ const unaryOperationSignatures = Object.freeze({
390
+ BooleanNot: Object.freeze({operand: "boolean", result: "boolean"}),
391
+ IntegerNegate: Object.freeze({operand: "integer", result: "integer"})
392
+ })
393
+
394
+ /** @type {Readonly<Record<import("./types.js").SemanticBinaryOperation, {left: import("./types.js").SemanticTypeName, right: import("./types.js").SemanticTypeName, result: import("./types.js").SemanticTypeName}>>} */
395
+ const binaryOperationSignatures = Object.freeze({
396
+ BooleanAnd: Object.freeze({left: "boolean", result: "boolean", right: "boolean"}),
397
+ BooleanEqual: Object.freeze({left: "boolean", result: "boolean", right: "boolean"}),
398
+ BooleanNotEqual: Object.freeze({left: "boolean", result: "boolean", right: "boolean"}),
399
+ BooleanOr: Object.freeze({left: "boolean", result: "boolean", right: "boolean"}),
400
+ IntegerAdd: Object.freeze({left: "integer", result: "integer", right: "integer"}),
401
+ IntegerEqual: Object.freeze({left: "integer", result: "boolean", right: "integer"}),
402
+ IntegerGreaterThan: Object.freeze({left: "integer", result: "boolean", right: "integer"}),
403
+ IntegerGreaterThanOrEqual: Object.freeze({left: "integer", result: "boolean", right: "integer"}),
404
+ IntegerLessThan: Object.freeze({left: "integer", result: "boolean", right: "integer"}),
405
+ IntegerLessThanOrEqual: Object.freeze({left: "integer", result: "boolean", right: "integer"}),
406
+ IntegerMultiply: Object.freeze({left: "integer", result: "integer", right: "integer"}),
407
+ IntegerNotEqual: Object.freeze({left: "integer", result: "boolean", right: "integer"}),
408
+ IntegerSubtract: Object.freeze({left: "integer", result: "integer", right: "integer"}),
409
+ StringConcat: Object.freeze({left: "string", result: "string", right: "string"}),
410
+ StringEqual: Object.freeze({left: "string", result: "boolean", right: "string"}),
411
+ StringNotEqual: Object.freeze({left: "string", result: "boolean", right: "string"})
412
+ })
413
+
414
+ /**
415
+ * Selects one typed unary meaning from parser-normalized source intent.
416
+ * @param {unknown} sourceOperation - Transient frontend operation.
417
+ * @param {import("./types.js").SemanticTypeName} operandType - Resolved operand type.
418
+ * @param {import("./types.js").SourceLocation} location - Operand location.
419
+ * @param {SemanticFail} fail - Diagnostic callback.
420
+ * @returns {import("./types.js").SemanticUnaryOperation} Semantic operation.
421
+ */
422
+ function normalizeUnaryOperation(sourceOperation, operandType, location, fail) {
423
+ if (sourceOperation == "Negate") {
424
+ if (operandType != "integer") fail("INVALID_OPERAND_TYPE", `Integer negation requires integer; received ${operandType}.`, location)
425
+ return "IntegerNegate"
426
+ }
427
+ if (sourceOperation == "Not") {
428
+ if (operandType != "boolean") fail("INVALID_OPERAND_TYPE", `Boolean not requires boolean; received ${operandType}.`, location)
429
+ return "BooleanNot"
430
+ }
431
+
432
+ return fail("TYPE_MISMATCH", `Unknown unary source operation ${String(sourceOperation)}.`, location)
433
+ }
434
+
435
+ /**
436
+ * Selects one typed binary meaning from parser-normalized source intent.
437
+ * @param {unknown} sourceOperation - Transient frontend operation.
438
+ * @param {import("./types.js").SemanticTypeName} leftType - Resolved left type.
439
+ * @param {import("./types.js").SemanticTypeName} rightType - Resolved right type.
440
+ * @param {import("./types.js").SourceLocation} leftLocation - Left operand location.
441
+ * @param {import("./types.js").SourceLocation} rightLocation - Right operand location.
442
+ * @param {import("./types.js").SourceLocation} expressionLocation - Whole operation location.
443
+ * @param {SemanticFail} fail - Diagnostic callback.
444
+ * @returns {import("./types.js").SemanticBinaryOperation} Semantic operation.
445
+ */
446
+ function normalizeBinaryOperation(sourceOperation, leftType, rightType, leftLocation, rightLocation, expressionLocation, fail) {
447
+ if (sourceOperation == "PhpAdd") {
448
+ if (leftType == "string" && rightType == "string") {
449
+ return fail("UNSUPPORTED_SYNTAX", "PHP binary + does not concatenate strings; use . instead.", expressionLocation)
450
+ }
451
+
452
+ sourceOperation = "Add"
453
+ }
454
+
455
+ if (sourceOperation == "StringConcat") {
456
+ if (leftType != "string") fail("INVALID_OPERAND_TYPE", `String concatenation requires string; received ${leftType}.`, leftLocation)
457
+ if (rightType != "string") fail("INVALID_OPERAND_TYPE", `String concatenation requires string; received ${rightType}.`, rightLocation)
458
+ return "StringConcat"
459
+ }
460
+
461
+ if (sourceOperation == "Add") {
462
+ if (leftType == "integer" && rightType == "integer") return "IntegerAdd"
463
+ if (leftType == "string" && rightType == "string") return "StringConcat"
464
+
465
+ const location = leftType == "integer" || leftType == "string" ? rightLocation : leftLocation
466
+
467
+ return fail("INVALID_OPERAND_TYPE", `Addition requires two integers or two strings; received ${leftType} and ${rightType}.`, location)
468
+ }
469
+
470
+ const fixedOperations = /** @type {const} */ ({
471
+ And: ["boolean", "BooleanAnd"],
472
+ GreaterThan: ["integer", "IntegerGreaterThan"],
473
+ GreaterThanOrEqual: ["integer", "IntegerGreaterThanOrEqual"],
474
+ LessThan: ["integer", "IntegerLessThan"],
475
+ LessThanOrEqual: ["integer", "IntegerLessThanOrEqual"],
476
+ Multiply: ["integer", "IntegerMultiply"],
477
+ Or: ["boolean", "BooleanOr"],
478
+ Subtract: ["integer", "IntegerSubtract"]
479
+ })
480
+ const fixed = Reflect.get(fixedOperations, String(sourceOperation))
481
+
482
+ if (fixed) {
483
+ const [requiredType, operation] = fixed
484
+
485
+ if (leftType != requiredType) fail("INVALID_OPERAND_TYPE", `${String(sourceOperation)} requires ${requiredType}; received ${leftType}.`, leftLocation)
486
+ if (rightType != requiredType) fail("INVALID_OPERAND_TYPE", `${String(sourceOperation)} requires ${requiredType}; received ${rightType}.`, rightLocation)
487
+
488
+ return /** @type {import("./types.js").SemanticBinaryOperation} */ (operation)
489
+ }
490
+
491
+ if (sourceOperation == "JavaEqual" || sourceOperation == "JavaNotEqual") {
492
+ if (leftType == "string" || rightType == "string") {
493
+ return fail("UNSUPPORTED_SYNTAX", "Java reference equality is outside the implemented semantic subset.", expressionLocation)
494
+ }
495
+
496
+ sourceOperation = sourceOperation == "JavaEqual" ? "Equal" : "NotEqual"
497
+ }
498
+
499
+ if (sourceOperation == "StringEqual" || sourceOperation == "StringNotEqual") {
500
+ if (leftType != "string") fail("INVALID_OPERAND_TYPE", `String equality requires string; received ${leftType}.`, leftLocation)
501
+ if (rightType != "string") fail("INVALID_OPERAND_TYPE", `String equality requires string; received ${rightType}.`, rightLocation)
502
+
503
+ return /** @type {"StringEqual" | "StringNotEqual"} */ (sourceOperation)
504
+ }
505
+
506
+ if (sourceOperation == "Equal" || sourceOperation == "NotEqual") {
507
+ if (leftType != rightType) {
508
+ return fail("MISMATCHED_EQUALITY_TYPES", `Equality requires matching scalar types; received ${leftType} and ${rightType}.`, rightLocation)
509
+ }
510
+
511
+ const prefix = leftType == "integer" ? "Integer" : leftType == "boolean" ? "Boolean" : "String"
512
+
513
+ return /** @type {import("./types.js").SemanticBinaryOperation} */ (`${prefix}${sourceOperation}`)
514
+ }
515
+
516
+ return fail("TYPE_MISMATCH", `Unknown binary source operation ${String(sourceOperation)}.`, leftLocation)
517
+ }
518
+
519
+ /**
520
+ * Replaces transient frontend intent with the public typed operation fields.
521
+ * @param {object} expression - Mutable adapted expression.
522
+ * @param {import("./types.js").SemanticUnaryOperation | import("./types.js").SemanticBinaryOperation} operation - Typed operation.
523
+ * @param {import("./types.js").SemanticTypeName} type - Explicit result type.
524
+ * @returns {void}
525
+ */
526
+ function setNormalizedOperation(expression, operation, type) {
527
+ Reflect.set(expression, "operation", operation)
528
+ Reflect.set(expression, "type", type)
529
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "allowJs": true,
4
+ "checkJs": true,
5
+ "declaration": true,
6
+ "declarationMap": true,
7
+ "inlineSourceMap": true,
8
+ "inlineSources": true,
9
+ "outDir": "build",
10
+ "target": "ES2024",
11
+ "moduleResolution": "nodenext",
12
+ "module": "nodenext",
13
+ "strict": true
14
+ },
15
+ "include": ["index.js", "src/**/*"]
16
+ }