semantifold 0.10.0 → 0.11.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 (41) hide show
  1. package/README.md +1 -1
  2. package/build/src/backends/kotlin.d.ts +1 -1
  3. package/build/src/backends/kotlin.js +2 -2
  4. package/build/src/backends/shared.d.ts.map +1 -1
  5. package/build/src/backends/shared.js +29 -1
  6. package/build/src/frontends/java.d.ts.map +1 -1
  7. package/build/src/frontends/java.js +23 -1
  8. package/build/src/frontends/javascript-typescript.d.ts.map +1 -1
  9. package/build/src/frontends/javascript-typescript.js +45 -4
  10. package/build/src/frontends/php.d.ts.map +1 -1
  11. package/build/src/frontends/php.js +54 -3
  12. package/build/src/frontends/ruby.d.ts.map +1 -1
  13. package/build/src/frontends/ruby.js +55 -5
  14. package/build/src/language-registry.d.ts.map +1 -1
  15. package/build/src/language-registry.js +18 -6
  16. package/build/src/semantic/effects.js +3 -1
  17. package/build/src/semantic/provenance.d.ts.map +1 -1
  18. package/build/src/semantic/provenance.js +11 -1
  19. package/build/src/semantic/types.d.ts +52 -2
  20. package/build/src/semantic/types.d.ts.map +1 -1
  21. package/build/src/semantic/types.js +14 -2
  22. package/build/src/semantic/validate.js +18 -1
  23. package/build/src/toolchains.js +3 -3
  24. package/docs/adr-expression-breadth.md +186 -0
  25. package/docs/kotlin.md +2 -2
  26. package/docs/language-support.md +11 -3
  27. package/docs/parser-qualification.md +1 -1
  28. package/docs/testing.md +2 -2
  29. package/package.json +1 -1
  30. package/src/backends/kotlin.js +1 -1
  31. package/src/backends/shared.js +30 -0
  32. package/src/frontends/java.js +24 -0
  33. package/src/frontends/javascript-typescript.js +49 -2
  34. package/src/frontends/php.js +61 -2
  35. package/src/frontends/ruby.js +62 -2
  36. package/src/language-registry.js +17 -5
  37. package/src/semantic/effects.js +1 -0
  38. package/src/semantic/provenance.js +8 -0
  39. package/src/semantic/types.js +14 -1
  40. package/src/semantic/validate.js +26 -0
  41. package/src/toolchains.js +2 -2
