semantifold 0.10.0 → 0.12.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 (56) hide show
  1. package/README.md +2 -2
  2. package/build/src/backends/c.d.ts +2 -1
  3. package/build/src/backends/c.d.ts.map +1 -1
  4. package/build/src/backends/c.js +26 -5
  5. package/build/src/backends/cpp.js +3 -1
  6. package/build/src/backends/kotlin.d.ts +1 -1
  7. package/build/src/backends/kotlin.js +2 -2
  8. package/build/src/backends/ordered-expressions.d.ts +19 -7
  9. package/build/src/backends/ordered-expressions.d.ts.map +1 -1
  10. package/build/src/backends/ordered-expressions.js +46 -11
  11. package/build/src/backends/shared.d.ts.map +1 -1
  12. package/build/src/backends/shared.js +30 -2
  13. package/build/src/frontends/c.d.ts +34 -1
  14. package/build/src/frontends/c.d.ts.map +1 -1
  15. package/build/src/frontends/c.js +128 -21
  16. package/build/src/frontends/java.d.ts.map +1 -1
  17. package/build/src/frontends/java.js +23 -1
  18. package/build/src/frontends/javascript-typescript.d.ts.map +1 -1
  19. package/build/src/frontends/javascript-typescript.js +45 -4
  20. package/build/src/frontends/php.d.ts.map +1 -1
  21. package/build/src/frontends/php.js +54 -3
  22. package/build/src/frontends/ruby.d.ts.map +1 -1
  23. package/build/src/frontends/ruby.js +55 -5
  24. package/build/src/language-registry.d.ts.map +1 -1
  25. package/build/src/language-registry.js +33 -6
  26. package/build/src/semantic/effects.js +3 -1
  27. package/build/src/semantic/provenance.d.ts.map +1 -1
  28. package/build/src/semantic/provenance.js +11 -1
  29. package/build/src/semantic/types.d.ts +52 -2
  30. package/build/src/semantic/types.d.ts.map +1 -1
  31. package/build/src/semantic/types.js +14 -2
  32. package/build/src/semantic/validate.js +19 -2
  33. package/build/src/toolchains.js +3 -3
  34. package/docs/adr-expression-breadth.md +186 -0
  35. package/docs/c.md +1 -1
  36. package/docs/kotlin.md +2 -2
  37. package/docs/language-support.md +14 -5
  38. package/docs/parser-qualification.md +1 -1
  39. package/docs/testing.md +5 -4
  40. package/package.json +1 -1
  41. package/src/backends/c.js +23 -4
  42. package/src/backends/cpp.js +1 -0
  43. package/src/backends/kotlin.js +1 -1
  44. package/src/backends/ordered-expressions.js +46 -10
  45. package/src/backends/shared.js +31 -1
  46. package/src/frontends/c.js +135 -19
  47. package/src/frontends/java.js +24 -0
  48. package/src/frontends/javascript-typescript.js +49 -2
  49. package/src/frontends/php.js +61 -2
  50. package/src/frontends/ruby.js +62 -2
  51. package/src/language-registry.js +32 -5
  52. package/src/semantic/effects.js +1 -0
  53. package/src/semantic/provenance.js +8 -0
  54. package/src/semantic/types.js +14 -1
  55. package/src/semantic/validate.js +27 -1
  56. package/src/toolchains.js +2 -2
@@ -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
  }))
