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.
- package/LICENSE +15 -0
- package/README.md +71 -0
- package/build/index.d.ts +54 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +45 -0
- package/build/src/backends/identifiers.d.ts +19 -0
- package/build/src/backends/identifiers.d.ts.map +1 -0
- package/build/src/backends/identifiers.js +93 -0
- package/build/src/backends/index.d.ts +35 -0
- package/build/src/backends/index.d.ts.map +1 -0
- package/build/src/backends/index.js +149 -0
- package/build/src/backends/java.d.ts +8 -0
- package/build/src/backends/java.d.ts.map +1 -0
- package/build/src/backends/java.js +172 -0
- package/build/src/backends/javascript.d.ts +8 -0
- package/build/src/backends/javascript.d.ts.map +1 -0
- package/build/src/backends/javascript.js +173 -0
- package/build/src/backends/php.d.ts +8 -0
- package/build/src/backends/php.d.ts.map +1 -0
- package/build/src/backends/php.js +175 -0
- package/build/src/backends/ruby.d.ts +8 -0
- package/build/src/backends/ruby.d.ts.map +1 -0
- package/build/src/backends/ruby.js +155 -0
- package/build/src/backends/scalars.d.ts +15 -0
- package/build/src/backends/scalars.d.ts.map +1 -0
- package/build/src/backends/scalars.js +138 -0
- package/build/src/backends/shared.d.ts +26 -0
- package/build/src/backends/shared.d.ts.map +1 -0
- package/build/src/backends/shared.js +424 -0
- package/build/src/backends/typescript.d.ts +8 -0
- package/build/src/backends/typescript.d.ts.map +1 -0
- package/build/src/backends/typescript.js +160 -0
- package/build/src/backends/writer.d.ts +87 -0
- package/build/src/backends/writer.d.ts.map +1 -0
- package/build/src/backends/writer.js +207 -0
- package/build/src/diagnostic.d.ts +67 -0
- package/build/src/diagnostic.d.ts.map +1 -0
- package/build/src/diagnostic.js +96 -0
- package/build/src/frontends/index.d.ts +14 -0
- package/build/src/frontends/index.d.ts.map +1 -0
- package/build/src/frontends/index.js +36 -0
- package/build/src/frontends/java.d.ts +12 -0
- package/build/src/frontends/java.d.ts.map +1 -0
- package/build/src/frontends/java.js +630 -0
- package/build/src/frontends/javascript-typescript.d.ts +19 -0
- package/build/src/frontends/javascript-typescript.d.ts.map +1 -0
- package/build/src/frontends/javascript-typescript.js +633 -0
- package/build/src/frontends/php.d.ts +12 -0
- package/build/src/frontends/php.d.ts.map +1 -0
- package/build/src/frontends/php.js +523 -0
- package/build/src/frontends/ruby.d.ts +12 -0
- package/build/src/frontends/ruby.d.ts.map +1 -0
- package/build/src/frontends/ruby.js +515 -0
- package/build/src/frontends/scalars.d.ts +19 -0
- package/build/src/frontends/scalars.d.ts.map +1 -0
- package/build/src/frontends/scalars.js +71 -0
- package/build/src/mapping.d.ts +160 -0
- package/build/src/mapping.d.ts.map +1 -0
- package/build/src/mapping.js +1120 -0
- package/build/src/semantic/location.d.ts +61 -0
- package/build/src/semantic/location.d.ts.map +1 -0
- package/build/src/semantic/location.js +142 -0
- package/build/src/semantic/operators.d.ts +16 -0
- package/build/src/semantic/operators.d.ts.map +1 -0
- package/build/src/semantic/operators.js +24 -0
- package/build/src/semantic/provenance.d.ts +74 -0
- package/build/src/semantic/provenance.d.ts.map +1 -0
- package/build/src/semantic/provenance.js +577 -0
- package/build/src/semantic/scalars.d.ts +19 -0
- package/build/src/semantic/scalars.d.ts.map +1 -0
- package/build/src/semantic/scalars.js +40 -0
- package/build/src/semantic/types.d.ts +1003 -0
- package/build/src/semantic/types.d.ts.map +1 -0
- package/build/src/semantic/types.js +279 -0
- package/build/src/semantic/validate.d.ts +60 -0
- package/build/src/semantic/validate.d.ts.map +1 -0
- package/build/src/semantic/validate.js +472 -0
- package/docs/architecture.md +39 -0
- package/docs/coding-standards.md +7 -0
- package/docs/goals.md +7 -0
- package/docs/language-support.md +69 -0
- package/docs/plans/2026-09-02-initial-toolchain.md +54 -0
- package/docs/source-maps.md +134 -0
- package/docs/testing.md +25 -0
- package/index.js +63 -0
- package/package.json +69 -0
- package/src/backends/identifiers.js +101 -0
- package/src/backends/index.js +159 -0
- package/src/backends/java.js +182 -0
- package/src/backends/javascript.js +184 -0
- package/src/backends/php.js +184 -0
- package/src/backends/ruby.js +163 -0
- package/src/backends/scalars.js +120 -0
- package/src/backends/shared.js +456 -0
- package/src/backends/typescript.js +168 -0
- package/src/backends/writer.js +224 -0
- package/src/diagnostic.js +103 -0
- package/src/frontends/index.js +36 -0
- package/src/frontends/java.js +735 -0
- package/src/frontends/javascript-typescript.js +718 -0
- package/src/frontends/php.js +593 -0
- package/src/frontends/ruby.js +622 -0
- package/src/frontends/scalars.js +77 -0
- package/src/mapping.js +1260 -0
- package/src/semantic/location.js +154 -0
- package/src/semantic/operators.js +28 -0
- package/src/semantic/provenance.js +629 -0
- package/src/semantic/scalars.js +44 -0
- package/src/semantic/types.js +319 -0
- package/src/semantic/validate.js +529 -0
- 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
|
+
}
|