@@ -37,6 +37,7 @@ const task012Languages = new Set(["php", "ruby", "javascript", "typescript", "ja
37
37
  const task014Languages = new Set(["php", "ruby", "javascript", "typescript", "java"])
38
38
  const task032Languages = new Set(["php", "ruby", "javascript", "typescript", "java"])
39
39
  const task033Languages = new Set(["php", "ruby", "javascript", "typescript", "java"])
40
+ const task048ConditionalLanguages = new Set(["php", "ruby", "javascript", "typescript", "java"])
40
41
  const javaReferenceMethodHooks = new Set(["clone", "equals", "finalize", "getClass", "hashCode", "notify", "notifyAll", "toString", "wait"])
41
42
  const javascriptReferenceMethodHooks = new Set([
42
43
  "__defineGetter__", "__defineSetter__", "__lookupGetter__", "__lookupSetter__", "__proto__", "constructor",
@@ -1129,6 +1130,11 @@ function expressionContainsKind(expression, kind) {
1129
1130
  if (expression.kind == "BinaryExpression") {
1130
1131
  return expressionContainsKind(expression.left, kind) || expressionContainsKind(expression.right, kind)
1131
1132
  }
1133
+ if (expression.kind == "ConditionalExpression") {
1134
+ return expressionContainsKind(expression.condition, kind) ||
1135
+ expressionContainsKind(expression.consequent, kind) ||
1136
+ expressionContainsKind(expression.alternate, kind)
1137
+ }
1132
1138
  if (expression.kind == "CallExpression") return expression.arguments.some((argument) => expressionContainsKind(argument, kind))
1133
1139
  if (expression.kind == "MethodCallExpression") {
1134
1140
  return expressionContainsKind(expression.receiver, kind) ||
@@ -1936,6 +1942,19 @@ function validateExpression(expression, language, ownerLocation, allowJavaNegate
1936
1942
  return
1937
1943
  }
1938
1944
 
1945
+ if (candidate.kind == "ConditionalExpression") {
1946
+ const fields = Object.keys(candidate).filter((key) => key != "sourceProvenance").sort().join(",")
1947
+
1948
+ if (fields != "alternate,condition,consequent,kind,location,type") {
1949
+ unsupportedCapability(language, "malformed ConditionalExpression", location)
1950
+ }
1951
+ if (!task048ConditionalLanguages.has(language)) unsupportedCapability(language, "Task 048 conditional expression", location)
1952
+ validateExpression(candidate.condition, language, location, false, expressionPath, externalDeclarationIds)
1953
+ validateExpression(candidate.consequent, language, location, false, expressionPath, externalDeclarationIds)
1954
+ validateExpression(candidate.alternate, language, location, false, expressionPath, externalDeclarationIds)
1955
+ return
1956
+ }
1957
+
1939
1958
  unsupportedCapability(language, String(Reflect.get(expression, "kind")), location)
1940
1959
  }
1941
1960
 
@@ -2364,6 +2383,17 @@ export function emitExpression(writer, expression, path, language, emitIdentifie
2364
2383
  return
2365
2384
  }
2366
2385
 
2386
+ if (expression.kind == "ConditionalExpression") {
2387
+ writer.mapped("(", {mappingKind: "anchor", node: expression, path})
2388
+ emitExpression(writer, expression.condition, `${path}/condition`, language, emitIdentifier)
2389
+ writer.mapped(" ? ", {mappingKind: "exact", node: expression, path, role: "operator"})
2390
+ emitExpression(writer, expression.consequent, `${path}/consequent`, language, emitIdentifier)
2391
+ writer.mapped(" : ", {mappingKind: "exact", node: expression, path, role: "colon"})
2392
+ emitExpression(writer, expression.alternate, `${path}/alternate`, language, emitIdentifier)
2393
+ writer.mapped(")", {mappingKind: "anchor", node: expression, path})
2394
+ return
2395
+ }
2396
+
2367
2397
  if (expression.kind == "UnaryExpression") {
2368
2398
  if (language == "dart" && expression.operation == "IntegerNegate") {
2369
2399
  emitDartIntegerHelper(writer, expression, path, "_semantifoldIntegerNegate", [
@@ -329,6 +329,29 @@ function convertExpression(node, filename, source, context, expectedType) {
329
329
  return /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (semantic))
330
330
  }
331
331
 
332
+ if (node.name == "TernaryExpression") {
333
+ const children = directChildren(node)
334
+ const question = children.find((child) => child.name == "?" || (child.name == "LogicOp" && nodeText(child, source) == "?"))
335
+ const colon = children.find((child) => child.name == ":" || (child.name == "LogicOp" && nodeText(child, source) == ":"))
336
+ const named = children.filter((child) => child !== question && child !== colon)
337
+
338
+ if (!question || !colon || named.length != 3) return unsupportedSyntax("java", "ternary expression", location)
339
+
340
+ return /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (withParserRanges({
341
+ alternate: convertExpression(named[2], filename, source, context),
342
+ condition: convertExpression(named[0], filename, source, context),
343
+ consequent: convertExpression(named[1], filename, source, context),
344
+ kind: /** @type {const} */ ("ConditionalExpression"),
345
+ location
346
+ }, {
347
+ alternate: nodeLocation(named[2], filename, source),
348
+ colon: nodeLocation(colon, filename, source),
349
+ condition: nodeLocation(named[0], filename, source),
350
+ consequent: nodeLocation(named[1], filename, source),
351
+ operator: nodeLocation(question, filename, source)
352
+ })))
353
+ }
354
+
332
355
  if (node.name == "MethodInvocation") {
333
356
  if (isEqualsInvocation(node, source) && !isZeroArgumentRecordMemberInvocation(node, filename, source, context)) {
334
357
  const methodName = requiredChild(node, "MethodName", filename, source)
@@ -626,6 +649,7 @@ function isSupportedExpressionNode(node) {
626
649
  "StringLiteral",
627
650
  "UnaryExpression",
628
651
  "BinaryExpression",
652
+ "TernaryExpression",
629
653
  "MethodInvocation",
630
654
  "ObjectCreationExpression",
631
655
  "FieldAccess",
@@ -7,7 +7,7 @@ import {blockReferencesProtectedEntry} from "../semantic/effects.js"
7
7
  import {locationFromOffsets, moduleLocation} from "../semantic/location.js"
8
8
  import {withAdaptedOperation} from "../semantic/operators.js"
9
9
  import {withParserRanges} from "../semantic/provenance.js"
10
- import {hasOnlyUnicodeScalars} from "../semantic/scalars.js"
10
+ import {hasOnlyUnicodeScalars, scalarType} from "../semantic/scalars.js"
11
11
  import {isTask034ResourceProbe} from "../semantic/capabilities.js"
12
12
  import {hasExactEffectSupport} from "../backends/effects.js"
13
13
  import {requireSourceReturnType, requireSourceScalarType} from "./scalars.js"
@@ -431,7 +431,38 @@ function convertExpression(node, language, filename, source, context = {bindings
431
431
  }
432
432
 
433
433
  if (node.type == "TemplateLiteral") {
434
- if (node.expressions.length > 0) return unsupportedSyntax(language, "interpolated string", location)
434
+ if (node.expressions.length > 0) {
435
+ /** @type {import("../semantic/types.js").Expression[]} */
436
+ const ordered = []
437
+
438
+ node.quasis.forEach((quasi, index) => {
439
+ if (typeof quasi.value.cooked != "string" || !hasOnlyUnicodeScalars(quasi.value.cooked)) {
440
+ return unsupportedSyntax(language, "invalid Unicode string literal", nodeLocation(quasi, filename, source))
441
+ }
442
+ const quasiLocation = nodeLocation(quasi, filename, source)
443
+
444
+ if (quasi.value.cooked.length > 0) ordered.push(withParserRanges({kind: /** @type {const} */ ("StringLiteral"), location: quasiLocation, value: quasi.value.cooked}, {literal: quasiLocation}))
445
+ if (index < node.expressions.length) {
446
+ ordered.push(convertExpression(/** @type {import("@babel/types").Expression} */ (node.expressions[index]), language, filename, source, context, scalarType("string")))
447
+ }
448
+ })
449
+ if (ordered.length == 0) return unsupportedSyntax(language, "interpolated string", location)
450
+
451
+ let lowered = ordered[0]
452
+
453
+ for (const part of ordered.slice(1)) {
454
+ const semantic = withAdaptedOperation({
455
+ kind: /** @type {const} */ ("BinaryExpression"),
456
+ left: lowered,
457
+ location,
458
+ right: part
459
+ }, /** @type {import("../semantic/operators.js").AdaptedOperation} */ ("StringConcat"))
460
+
461
+ lowered = /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (semantic))
462
+ }
463
+
464
+ return lowered
465
+ }
435
466
 
436
467
  const quasi = node.quasis[0]
437
468
 
@@ -442,6 +473,22 @@ function convertExpression(node, language, filename, source, context = {bindings
442
473
  return withParserRanges({kind: /** @type {const} */ ("StringLiteral"), location, value: quasi.value.cooked}, {literal: location})
443
474
  }
444
475
 
476
+ if (node.type == "ConditionalExpression") {
477
+ return /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (withParserRanges({
478
+ alternate: convertExpression(node.alternate, language, filename, source, context),
479
+ condition: convertExpression(node.test, language, filename, source, context),
480
+ consequent: convertExpression(node.consequent, language, filename, source, context),
481
+ kind: /** @type {const} */ ("ConditionalExpression"),
482
+ location
483
+ }, {
484
+ alternate: nodeLocation(node.alternate, filename, source),
485
+ colon: tokenLocation(node, ":", node.consequent.end ?? node.test.end ?? node.start ?? 0, node.alternate.start ?? node.end ?? source.length, filename, source),
486
+ condition: nodeLocation(node.test, filename, source),
487
+ consequent: nodeLocation(node.consequent, filename, source),
488
+ operator: tokenLocation(node, "?", node.test.end ?? node.start ?? 0, node.consequent.start ?? node.end ?? source.length, filename, source)
489
+ })))
490
+ }
491
+
445
492
  if (node.type == "UnaryExpression" && ["!", "-"].includes(node.operator)) {
446
493
  const operand = convertExpression(node.argument, language, filename, source, context)
447
494
  const semantic = withAdaptedOperation(withParserRanges({
@@ -222,9 +222,37 @@ function convertExpression(node, filename, source, context, expectedType, preser
222
222
 
223
223
  if (node.kind == "encapsed") {
224
224
  const literal = /** @type {import("php-parser").Encapsed} */ (node)
225
- const detail = literal.type == "string" ? "interpolated string" : `${literal.type} string`
226
225
 
227
- return unsupportedSyntax("php", detail, location)
226
+ if (literal.type != "string") return unsupportedSyntax("php", `${literal.type} string`, location)
227
+ if (!Array.isArray(literal.value)) return unsupportedSyntax("php", "interpolated string", location)
228
+
229
+ const parts = literal.value.map((part) => {
230
+ if (part.kind != "encapsedpart") return unsupportedSyntax("php", `interpolated string part ${part.kind}`, nodeLocation(part, filename, source))
231
+ const encapsedPart = /** @type {import("php-parser").EncapsedPart} */ (part)
232
+
233
+ if (!encapsedPart.expression) return unsupportedSyntax("php", "interpolated string part without expression", nodeLocation(encapsedPart, filename, source))
234
+
235
+ return convertExpression(encapsedPart.expression, filename, source, context, scalarType("string"))
236
+ })
237
+
238
+ if (parts.length == 0) {
239
+ return withParserRanges({kind: /** @type {const} */ ("StringLiteral"), location, value: ""}, {literal: location})
240
+ }
241
+
242
+ let lowered = parts[0]
243
+
244
+ for (const part of parts.slice(1)) {
245
+ const semantic = withAdaptedOperation({
246
+ kind: /** @type {const} */ ("BinaryExpression"),
247
+ left: lowered,
248
+ location,
249
+ right: part
250
+ }, /** @type {import("../semantic/operators.js").AdaptedOperation} */ ("StringConcat"))
251
+
252
+ lowered = /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (semantic))
253
+ }
254
+
255
+ return withParserRanges(lowered, {literal: location})
228
256
  }
229
257
 
230
258
  if (node.kind == "nowdoc") return unsupportedSyntax("php", "nowdoc string", location)
@@ -414,6 +442,37 @@ function convertExpression(node, filename, source, context, expectedType, preser
414
442
  return /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (semantic))
415
443
  }
416
444
 
445
+ if (node.kind == "retif") {
446
+ const ternary = /** @type {import("php-parser").RetIf} */ (node)
447
+
448
+ if (!ternary.test || !ternary.trueExpr || !ternary.falseExpr) return unsupportedSyntax("php", "short ternary", location)
449
+ if (ternary.test.kind == "variable") {
450
+ const tested = /** @type {import("php-parser").Variable} */ (ternary.test)
451
+
452
+ if (typeof tested.name == "string" && context.bindings.get(tested.name)?.kind == "OptionalType") {
453
+ return unsupportedSyntax("php", "optional truthiness condition", nodeLocation(tested, filename, source))
454
+ }
455
+ }
456
+
457
+ return /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (withParserRanges({
458
+ alternate: convertExpression(ternary.falseExpr, filename, source, context),
459
+ condition: convertExpression(ternary.test, filename, source, context),
460
+ consequent: convertExpression(ternary.trueExpr, filename, source, context),
461
+ kind: /** @type {const} */ ("ConditionalExpression"),
462
+ location
463
+ }, {
464
+ alternate: tokenLocation(":", ternary.trueExpr.loc?.end.offset ?? 0, ternary.falseExpr.loc?.start.offset ?? source.length, filename, source),
465
+ colon: tokenLocation(":", ternary.trueExpr.loc?.end.offset ?? 0, ternary.falseExpr.loc?.start.offset ?? source.length, filename, source),
466
+ condition: ternary.test.loc ? nodeLocation(ternary.test, filename, source) : location,
467
+ consequent: ternary.trueExpr.loc ? nodeLocation(ternary.trueExpr, filename, source) : location,
468
+ operator: tokenLocation("?", ternary.test.loc?.end.offset ?? 0, ternary.trueExpr.loc?.start.offset ?? source.length, filename, source)
469
+ })))
470
+ }
471
+
472
+ if (node.kind == "nullsafepropertylookup" || node.kind == "nullsafemethodcall") {
473
+ return unsupportedSyntax("php", "safe navigation", location)
474
+ }
475
+
417
476
  if (node.kind == "bin") {
418
477
  const binary = /** @type {import("php-parser").Bin} */ (node)
419
478
  if (binary.type == "!==" && binary.left.kind == "variable" && binary.right.kind == "nullkeyword") {
@@ -14,6 +14,7 @@ import {
14
14
  ConstantReadNode,
15
15
  DefNode,
16
16
  ElseNode,
17
+ EmbeddedStatementsNode,
17
18
  FalseNode,
18
19
  HashNode,
19
20
  IfNode,
@@ -49,7 +50,7 @@ import {blockReferencesProtectedEntry} from "../semantic/effects.js"
49
50
  import {locationFromOffsets, moduleLocation, utf8ByteOffsetToUtf16Offset} from "../semantic/location.js"
50
51
  import {withAdaptedOperation} from "../semantic/operators.js"
51
52
  import {withParserRanges} from "../semantic/provenance.js"
52
- import {hasOnlyUnicodeScalars} from "../semantic/scalars.js"
53
+ import {hasOnlyUnicodeScalars, scalarType} from "../semantic/scalars.js"
53
54
  import {isTask034ResourceProbe} from "../semantic/capabilities.js"
54
55
  import {hasExactEffectSupport} from "../backends/effects.js"
55
56
  import {requireSourceReturnType} from "./scalars.js"
@@ -207,7 +208,65 @@ function convertExpression(node, filename, source, context, expectedType, preser
207
208
  return withParserRanges({kind: /** @type {const} */ ("StringLiteral"), location, value: decoded.value}, {literal: location})
208
209
  }
209
210
 
210
- if (node instanceof InterpolatedStringNode) return unsupportedSyntax("ruby", "interpolated string", location)
211
+ if (node instanceof InterpolatedStringNode) {
212
+ if (node.openingLoc && slicePrismSource(source, node.openingLoc.startOffset, node.openingLoc.startOffset + node.openingLoc.length).startsWith("<<")) {
213
+ return unsupportedSyntax("ruby", "heredoc string", location)
214
+ }
215
+
216
+ const parts = node.parts.map((part) => {
217
+ if (part instanceof StringNode) return convertExpression(part, filename, source, context, scalarType("string"))
218
+ if (part instanceof EmbeddedStatementsNode) {
219
+ const statements = part.statements
220
+
221
+ if (!statements || statements.body.length != 1) return unsupportedSyntax("ruby", "interpolated statement", nodeLocation(part, filename, source))
222
+
223
+ return convertExpression(statements.body[0], filename, source, context, scalarType("string"))
224
+ }
225
+
226
+ return unsupportedSyntax("ruby", part.constructor.name, nodeLocation(part, filename, source))
227
+ })
228
+
229
+ if (parts.length == 0) return withParserRanges({kind: /** @type {const} */ ("StringLiteral"), location, value: ""}, {literal: location})
230
+
231
+ let lowered = parts[0]
232
+
233
+ for (const part of parts.slice(1)) {
234
+ const semantic = withAdaptedOperation({
235
+ kind: /** @type {const} */ ("BinaryExpression"),
236
+ left: lowered,
237
+ location,
238
+ right: part
239
+ }, /** @type {import("../semantic/operators.js").AdaptedOperation} */ ("StringConcat"))
240
+
241
+ lowered = /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (semantic))
242
+ }
243
+
244
+ return withParserRanges(lowered, {literal: location})
245
+ }
246
+
247
+ if (node instanceof IfNode && !node.ifKeywordLoc) {
248
+ const elseClause = node.subsequent
249
+ const consequentStatements = node.statements
250
+ const consequentArm = consequentStatements && consequentStatements.body.length == 1 ? consequentStatements.body[0] : undefined
251
+
252
+ if (!(elseClause instanceof ElseNode) || !elseClause.statements || elseClause.statements.body.length != 1 || !consequentArm || !node.thenKeywordLoc) {
253
+ return unsupportedSyntax("ruby", "ternary without alternate", location)
254
+ }
255
+
256
+ return /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (withParserRanges({
257
+ alternate: convertExpression(elseClause.statements.body[0], filename, source, context),
258
+ condition: convertExpression(node.predicate, filename, source, context),
259
+ consequent: convertExpression(consequentArm, filename, source, context),
260
+ kind: /** @type {const} */ ("ConditionalExpression"),
261
+ location
262
+ }, {
263
+ alternate: nodeLocation(elseClause.statements.body[0], filename, source),
264
+ colon: prismLocation(elseClause.elseKeywordLoc ?? elseClause.location, filename, source),
265
+ condition: nodeLocation(node.predicate, filename, source),
266
+ consequent: nodeLocation(consequentArm, filename, source),
267
+ operator: prismLocation(node.thenKeywordLoc, filename, source)
268
+ })))
269
+ }
211
270
 
212
271
  if (node instanceof OrNode && node.left instanceof LocalVariableReadNode &&
213
272
  context.bindings.get(node.left.name)?.kind == "OptionalType") {
@@ -863,6 +922,7 @@ function convertStatement(node, comments, context, filename, source) {
863
922
  if ((context.loopDepth ?? 0) > 0) return unsupportedSyntax("ruby", "nonlocal return from each block", nodeLocation(node, filename, source))
864
923
  return convertReturn(node, filename, source, context)
865
924
  }
925
+ if (node instanceof IfNode && !node.ifKeywordLoc) return unsupportedSyntax("ruby", "ternary statement", nodeLocation(node, filename, source))
866
926
  if (node instanceof IfNode || node instanceof UnlessNode) return convertIf(node, comments, context, filename, source)
867
927
  if (node instanceof WhileNode) return convertWhile(node, comments, context, filename, source)
868
928
  if (node instanceof BeginNode) return convertRubyTry(node, comments, context, filename, source)
@@ -171,6 +171,8 @@ export function createLanguageRegistry(candidateRecords) {
171
171
  const featuresCandidate = candidate.features ?? {
172
172
  closedRecords: false,
173
173
  conditionControlledLoops: false,
174
+ conditionalExpressions: false,
175
+ stringInterpolation: false,
174
176
  effectfulCapabilitiesAndResources: false,
175
177
  generalFunctionsAndCalls: false,
176
178
  immutableCollections: false,
@@ -183,9 +185,10 @@ export function createLanguageRegistry(candidateRecords) {
183
185
  }
184
186
 
185
187
  if (!isPlainObject(featuresCandidate) ||
186
- Object.keys(featuresCandidate).sort().join(",") != "closedRecords,conditionControlledLoops,effectfulCapabilitiesAndResources,generalFunctionsAndCalls,immutableCollections,optionalValues,orderedListIteration,orderedMapIteration,referenceClasses,typeParametersAndGenerics,typedErrors" ||
188
+ Object.keys(featuresCandidate).sort().join(",") != "closedRecords,conditionControlledLoops,conditionalExpressions,effectfulCapabilitiesAndResources,generalFunctionsAndCalls,immutableCollections,optionalValues,orderedListIteration,orderedMapIteration,referenceClasses,stringInterpolation,typeParametersAndGenerics,typedErrors" ||
187
189
  typeof featuresCandidate.closedRecords != "boolean" ||
188
190
  typeof featuresCandidate.conditionControlledLoops != "boolean" ||
191
+ typeof featuresCandidate.conditionalExpressions != "boolean" ||
189
192
  typeof featuresCandidate.effectfulCapabilitiesAndResources != "boolean" ||
190
193
  typeof featuresCandidate.generalFunctionsAndCalls != "boolean" ||
191
194
  typeof featuresCandidate.immutableCollections != "boolean" ||
@@ -193,6 +196,7 @@ export function createLanguageRegistry(candidateRecords) {
193
196
  typeof featuresCandidate.orderedListIteration != "boolean" ||
194
197
  typeof featuresCandidate.orderedMapIteration != "boolean" ||
195
198
  typeof featuresCandidate.referenceClasses != "boolean" ||
199
+ typeof featuresCandidate.stringInterpolation != "boolean" ||
196
200
  typeof featuresCandidate.typedErrors != "boolean" ||
197
201
  typeof featuresCandidate.typeParametersAndGenerics != "boolean") {
198
202
  invalidRegistry(`Registry record '${id}' has an invalid feature declaration.`, id)
@@ -200,6 +204,7 @@ export function createLanguageRegistry(candidateRecords) {
200
204
  const features = deepFreeze(/** @type {import("./semantic/types.js").LanguageFeatureCapabilities} */ ({
201
205
  closedRecords: featuresCandidate.closedRecords,
202
206
  conditionControlledLoops: featuresCandidate.conditionControlledLoops,
207
+ conditionalExpressions: featuresCandidate.conditionalExpressions,
203
208
  effectfulCapabilitiesAndResources: featuresCandidate.effectfulCapabilitiesAndResources,
204
209
  generalFunctionsAndCalls: featuresCandidate.generalFunctionsAndCalls,
205
210
  immutableCollections: featuresCandidate.immutableCollections,
@@ -207,6 +212,7 @@ export function createLanguageRegistry(candidateRecords) {
207
212
  orderedListIteration: featuresCandidate.orderedListIteration,
208
213
  orderedMapIteration: featuresCandidate.orderedMapIteration,
209
214
  referenceClasses: featuresCandidate.referenceClasses,
215
+ stringInterpolation: featuresCandidate.stringInterpolation,
210
216
  typedErrors: featuresCandidate.typedErrors,
211
217
  typeParametersAndGenerics: featuresCandidate.typeParametersAndGenerics
212
218
  }))
@@ -592,6 +598,8 @@ const records = [
592
598
  features: {
593
599
  closedRecords: false,
594
600
  conditionControlledLoops: false,
601
+ conditionalExpressions: false,
602
+ stringInterpolation: false,
595
603
  effectfulCapabilitiesAndResources: false,
596
604
  generalFunctionsAndCalls: true,
597
605
  immutableCollections: false,
@@ -615,6 +623,8 @@ const records = [
615
623
  features: {
616
624
  closedRecords: false,
617
625
  conditionControlledLoops: false,
626
+ conditionalExpressions: false,
627
+ stringInterpolation: false,
618
628
  effectfulCapabilitiesAndResources: false,
619
629
  generalFunctionsAndCalls: true,
620
630
  immutableCollections: false,
@@ -636,7 +646,7 @@ const records = [
636
646
  artifactMultiplicity: "multiple",
637
647
  binaryBackend: generateBrowserWasm,
638
648
  defaultFilename: "program.wasm",
639
- features: {closedRecords: false, conditionControlledLoops: false, effectfulCapabilitiesAndResources: false, generalFunctionsAndCalls: false, immutableCollections: false, optionalValues: false, orderedListIteration: false, orderedMapIteration: false, referenceClasses: false, typedErrors: false, typeParametersAndGenerics: false},
649
+ features: {closedRecords: false, conditionControlledLoops: false, conditionalExpressions: false, stringInterpolation: false, effectfulCapabilitiesAndResources: false, generalFunctionsAndCalls: false, immutableCollections: false, optionalValues: false, orderedListIteration: false, orderedMapIteration: false, referenceClasses: false, typedErrors: false, typeParametersAndGenerics: false},
640
650
  id: "wasm",
641
651
  mapping: {binaryRanges: true, richText: true, sourceMapV3: true},
642
652
  mediaType: "application/wasm",
@@ -646,7 +656,7 @@ const records = [
646
656
  acceptance: {stages: ["generate", "compile", "validate", "instantiate", "execute"], toolchains: ["android"]},
647
657
  applicationBackend: generateAndroidApplication,
648
658
  artifactMultiplicity: "multiple",
649
- features: {closedRecords: false, conditionControlledLoops: false, effectfulCapabilitiesAndResources: false, generalFunctionsAndCalls: false, immutableCollections: false, optionalValues: false, orderedListIteration: false, orderedMapIteration: false, referenceClasses: false, typedErrors: false, typeParametersAndGenerics: false},
659
+ features: {closedRecords: false, conditionControlledLoops: false, conditionalExpressions: false, stringInterpolation: false, effectfulCapabilitiesAndResources: false, generalFunctionsAndCalls: false, immutableCollections: false, optionalValues: false, orderedListIteration: false, orderedMapIteration: false, referenceClasses: false, typedErrors: false, typeParametersAndGenerics: false},
650
660
  id: "android",
651
661
  mapping: {binaryRanges: true, richText: true, sourceMapV3: true},
652
662
  roundTrip: false
@@ -655,7 +665,7 @@ const records = [
655
665
  acceptance: {stages: ["generate"], toolchains: []},
656
666
  applicationBackend: generateIosApplication,
657
667
  artifactMultiplicity: "multiple",
658
- features: {closedRecords: false, conditionControlledLoops: false, effectfulCapabilitiesAndResources: false, generalFunctionsAndCalls: false, immutableCollections: false, optionalValues: false, orderedListIteration: false, orderedMapIteration: false, referenceClasses: false, typedErrors: false, typeParametersAndGenerics: false},
668
+ features: {closedRecords: false, conditionControlledLoops: false, conditionalExpressions: false, stringInterpolation: false, effectfulCapabilitiesAndResources: false, generalFunctionsAndCalls: false, immutableCollections: false, optionalValues: false, orderedListIteration: false, orderedMapIteration: false, referenceClasses: false, typedErrors: false, typeParametersAndGenerics: false},
659
669
  id: "ios",
660
670
  mapping: {binaryRanges: true, richText: true, sourceMapV3: true},
661
671
  roundTrip: false
@@ -664,7 +674,7 @@ const records = [
664
674
  acceptance: {stages: ["generate", "restore", "compile", "validate", "instantiate", "execute"], toolchains: ["flutter", "android"]},
665
675
  applicationBackend: generateFlutterApplication,
666
676
  artifactMultiplicity: "multiple",
667
- features: {closedRecords: false, conditionControlledLoops: false, effectfulCapabilitiesAndResources: false, generalFunctionsAndCalls: false, immutableCollections: false, optionalValues: false, orderedListIteration: false, orderedMapIteration: false, referenceClasses: false, typedErrors: false, typeParametersAndGenerics: false},
677
+ features: {closedRecords: false, conditionControlledLoops: false, conditionalExpressions: false, stringInterpolation: false, effectfulCapabilitiesAndResources: false, generalFunctionsAndCalls: false, immutableCollections: false, optionalValues: false, orderedListIteration: false, orderedMapIteration: false, referenceClasses: false, typedErrors: false, typeParametersAndGenerics: false},
668
678
  id: "flutter",
669
679
  mapping: {binaryRanges: true, richText: true, sourceMapV3: true},
670
680
  roundTrip: false
@@ -689,6 +699,8 @@ function language(values) {
689
699
  features: {
690
700
  closedRecords: ["php", "ruby", "javascript", "typescript", "java"].includes(String(values.id)),
691
701
  conditionControlledLoops: ["php", "ruby", "javascript", "typescript", "java"].includes(String(values.id)),
702
+ conditionalExpressions: ["php", "ruby", "javascript", "typescript", "java"].includes(String(values.id)),
703
+ stringInterpolation: ["php", "ruby", "javascript", "typescript"].includes(String(values.id)),
692
704
  effectfulCapabilitiesAndResources: ["php", "ruby", "javascript", "typescript", "java"].includes(String(values.id)),
693
705
  generalFunctionsAndCalls: ["php", "ruby", "javascript", "typescript", "java"].includes(String(values.id)),
694
706
  immutableCollections: ["php", "ruby", "javascript", "typescript", "java"].includes(String(values.id)),
@@ -722,6 +722,7 @@ function expressionChildren(expression) {
722
722
  if (expression.kind == "MethodCallExpression") return [expression.receiver, ...expression.arguments]
723
723
  if (expression.kind == "OwnedBorrowExpression" || expression.kind == "OwnedMoveExpression") return [expression.expression]
724
724
  if (expression.kind == "BinaryExpression") return [expression.left, expression.right]
725
+ if (expression.kind == "ConditionalExpression") return [expression.condition, expression.consequent, expression.alternate]
725
726
  if (expression.kind == "UnaryExpression") return [expression.operand]
726
727
  if (expression.kind == "OptionalSome") return [expression.value]
727
728
  if (expression.kind == "OptionalIsPresent" || expression.kind == "OptionalUnwrap") return [expression.operand]
@@ -592,6 +592,10 @@ export function semanticEntries(module) {
592
592
  } else if (node.kind == "BinaryExpression") {
593
593
  visit(node.left, `${path}/left`, location)
594
594
  visit(node.right, `${path}/right`, location)
595
+ } else if (node.kind == "ConditionalExpression") {
596
+ visit(node.condition, `${path}/condition`, location)
597
+ visit(node.consequent, `${path}/consequent`, location)
598
+ visit(node.alternate, `${path}/alternate`, location)
595
599
  } else if (node.kind == "CallExpression" || node.kind == "EffectCallExpression") {
596
600
  node.arguments.forEach((child, index) => visit(child, `${path}/arguments/${index}`, location))
597
601
  } else if (node.kind == "OwnedMoveExpression" || node.kind == "OwnedBorrowExpression") {
@@ -951,6 +955,10 @@ function resolveSymbols(module, records) {
951
955
  } else if (expression.kind == "BinaryExpression") {
952
956
  visitExpression(expression.left, scope, `${path}/left`)
953
957
  visitExpression(expression.right, scope, `${path}/right`)
958
+ } else if (expression.kind == "ConditionalExpression") {
959
+ visitExpression(expression.condition, scope, `${path}/condition`)
960
+ visitExpression(expression.consequent, scope, `${path}/consequent`)
961
+ visitExpression(expression.alternate, scope, `${path}/alternate`)
954
962
  } else if (expression.kind == "ListLiteral") {
955
963
  for (const [index, element] of expression.elements.entries()) visitExpression(element, scope, `${path}/elements/${index}`)
956
964
  } else if (expression.kind == "MapLiteral" || expression.kind == "OrderedMapLiteral") {
@@ -180,6 +180,8 @@
180
180
  * @property {boolean} orderedListIteration - Task 008 ordered immutable-list iteration and nearest-loop control.
181
181
  * @property {boolean} orderedMapIteration - Task 014 insertion-ordered immutable-map pair iteration.
182
182
  * @property {boolean} conditionControlledLoops - Task 032 strict-Boolean pre-condition loops and resolved control.
183
+ * @property {boolean} conditionalExpressions - Task 048 strict-Boolean conditional expressions with joined arm types.
184
+ * @property {boolean} stringInterpolation - Task 048 parser-proved string interpolation lowered to checked string concatenation.
183
185
  * @property {boolean} closedRecords - Task 009 nominal closed immutable records, construction, and member reads.
184
186
  * @property {boolean} typedErrors - Task 011 nominal unchecked errors, raises, and exact typed catches.
185
187
  * @property {boolean} typeParametersAndGenerics - Task 012 invariant unbounded type parameters and closed generic applications.
@@ -983,6 +985,17 @@
983
985
  * @property {SemanticNodeSourceProvenance} [sourceProvenance] - Node-associated provenance.
984
986
  */
985
987
 
988
+ /**
989
+ * @typedef ConditionalExpression
990
+ * @property {"ConditionalExpression"} kind - Node discriminator.
991
+ * @property {Expression} condition - Strict boolean test evaluated exactly once.
992
+ * @property {Expression} consequent - Arm evaluated when the test is true.
993
+ * @property {Expression} alternate - Arm evaluated when the test is false.
994
+ * @property {SemanticTypeName} type - Joined arm result type assigned by validation.
995
+ * @property {SourceLocation} location - Source location.
996
+ * @property {SemanticNodeSourceProvenance} [sourceProvenance] - Node-associated provenance.
997
+ */
998
+
986
999
  /**
987
1000
  * @typedef CallExpression
988
1001
  * @property {"CallExpression"} kind - Node discriminator.
@@ -1082,7 +1095,7 @@
1082
1095
  * @property {SemanticNodeSourceProvenance} [sourceProvenance] - Parser-owned message-member range.
1083
1096
  */
1084
1097
 
1085
- /** @typedef {IdentifierExpression | ReceiverExpression | IntegerLiteral | BooleanLiteral | StringLiteral | OptionalNone | OptionalSome | OptionalIsPresent | OptionalUnwrap | ListLiteral | MapLiteral | OrderedMapLiteral | ListIndexExpression | MapLookupExpression | CollectionSizeExpression | UnaryExpression | BinaryExpression | CallExpression | EffectCallExpression | OwnedMoveExpression | OwnedBorrowExpression | MethodCallExpression | ReferenceConstruction | RecordConstruction | MemberRead | PrivateFieldRead | ErrorMessageRead} Expression */
1098
+ /** @typedef {IdentifierExpression | ReceiverExpression | IntegerLiteral | BooleanLiteral | StringLiteral | OptionalNone | OptionalSome | OptionalIsPresent | OptionalUnwrap | ListLiteral | MapLiteral | OrderedMapLiteral | ListIndexExpression | MapLookupExpression | CollectionSizeExpression | UnaryExpression | BinaryExpression | ConditionalExpression | CallExpression | EffectCallExpression | OwnedMoveExpression | OwnedBorrowExpression | MethodCallExpression | ReferenceConstruction | RecordConstruction | MemberRead | PrivateFieldRead | ErrorMessageRead} Expression */
1086
1099
 
1087
1100
  /**
1088
1101
  * @typedef LocalDeclaration
@@ -2284,6 +2284,32 @@ function inferExpressionType(expression, scope, functions, records, fail, normal
2284
2284
  }
2285
2285
  return scalarType(signature.result)
2286
2286
  }
2287
+ if (expression.kind == "ConditionalExpression") {
2288
+ const conditionType = inferValueExpressionType(expression.condition, scope, functions, records, fail,
2289
+ normalizeOperations, "a condition")
2290
+
2291
+ if (!isScalarType(conditionType, "boolean")) {
2292
+ fail("NON_BOOLEAN_CONDITION", `Conditional expression condition type ${typeDescription(conditionType)}; expected boolean.`,
2293
+ expression.condition.location)
2294
+ }
2295
+ const consequentScope = createScope(scope, [])
2296
+ const alternateScope = createScope(scope, [])
2297
+
2298
+ applyPresenceNarrowing(expression.condition, scope, consequentScope, true)
2299
+ applyPresenceNarrowing(expression.condition, scope, alternateScope, false)
2300
+ const consequentType = inferValueExpressionType(expression.consequent, consequentScope, functions, records, fail,
2301
+ normalizeOperations, "a conditional expression consequent")
2302
+ const alternateType = inferValueExpressionType(expression.alternate, alternateScope, functions, records, fail,
2303
+ normalizeOperations, "a conditional expression alternate")
2304
+
2305
+ if (!sameType(consequentType, alternateType)) {
2306
+ fail("TYPE_MISMATCH", `Conditional expression arms ${typeDescription(consequentType)} and ${typeDescription(alternateType)}; expected one joined type.`,
2307
+ expression.alternate.location)
2308
+ }
2309
+ Reflect.set(expression, "type", consequentType)
2310
+
2311
+ return consequentType
2312
+ }
2287
2313
  if (expression.kind == "BinaryExpression") {
2288
2314
  const left = inferValueExpressionType(expression.left, scope, functions, records, fail, normalizeOperations, "a binary operand")
2289
2315
  const right = inferValueExpressionType(expression.right, scope, functions, records, fail, normalizeOperations, "a binary operand")
package/src/toolchains.js CHANGED
@@ -21,9 +21,9 @@ export const canonicalToolchains = deepFreeze({
21
21
  javac: definition("javac", "SEMANTIFOLD_JAVAC", ["-version"], /^javac (?:1[7-9]|2[0-9])\./u),
22
22
  java: definition("java", "SEMANTIFOLD_JAVA", ["-version"], /^(?:openjdk|java) version "(?:1[7-9]|2[0-9])\./u),
23
23
  java25: definition("java", "SEMANTIFOLD_JAVA", ["-version"],
24
- /^(?:openjdk version "25\.0\.4" 2026-07-21\nOpenJDK Runtime Environment \(build 25\.0\.4\+7-1-24\.04-Ubuntu\)\nOpenJDK 64-Bit Server VM \(build 25\.0\.4\+7-1-24\.04-Ubuntu, mixed mode, sharing\)|openjdk version "25\.0\.4\.1" 2026-08-18\nOpenJDK Runtime Environment \(build 25\.0\.4\.1\+1-1-26\.04\.4-Ubuntu\)\nOpenJDK 64-Bit Server VM \(build 25\.0\.4\.1\+1-1-26\.04\.4-Ubuntu, mixed mode, sharing\))$/u),
24
+ /^openjdk version "25\.0\.4\.1" 2026-08-18\nOpenJDK Runtime Environment \(build 25\.0\.4\.1\+1-1-(?:24\.04\.4|26\.04\.4)-Ubuntu\)\nOpenJDK 64-Bit Server VM \(build 25\.0\.4\.1\+1-1-(?:24\.04\.4|26\.04\.4)-Ubuntu, mixed mode, sharing\)$/u),
25
25
  kotlinc: definition("kotlinc", "SEMANTIFOLD_KOTLINC", ["-version"],
26
- /^info: kotlinc-jvm 2\.4\.20 \(JRE (?:25\.0\.4\+7-1-24\.04|25\.0\.4\.1\+1-1-26\.04\.4)-Ubuntu\)$/u),
26
+ /^info: kotlinc-jvm 2\.4\.20 \(JRE 25\.0\.4\.1\+1-1-(?:24\.04\.4|26\.04\.4)-Ubuntu\)$/u),
27
27
  python: definition("python3", "SEMANTIFOLD_PYTHON", ["--version"], /^Python 3\./u),
28
28
  dotnet: definition("dotnet", "SEMANTIFOLD_DOTNET", ["--version"], /^10\./u),
29
29
  go: definition("go", "SEMANTIFOLD_GO", ["version"], /^go version go1\.26\.\d+ linux\/amd64$/u),