@@ -565,6 +571,21 @@ const records = [
565
571
  artifactMultiplicity: "multiple",
566
572
  check: {factory: createCCheckPlan, stages: ["compile", "link"], toolchains: ["clang"]},
567
573
  defaultFilename: "program.c",
574
+ features: {
575
+ closedRecords: false,
576
+ conditionControlledLoops: false,
577
+ conditionalExpressions: false,
578
+ stringInterpolation: false,
579
+ effectfulCapabilitiesAndResources: false,
580
+ generalFunctionsAndCalls: true,
581
+ immutableCollections: false,
582
+ optionalValues: false,
583
+ orderedListIteration: false,
584
+ orderedMapIteration: false,
585
+ referenceClasses: false,
586
+ typedErrors: false,
587
+ typeParametersAndGenerics: false
588
+ },
568
589
  frontend: parseC,
569
590
  id: "c",
570
591
  mediaType: "text/x-c",
@@ -592,6 +613,8 @@ const records = [
592
613
  features: {
593
614
  closedRecords: false,
594
615
  conditionControlledLoops: false,
616
+ conditionalExpressions: false,
617
+ stringInterpolation: false,
595
618
  effectfulCapabilitiesAndResources: false,
596
619
  generalFunctionsAndCalls: true,
597
620
  immutableCollections: false,
@@ -615,6 +638,8 @@ const records = [
615
638
  features: {
616
639
  closedRecords: false,
617
640
  conditionControlledLoops: false,
641
+ conditionalExpressions: false,
642
+ stringInterpolation: false,
618
643
  effectfulCapabilitiesAndResources: false,
619
644
  generalFunctionsAndCalls: true,
620
645
  immutableCollections: false,
@@ -636,7 +661,7 @@ const records = [
636
661
  artifactMultiplicity: "multiple",
637
662
  binaryBackend: generateBrowserWasm,
638
663
  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},
664
+ 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
665
  id: "wasm",
641
666
  mapping: {binaryRanges: true, richText: true, sourceMapV3: true},
642
667
  mediaType: "application/wasm",
@@ -646,7 +671,7 @@ const records = [
646
671
  acceptance: {stages: ["generate", "compile", "validate", "instantiate", "execute"], toolchains: ["android"]},
647
672
  applicationBackend: generateAndroidApplication,
648
673
  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},
674
+ 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
675
  id: "android",
651
676
  mapping: {binaryRanges: true, richText: true, sourceMapV3: true},
652
677
  roundTrip: false
@@ -655,7 +680,7 @@ const records = [
655
680
  acceptance: {stages: ["generate"], toolchains: []},
656
681
  applicationBackend: generateIosApplication,
657
682
  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},
683
+ 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
684
  id: "ios",
660
685
  mapping: {binaryRanges: true, richText: true, sourceMapV3: true},
661
686
  roundTrip: false
@@ -664,7 +689,7 @@ const records = [
664
689
  acceptance: {stages: ["generate", "restore", "compile", "validate", "instantiate", "execute"], toolchains: ["flutter", "android"]},
665
690
  applicationBackend: generateFlutterApplication,
666
691
  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},
692
+ 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
693
  id: "flutter",
669
694
  mapping: {binaryRanges: true, richText: true, sourceMapV3: true},
670
695
  roundTrip: false
@@ -689,6 +714,8 @@ function language(values) {
689
714
  features: {
690
715
  closedRecords: ["php", "ruby", "javascript", "typescript", "java"].includes(String(values.id)),
691
716
  conditionControlledLoops: ["php", "ruby", "javascript", "typescript", "java"].includes(String(values.id)),
717
+ conditionalExpressions: ["php", "ruby", "javascript", "typescript", "java"].includes(String(values.id)),
718
+ stringInterpolation: ["php", "ruby", "javascript", "typescript"].includes(String(values.id)),
692
719
  effectfulCapabilitiesAndResources: ["php", "ruby", "javascript", "typescript", "java"].includes(String(values.id)),
693
720
  generalFunctionsAndCalls: ["php", "ruby", "javascript", "typescript", "java"].includes(String(values.id)),
694
721
  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
@@ -8,7 +8,7 @@ import {parserRangeFor} from "./provenance.js"
8
8
  import {validateEffectGraph} from "./effects.js"
9
9
  import {validateResourceLifetimes} from "./lifetime.js"
10
10
 
11
- const task005Languages = new Set(["php", "ruby", "javascript", "typescript", "java", "dart", "zig"])
11
+ const task005Languages = new Set(["php", "ruby", "javascript", "typescript", "java", "c", "dart", "zig"])
12
12
 
13
13
  /** @typedef {Map<string, import("./types.js").RecordDeclaration>} RecordRegistry */
14
14
  /** @typedef {Map<string, import("./types.js").ClassDeclaration>} ClassRegistry */
@@ -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),