semantifold 0.3.0 → 0.4.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/README.md +58 -15
- package/build/index.d.ts +48 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +57 -8
- package/build/src/artifact-path.d.ts +23 -1
- package/build/src/artifact-path.d.ts.map +1 -1
- package/build/src/artifact-path.js +45 -3
- package/build/src/artifacts.d.ts.map +1 -1
- package/build/src/artifacts.js +66 -1
- package/build/src/backends/c-validation.d.ts.map +1 -1
- package/build/src/backends/c-validation.js +7 -3
- package/build/src/backends/cpp-validation.d.ts.map +1 -1
- package/build/src/backends/cpp-validation.js +7 -3
- package/build/src/backends/csharp.js +7 -1
- package/build/src/backends/dart-runtime.d.ts +2 -0
- package/build/src/backends/dart-runtime.d.ts.map +1 -0
- package/build/src/backends/dart-runtime.js +41 -0
- package/build/src/backends/dart.d.ts +20 -0
- package/build/src/backends/dart.d.ts.map +1 -0
- package/build/src/backends/dart.js +321 -0
- package/build/src/backends/effects.d.ts +60 -0
- package/build/src/backends/effects.d.ts.map +1 -0
- package/build/src/backends/effects.js +746 -0
- package/build/src/backends/go.js +18 -4
- package/build/src/backends/identifiers.d.ts +36 -4
- package/build/src/backends/identifiers.d.ts.map +1 -1
- package/build/src/backends/identifiers.js +154 -10
- package/build/src/backends/index.d.ts.map +1 -1
- package/build/src/backends/index.js +27 -4
- package/build/src/backends/ios.d.ts +68 -0
- package/build/src/backends/ios.d.ts.map +1 -0
- package/build/src/backends/ios.js +1503 -0
- package/build/src/backends/java.d.ts.map +1 -1
- package/build/src/backends/java.js +429 -36
- package/build/src/backends/javascript.d.ts +6 -0
- package/build/src/backends/javascript.d.ts.map +1 -1
- package/build/src/backends/javascript.js +380 -24
- package/build/src/backends/kotlin-runtime.d.ts +3 -0
- package/build/src/backends/kotlin-runtime.d.ts.map +1 -0
- package/build/src/backends/kotlin-runtime.js +29 -0
- package/build/src/backends/kotlin.d.ts +23 -0
- package/build/src/backends/kotlin.d.ts.map +1 -0
- package/build/src/backends/kotlin.js +136 -0
- package/build/src/backends/native-validation.d.ts +2 -2
- package/build/src/backends/native-validation.d.ts.map +1 -1
- package/build/src/backends/native-validation.js +2 -2
- package/build/src/backends/ordered-expressions.d.ts.map +1 -1
- package/build/src/backends/ordered-expressions.js +39 -7
- package/build/src/backends/php-stdlib-support.d.ts +19 -0
- package/build/src/backends/php-stdlib-support.d.ts.map +1 -0
- package/build/src/backends/php-stdlib-support.js +227 -0
- package/build/src/backends/php.d.ts.map +1 -1
- package/build/src/backends/php.js +448 -33
- package/build/src/backends/program.d.ts +81 -0
- package/build/src/backends/program.d.ts.map +1 -0
- package/build/src/backends/program.js +1308 -0
- package/build/src/backends/python.js +7 -1
- package/build/src/backends/ruby.d.ts.map +1 -1
- package/build/src/backends/ruby.js +310 -24
- package/build/src/backends/rust-runtime.d.ts +7 -0
- package/build/src/backends/rust-runtime.d.ts.map +1 -0
- package/build/src/backends/rust-runtime.js +32 -0
- package/build/src/backends/rust-validation.d.ts +9 -0
- package/build/src/backends/rust-validation.d.ts.map +1 -0
- package/build/src/backends/rust-validation.js +78 -0
- package/build/src/backends/rust.d.ts +24 -0
- package/build/src/backends/rust.d.ts.map +1 -0
- package/build/src/backends/rust.js +344 -0
- package/build/src/backends/scalars.d.ts +14 -6
- package/build/src/backends/scalars.d.ts.map +1 -1
- package/build/src/backends/scalars.js +167 -17
- package/build/src/backends/shared.d.ts +23 -4
- package/build/src/backends/shared.d.ts.map +1 -1
- package/build/src/backends/shared.js +2158 -62
- package/build/src/backends/stdlib-support.d.ts +14 -0
- package/build/src/backends/stdlib-support.d.ts.map +1 -0
- package/build/src/backends/stdlib-support.js +237 -0
- package/build/src/backends/swift-runtime.d.ts +3 -0
- package/build/src/backends/swift-runtime.d.ts.map +1 -0
- package/build/src/backends/swift-runtime.js +14 -0
- package/build/src/backends/swift.d.ts +8 -0
- package/build/src/backends/swift.d.ts.map +1 -0
- package/build/src/backends/swift.js +127 -0
- package/build/src/backends/typescript.d.ts.map +1 -1
- package/build/src/backends/typescript.js +344 -24
- package/build/src/backends/wasm.d.ts +43 -0
- package/build/src/backends/wasm.d.ts.map +1 -0
- package/build/src/backends/wasm.js +2164 -0
- package/build/src/backends/writer.d.ts +235 -4
- package/build/src/backends/writer.d.ts.map +1 -1
- package/build/src/backends/writer.js +468 -3
- package/build/src/diagnostic.d.ts +4 -3
- package/build/src/diagnostic.d.ts.map +1 -1
- package/build/src/diagnostic.js +5 -3
- package/build/src/frontends/c.d.ts.map +1 -1
- package/build/src/frontends/c.js +5 -3
- package/build/src/frontends/cpp.d.ts.map +1 -1
- package/build/src/frontends/cpp.js +5 -3
- package/build/src/frontends/csharp.d.ts.map +1 -1
- package/build/src/frontends/csharp.js +7 -3
- package/build/src/frontends/dart.d.ts +10 -0
- package/build/src/frontends/dart.d.ts.map +1 -0
- package/build/src/frontends/dart.js +913 -0
- package/build/src/frontends/index.d.ts +3 -1
- package/build/src/frontends/index.d.ts.map +1 -1
- package/build/src/frontends/index.js +15 -5
- package/build/src/frontends/java.d.ts +110 -1
- package/build/src/frontends/java.d.ts.map +1 -1
- package/build/src/frontends/java.js +1725 -70
- package/build/src/frontends/javascript-typescript.d.ts +69 -1
- package/build/src/frontends/javascript-typescript.d.ts.map +1 -1
- package/build/src/frontends/javascript-typescript.js +1825 -93
- package/build/src/frontends/kotlin.d.ts +10 -0
- package/build/src/frontends/kotlin.d.ts.map +1 -0
- package/build/src/frontends/kotlin.js +685 -0
- package/build/src/frontends/php.d.ts +118 -1
- package/build/src/frontends/php.d.ts.map +1 -1
- package/build/src/frontends/php.js +1583 -76
- package/build/src/frontends/program.d.ts +74 -0
- package/build/src/frontends/program.d.ts.map +1 -0
- package/build/src/frontends/program.js +989 -0
- package/build/src/frontends/ruby.d.ts +68 -1
- package/build/src/frontends/ruby.d.ts.map +1 -1
- package/build/src/frontends/ruby.js +1446 -101
- package/build/src/frontends/rust-parser.d.ts +11 -0
- package/build/src/frontends/rust-parser.d.ts.map +1 -0
- package/build/src/frontends/rust-parser.js +15 -0
- package/build/src/frontends/rust.d.ts +14 -0
- package/build/src/frontends/rust.d.ts.map +1 -0
- package/build/src/frontends/rust.js +715 -0
- package/build/src/frontends/scalars.d.ts +10 -0
- package/build/src/frontends/scalars.d.ts.map +1 -1
- package/build/src/frontends/scalars.js +28 -2
- package/build/src/frontends/swift.d.ts +10 -0
- package/build/src/frontends/swift.d.ts.map +1 -0
- package/build/src/frontends/swift.js +860 -0
- package/build/src/frontends/types.d.ts +186 -0
- package/build/src/frontends/types.d.ts.map +1 -0
- package/build/src/frontends/types.js +634 -0
- package/build/src/language-registry.d.ts +46 -4
- package/build/src/language-registry.d.ts.map +1 -1
- package/build/src/language-registry.js +167 -13
- package/build/src/mapping.d.ts +2 -2
- package/build/src/mapping.d.ts.map +1 -1
- package/build/src/mapping.js +31 -10
- package/build/src/semantic/capabilities.d.ts +66 -0
- package/build/src/semantic/capabilities.d.ts.map +1 -0
- package/build/src/semantic/capabilities.js +649 -0
- package/build/src/semantic/effects.d.ts +26 -0
- package/build/src/semantic/effects.d.ts.map +1 -0
- package/build/src/semantic/effects.js +764 -0
- package/build/src/semantic/generics.d.ts +23 -0
- package/build/src/semantic/generics.d.ts.map +1 -0
- package/build/src/semantic/generics.js +61 -0
- package/build/src/semantic/lifetime.d.ts +37 -0
- package/build/src/semantic/lifetime.d.ts.map +1 -0
- package/build/src/semantic/lifetime.js +527 -0
- package/build/src/semantic/provenance.d.ts +9 -2
- package/build/src/semantic/provenance.d.ts.map +1 -1
- package/build/src/semantic/provenance.js +462 -20
- package/build/src/semantic/scalars.d.ts +11 -0
- package/build/src/semantic/scalars.d.ts.map +1 -1
- package/build/src/semantic/scalars.js +16 -1
- package/build/src/semantic/stdlib.d.ts +254 -0
- package/build/src/semantic/stdlib.d.ts.map +1 -0
- package/build/src/semantic/stdlib.js +880 -0
- package/build/src/semantic/types.d.ts +2952 -202
- package/build/src/semantic/types.d.ts.map +1 -1
- package/build/src/semantic/types.js +688 -15
- package/build/src/semantic/validate.d.ts +85 -5
- package/build/src/semantic/validate.d.ts.map +1 -1
- package/build/src/semantic/validate.js +2576 -98
- package/build/src/stdlib-facades.d.ts +304 -0
- package/build/src/stdlib-facades.d.ts.map +1 -0
- package/build/src/stdlib-facades.js +1167 -0
- package/build/src/stdlib-negotiation.d.ts +69 -0
- package/build/src/stdlib-negotiation.d.ts.map +1 -0
- package/build/src/stdlib-negotiation.js +403 -0
- package/build/src/stdlib-providers.d.ts +72 -0
- package/build/src/stdlib-providers.d.ts.map +1 -0
- package/build/src/stdlib-providers.js +339 -0
- package/build/src/toolchains.d.ts +55 -1
- package/build/src/toolchains.d.ts.map +1 -1
- package/build/src/toolchains.js +15 -4
- package/docs/architecture.md +45 -13
- package/docs/dart.md +60 -0
- package/docs/goals.md +3 -3
- package/docs/ios.md +121 -0
- package/docs/kotlin.md +42 -0
- package/docs/language-support.md +231 -26
- package/docs/multi-file-modules.md +52 -0
- package/docs/parser-qualification.md +121 -8
- package/docs/plans/2026-09-05-standard-library-portability.md +10 -10
- package/docs/rust.md +88 -0
- package/docs/source-maps.md +15 -7
- package/docs/standard-library-portability.md +39 -8
- package/docs/swift.md +44 -0
- package/docs/testing.md +366 -4
- package/docs/webassembly.md +52 -0
- package/index.js +63 -8
- package/node_modules/semantifold-tree-sitter-legacy-internal/README.md +2 -2
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/LICENSE +21 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/README.md +45 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/binding.gyp +30 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/bindings/node/binding.cc +20 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/bindings/node/binding_test.js +9 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/bindings/node/index.d.ts +28 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/bindings/node/index.js +11 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/grammar.js +1630 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/package.json +64 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/prebuilds/darwin-arm64/tree-sitter-rust.node +0 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/prebuilds/darwin-x64/tree-sitter-rust.node +0 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/prebuilds/linux-arm64/tree-sitter-rust.node +0 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/prebuilds/linux-x64/tree-sitter-rust.node +0 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/prebuilds/win32-arm64/tree-sitter-rust.node +0 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/prebuilds/win32-x64/tree-sitter-rust.node +0 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/queries/highlights.scm +159 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/queries/injections.scm +9 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/queries/tags.scm +60 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/src/grammar.json +9405 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/src/node-types.json +5399 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/src/parser.c +192569 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/src/scanner.c +393 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/src/tree_sitter/alloc.h +54 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/src/tree_sitter/array.h +290 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/src/tree_sitter/parser.h +266 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/tree-sitter-rust.wasm +0 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/tree-sitter.json +53 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/package.json +3 -2
- package/node_modules/semantifold-tree-sitter-legacy-internal/src/c.js +5 -4
- package/package.json +5 -2
- package/src/artifact-path.js +47 -2
- package/src/artifacts.js +67 -0
- package/src/backends/c-validation.js +6 -1
- package/src/backends/cpp-validation.js +6 -1
- package/src/backends/csharp.js +3 -0
- package/src/backends/dart-runtime.js +41 -0
- package/src/backends/dart.js +326 -0
- package/src/backends/effects.js +705 -0
- package/src/backends/go.js +14 -2
- package/src/backends/identifiers.js +160 -9
- package/src/backends/index.js +27 -2
- package/src/backends/ios.js +1612 -0
- package/src/backends/java.js +454 -36
- package/src/backends/javascript.js +389 -23
- package/src/backends/kotlin-runtime.js +29 -0
- package/src/backends/kotlin.js +141 -0
- package/src/backends/native-validation.js +1 -1
- package/src/backends/ordered-expressions.js +40 -6
- package/src/backends/php-stdlib-support.js +239 -0
- package/src/backends/php.js +469 -32
- package/src/backends/program.js +1407 -0
- package/src/backends/python.js +3 -0
- package/src/backends/ruby.js +326 -23
- package/src/backends/rust-runtime.js +34 -0
- package/src/backends/rust-validation.js +75 -0
- package/src/backends/rust.js +335 -0
- package/src/backends/scalars.js +141 -16
- package/src/backends/shared.js +2304 -124
- package/src/backends/stdlib-support.js +234 -0
- package/src/backends/swift-runtime.js +14 -0
- package/src/backends/swift.js +132 -0
- package/src/backends/typescript.js +349 -23
- package/src/backends/wasm.js +2208 -0
- package/src/backends/writer.js +520 -2
- package/src/diagnostic.js +4 -2
- package/src/frontends/c.js +4 -2
- package/src/frontends/cpp.js +4 -2
- package/src/frontends/csharp.js +4 -2
- package/src/frontends/dart.js +963 -0
- package/src/frontends/index.js +14 -4
- package/src/frontends/java.js +1977 -69
- package/src/frontends/javascript-typescript.js +2046 -102
- package/src/frontends/kotlin.js +713 -0
- package/src/frontends/php.js +1725 -79
- package/src/frontends/program.js +1062 -0
- package/src/frontends/ruby.js +1624 -98
- package/src/frontends/rust-parser.js +18 -0
- package/src/frontends/rust.js +714 -0
- package/src/frontends/scalars.js +31 -1
- package/src/frontends/swift.js +893 -0
- package/src/frontends/types.js +750 -0
- package/src/language-registry.js +172 -12
- package/src/mapping.js +33 -9
- package/src/semantic/capabilities.js +671 -0
- package/src/semantic/effects.js +737 -0
- package/src/semantic/generics.js +58 -0
- package/src/semantic/lifetime.js +517 -0
- package/src/semantic/provenance.js +412 -19
- package/src/semantic/scalars.js +17 -0
- package/src/semantic/stdlib.js +921 -0
- package/src/semantic/types.js +765 -14
- package/src/semantic/validate.js +2934 -209
- package/src/stdlib-facades.js +1211 -0
- package/src/stdlib-negotiation.js +446 -0
- package/src/stdlib-providers.js +377 -0
- package/src/toolchains.js +16 -3
|
@@ -0,0 +1,1612 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {createHash} from "node:crypto"
|
|
4
|
+
import {isDenseArray} from "../array.js"
|
|
5
|
+
import {findPortableArtifactPathConflict, isSafeArtifactPath} from "../artifact-path.js"
|
|
6
|
+
import {createByteMapping} from "../binary-mapping.js"
|
|
7
|
+
import {SemantifoldDiagnostic, unsupportedCapability} from "../diagnostic.js"
|
|
8
|
+
import {finalizeMapping, toSourceMapV3} from "../mapping.js"
|
|
9
|
+
import {hasOnlyUnicodeScalars} from "../semantic/scalars.js"
|
|
10
|
+
import {preflightSemanticProgram} from "./program.js"
|
|
11
|
+
import {emitScalarType, emitStringLiteral} from "./scalars.js"
|
|
12
|
+
import {SourceWriter} from "./writer.js"
|
|
13
|
+
|
|
14
|
+
/** @type {Readonly<Set<import("../semantic/types.js").SemanticLanguage>>} */
|
|
15
|
+
const iosSourceLanguages = new Set([
|
|
16
|
+
"php", "ruby", "javascript", "typescript", "java", "kotlin", "python", "csharp", "go", "c", "cpp", "rust", "swift", "dart"
|
|
17
|
+
])
|
|
18
|
+
const configurationFields = new Set([
|
|
19
|
+
"bundleIdentifier", "capabilities", "deploymentTarget", "displayName", "entitlements", "infoPlist", "lifecycle",
|
|
20
|
+
"moduleName", "organizationPrefix", "permissions", "privacyDeclarations", "productName", "resourceRoot", "sourceRoot"
|
|
21
|
+
])
|
|
22
|
+
const assetFields = new Set(["content", "mediaType", "path", "sha256"])
|
|
23
|
+
const generatorVersion = "0.3.0"
|
|
24
|
+
const excludedPathPatterns = Object.freeze([
|
|
25
|
+
"**/*.xcuserstate",
|
|
26
|
+
"**/xcuserdata/**",
|
|
27
|
+
".swiftpm/**",
|
|
28
|
+
"DerivedData/**",
|
|
29
|
+
"build/**"
|
|
30
|
+
])
|
|
31
|
+
const mediaTypePattern = /^[A-Za-z0-9!#$&^_.+-]+\/[A-Za-z0-9!#$&^_.+-]+(?:;[\u0020-\u007e]+)?$/u
|
|
32
|
+
const binaryOperations = Object.freeze({
|
|
33
|
+
BooleanAnd: "&&",
|
|
34
|
+
BooleanEqual: "==",
|
|
35
|
+
BooleanNotEqual: "!=",
|
|
36
|
+
BooleanOr: "||",
|
|
37
|
+
IntegerAdd: "+",
|
|
38
|
+
IntegerEqual: "==",
|
|
39
|
+
IntegerGreaterThan: ">",
|
|
40
|
+
IntegerGreaterThanOrEqual: ">=",
|
|
41
|
+
IntegerLessThan: "<",
|
|
42
|
+
IntegerLessThanOrEqual: "<=",
|
|
43
|
+
IntegerMultiply: "*",
|
|
44
|
+
IntegerNotEqual: "!=",
|
|
45
|
+
IntegerSubtract: "-",
|
|
46
|
+
StringConcat: "+",
|
|
47
|
+
StringEqual: "==",
|
|
48
|
+
StringNotEqual: "!="
|
|
49
|
+
})
|
|
50
|
+
const unaryOperations = Object.freeze({BooleanNot: "!", IntegerNegate: "-"})
|
|
51
|
+
const iosSwiftRuntime = `final class SemantifoldOutputSink {
|
|
52
|
+
private(set) var lines: [String] = []
|
|
53
|
+
|
|
54
|
+
func write(_ value: Int64) {
|
|
55
|
+
lines.append(String(value))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func write(_ value: Bool) {
|
|
59
|
+
lines.append(value ? "true" : "false")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func write(_ value: String) {
|
|
63
|
+
lines.append(value)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func semantifold_string_equal(_ left: String, _ right: String) -> Bool {
|
|
68
|
+
return left.unicodeScalars.elementsEqual(right.unicodeScalars)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func semantifold_string_not_equal(_ left: String, _ right: String) -> Bool {
|
|
72
|
+
return !left.unicodeScalars.elementsEqual(right.unicodeScalars)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func semantifold_keep_mutable<T>(_ value: inout T) {
|
|
76
|
+
}
|
|
77
|
+
`
|
|
78
|
+
|
|
79
|
+
/** @typedef {{content: string, field: string}} ConfigurationFragment */
|
|
80
|
+
/** @typedef {string | ConfigurationFragment} ConfigurationTextPart */
|
|
81
|
+
/** @typedef {string | readonly ConfigurationTextPart[]} ConfigurationLine */
|
|
82
|
+
/** @typedef {{end: number, field: string, start: number}} ConfigurationSpan */
|
|
83
|
+
/** @typedef {{content: string, configurationSpans: ConfigurationSpan[]}} RenderedConfigurationText */
|
|
84
|
+
/** @typedef {{end: number, field: string, path: string, start: number}} ConfigurationArtifactSpan */
|
|
85
|
+
|
|
86
|
+
/** Records configuration causality while an owning renderer emits text. */
|
|
87
|
+
class ConfigurationTextWriter {
|
|
88
|
+
constructor() {
|
|
89
|
+
this.content = ""
|
|
90
|
+
/** @type {ConfigurationSpan[]} */
|
|
91
|
+
this.configurationSpans = []
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Appends literal or field-owned text.
|
|
96
|
+
* @param {ConfigurationTextPart | readonly ConfigurationTextPart[]} value - Ordered output text.
|
|
97
|
+
*/
|
|
98
|
+
append(value) {
|
|
99
|
+
const parts = Array.isArray(value) ? value : [value]
|
|
100
|
+
|
|
101
|
+
for (const part of parts) {
|
|
102
|
+
if (typeof part == "string") {
|
|
103
|
+
this.content += part
|
|
104
|
+
} else {
|
|
105
|
+
const start = this.content.length
|
|
106
|
+
|
|
107
|
+
this.content += part.content
|
|
108
|
+
this.configurationSpans.push({end: this.content.length, field: part.field, start})
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Finishes renderer-owned text collection.
|
|
115
|
+
* @returns {RenderedConfigurationText} Finished content and causal spans.
|
|
116
|
+
*/
|
|
117
|
+
finish() {
|
|
118
|
+
return {configurationSpans: this.configurationSpans, content: this.content}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Marks text as causally owned by one normalized configuration field.
|
|
124
|
+
* @param {string} field - Configuration field.
|
|
125
|
+
* @param {string} content - Exact rendered token content.
|
|
126
|
+
* @returns {ConfigurationFragment} Tagged token.
|
|
127
|
+
*/
|
|
128
|
+
function configurationFragment(field, content) {
|
|
129
|
+
return {content, field}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Preflights one complete project against the delivered Swift Tasks 001-004 profile.
|
|
134
|
+
* @param {object} input - iOS application generation input.
|
|
135
|
+
* @param {import("../semantic/types.js").SemanticModule} [input.module] - Single module to normalize.
|
|
136
|
+
* @param {import("../semantic/types.js").SemanticProgram} [input.program] - Caller-owned Task 010 project.
|
|
137
|
+
* @param {import("../semantic/types.js").IosApplicationConfigurationInput} [input.configuration] - Closed application configuration.
|
|
138
|
+
* @param {import("../semantic/types.js").IosApplicationAssetInput[]} [input.assets] - Exact caller-provided assets.
|
|
139
|
+
* @returns {{assets: import("../semantic/types.js").IosApplicationAssetInput[], configuration: import("../semantic/types.js").IosApplicationConfiguration, modulePaths: Map<string, string>, modules: import("../semantic/types.js").SemanticModule[], program: import("../semantic/types.js").SemanticProgram, sources: {content: string, filename: string, language?: import("../semantic/types.js").SemanticLanguage}[]}} Prepared application program.
|
|
140
|
+
*/
|
|
141
|
+
export function preflightIosApplication(input) {
|
|
142
|
+
if (!isPlainObject(input) || Boolean(input.module) == Boolean(input.program)) {
|
|
143
|
+
throw new SemantifoldDiagnostic({
|
|
144
|
+
code: "INVALID_APPLICATION_INPUT",
|
|
145
|
+
language: "ios",
|
|
146
|
+
message: "iOS application generation requires exactly one semantic module or program."
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
const configuration = normalizeConfiguration(input.configuration)
|
|
150
|
+
const program = input.program ?? normalizeSingleModule(input.module)
|
|
151
|
+
const prepared = preflightSemanticProgram({
|
|
152
|
+
backendLanguage: "swift",
|
|
153
|
+
diagnosticLanguage: "ios",
|
|
154
|
+
program,
|
|
155
|
+
sourceLanguages: iosSourceLanguages
|
|
156
|
+
})
|
|
157
|
+
const modulePaths = new Map(prepared.modules.map(module => [
|
|
158
|
+
/** @type {string} */ (Reflect.get(module, "id")),
|
|
159
|
+
`${configuration.sourceRoot}/Generated/${moduleName(/** @type {string} */ (Reflect.get(module, "id")))}.swift`
|
|
160
|
+
]))
|
|
161
|
+
const assets = normalizeAssets(input.assets, configuration.resourceRoot)
|
|
162
|
+
const conflict = findPortableArtifactPathConflict([
|
|
163
|
+
`${configuration.sourceRoot}/Generated/SemantifoldRuntime.swift`,
|
|
164
|
+
...modulePaths.values(),
|
|
165
|
+
`${configuration.resourceRoot}/Contents.json`,
|
|
166
|
+
...assets.map(({path}) => path)
|
|
167
|
+
])
|
|
168
|
+
|
|
169
|
+
if (conflict) invalidPath(`Application artifact path '${conflict.path}' has a ${conflict.kind} conflict${conflict.other ? ` with '${conflict.other}'` : ""}.`)
|
|
170
|
+
|
|
171
|
+
return {...prepared, assets, configuration, modulePaths}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Generates the deterministic semantic Swift portion of one iOS application artifact set.
|
|
176
|
+
* @param {Parameters<typeof preflightIosApplication>[0]} input - iOS generation request.
|
|
177
|
+
* @returns {{artifacts: import("../semantic/types.js").GeneratedSetArtifact[], metadata: {applicationManifest: string, platformQualification: string}, target: "ios"}} Candidate artifact set.
|
|
178
|
+
*/
|
|
179
|
+
export function generateIosApplication(input) {
|
|
180
|
+
const prepared = preflightIosApplication(input)
|
|
181
|
+
const semanticArtifacts = renderSemanticArtifacts(prepared)
|
|
182
|
+
const applicationPaths = applicationArtifactPaths(prepared)
|
|
183
|
+
const project = renderPbxProject(prepared, applicationPaths)
|
|
184
|
+
const scheme = renderSharedScheme(prepared)
|
|
185
|
+
const baseConfiguration = renderBaseConfiguration(prepared.configuration)
|
|
186
|
+
const infoPlist = renderInfoPlist(prepared.configuration)
|
|
187
|
+
const unitTests = renderUnitTests(prepared.configuration)
|
|
188
|
+
const uiTests = renderUiTests(prepared.configuration)
|
|
189
|
+
const configuration = prepared.configuration
|
|
190
|
+
const entryNamespace = `SemantifoldModule${moduleName(prepared.program.entryModule)}`
|
|
191
|
+
const app = `import SwiftUI
|
|
192
|
+
|
|
193
|
+
@main
|
|
194
|
+
struct SemantifoldApplication: App {
|
|
195
|
+
var body: some Scene {
|
|
196
|
+
WindowGroup {
|
|
197
|
+
SemantifoldOutputView(lines: SemantifoldBridge.run())
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
`
|
|
202
|
+
const bridge = `enum SemantifoldBridge {
|
|
203
|
+
static func run() -> [String] {
|
|
204
|
+
return ${entryNamespace}.semantifoldEntry()
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
`
|
|
208
|
+
const view = `import SwiftUI
|
|
209
|
+
|
|
210
|
+
struct SemantifoldOutputView: View {
|
|
211
|
+
let lines: [String]
|
|
212
|
+
|
|
213
|
+
var body: some View {
|
|
214
|
+
Text(lines.joined(separator: "\\n"))
|
|
215
|
+
.accessibilityIdentifier("semantifold-output")
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
`
|
|
219
|
+
const product = configuration.productName
|
|
220
|
+
/** @type {ConfigurationArtifactSpan[]} */
|
|
221
|
+
const configurationSpans = []
|
|
222
|
+
const artifacts = [
|
|
223
|
+
configuredSyntheticText(`${product}.xcodeproj/project.pbxproj`, "manifest", project,
|
|
224
|
+
configurationSpans,
|
|
225
|
+
"Canonical deterministic Xcode project structure."),
|
|
226
|
+
configuredSyntheticText(`${product}.xcodeproj/xcshareddata/xcschemes/${product}.xcscheme`, "support",
|
|
227
|
+
scheme, configurationSpans, "Canonical shared Xcode application/test scheme."),
|
|
228
|
+
configuredSyntheticText("Configuration/Base.xcconfig", "support", baseConfiguration, configurationSpans,
|
|
229
|
+
"Closed unsigned iOS build configuration."),
|
|
230
|
+
configuredSyntheticText("Configuration/Info.plist", "support", infoPlist, configurationSpans,
|
|
231
|
+
"Allowlisted iOS application property list."),
|
|
232
|
+
...semanticArtifacts,
|
|
233
|
+
syntheticText("Sources/Application/App.swift", "entry", app, "Synthetic SwiftUI application lifecycle."),
|
|
234
|
+
syntheticText("Sources/Application/SemantifoldBridge.swift", "support", bridge,
|
|
235
|
+
"Synthetic bridge from SwiftUI to the semantic entry."),
|
|
236
|
+
syntheticText("Sources/Application/SemantifoldOutputView.swift", "support", view,
|
|
237
|
+
"Synthetic stable text output view and accessibility identifier."),
|
|
238
|
+
syntheticText("Assets.xcassets/Contents.json", "resource", stringifyCanonicalJson({
|
|
239
|
+
info: {author: "semantifold", version: 1}
|
|
240
|
+
}), "Base asset-catalog metadata; no application icon is synthesized."),
|
|
241
|
+
...prepared.assets.map(assetArtifact),
|
|
242
|
+
configuredSyntheticText(`Tests/${product}Tests.swift`, "support", unitTests, configurationSpans,
|
|
243
|
+
"Synthetic pure-logic determinism XCTest source."),
|
|
244
|
+
configuredSyntheticText(`UITests/${product}UITests.swift`, "support", uiTests, configurationSpans,
|
|
245
|
+
"Synthetic XCUI accessibility-route test source.")
|
|
246
|
+
]
|
|
247
|
+
const manifestPath = "semantifold-project.json"
|
|
248
|
+
|
|
249
|
+
artifacts.push(syntheticText(manifestPath, "manifest",
|
|
250
|
+
renderProjectManifest(prepared, artifacts, manifestPath, configurationSpans),
|
|
251
|
+
"Versioned iOS project ownership and provenance manifest."))
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
artifacts,
|
|
255
|
+
metadata: {applicationManifest: manifestPath, platformQualification: "deferred"},
|
|
256
|
+
target: "ios"
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Renders the complete deterministic application ownership and provenance record.
|
|
262
|
+
* @param {ReturnType<typeof preflightIosApplication>} prepared - Fully preflighted application.
|
|
263
|
+
* @param {import("../semantic/types.js").GeneratedSetArtifact[]} artifacts - All non-manifest output artifacts.
|
|
264
|
+
* @param {string} manifestPath - Owned manifest path.
|
|
265
|
+
* @param {ConfigurationArtifactSpan[]} configurationSpans - Renderer-owned configuration ranges.
|
|
266
|
+
* @returns {string} Canonical manifest JSON.
|
|
267
|
+
*/
|
|
268
|
+
function renderProjectManifest(prepared, artifacts, manifestPath, configurationSpans) {
|
|
269
|
+
const assetPaths = new Set(prepared.assets.map(({path}) => path))
|
|
270
|
+
const semanticArtifacts = artifacts.filter(artifact => artifact.provenance.kind == "text")
|
|
271
|
+
const configuration = prepared.configuration
|
|
272
|
+
|
|
273
|
+
return stringifyCanonicalJson({
|
|
274
|
+
configuration,
|
|
275
|
+
generator: {name: "semantifold", version: generatorVersion},
|
|
276
|
+
ownership: {
|
|
277
|
+
excludedPathPatterns,
|
|
278
|
+
ownedPaths: [...artifacts.map(({path}) => path), manifestPath]
|
|
279
|
+
},
|
|
280
|
+
platformRequirements: {
|
|
281
|
+
appleSdk: {status: "deferred"},
|
|
282
|
+
iosSimulator: {status: "deferred"},
|
|
283
|
+
macosHost: {status: "deferred"},
|
|
284
|
+
swiftc: {status: "deferred"},
|
|
285
|
+
xcode: {status: "deferred"},
|
|
286
|
+
xctest: {status: "deferred"},
|
|
287
|
+
xcuiautomation: {status: "deferred"}
|
|
288
|
+
},
|
|
289
|
+
provenance: {
|
|
290
|
+
assets: prepared.assets.map(asset => ({
|
|
291
|
+
contentKind: typeof asset.content == "string" ? "text" : "binary",
|
|
292
|
+
mediaType: asset.mediaType,
|
|
293
|
+
path: asset.path,
|
|
294
|
+
sha256: asset.sha256
|
|
295
|
+
})),
|
|
296
|
+
configuration: Object.keys(configuration).sort().map(field => ({
|
|
297
|
+
field,
|
|
298
|
+
manifestPointer: `/configuration/${field}`,
|
|
299
|
+
outputs: configurationOutputCitations(configurationSpans, field)
|
|
300
|
+
})),
|
|
301
|
+
semanticSources: prepared.sources.map(source => ({
|
|
302
|
+
artifacts: semanticArtifacts.filter(artifact => artifact.provenance.kind == "text" &&
|
|
303
|
+
artifact.provenance.mapping.spans.some(({origin}) => originReferencesFilename(origin, source.filename)))
|
|
304
|
+
.map(({path}) => path),
|
|
305
|
+
filename: source.filename,
|
|
306
|
+
...(source.language === undefined ? {} : {language: source.language}),
|
|
307
|
+
sha256: createHash("sha256").update(source.content).digest("hex")
|
|
308
|
+
})),
|
|
309
|
+
syntheticScaffolding: [
|
|
310
|
+
...artifacts.filter(artifact => artifact.provenance.kind == "synthetic" && !assetPaths.has(artifact.path))
|
|
311
|
+
.map(({path}) => path),
|
|
312
|
+
manifestPath
|
|
313
|
+
]
|
|
314
|
+
},
|
|
315
|
+
schema: "SemantifoldIosProject",
|
|
316
|
+
semanticProgram: semanticProgramManifest(prepared),
|
|
317
|
+
target: "ios",
|
|
318
|
+
version: 1
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Records stable semantic input identities and their generated Swift linkage.
|
|
324
|
+
* @param {ReturnType<typeof preflightIosApplication>} prepared - Fully preflighted application.
|
|
325
|
+
* @returns {Record<string, unknown>} Versioned semantic-program manifest section.
|
|
326
|
+
*/
|
|
327
|
+
function semanticProgramManifest(prepared) {
|
|
328
|
+
const moduleIds = new Map(prepared.program.modules.map((module, index) => [module.id, index]))
|
|
329
|
+
const sourceContent = new Map(prepared.sources.map(source => [source.filename, source.content]))
|
|
330
|
+
const modules = prepared.program.modules.map(module => {
|
|
331
|
+
const id = module.id
|
|
332
|
+
const path = /** @type {string} */ (prepared.modulePaths.get(id))
|
|
333
|
+
const source = prepared.program.sources.find(candidate => candidate.filename == module.sourceFilename)
|
|
334
|
+
const content = sourceContent.get(module.sourceFilename)
|
|
335
|
+
|
|
336
|
+
if (!source || content === undefined) throw new TypeError(`Preflighted iOS module '${id}' lost its source ownership.`)
|
|
337
|
+
const dependencies = [...new Set(module.imports.map(({moduleId}) => moduleId))]
|
|
338
|
+
.sort((left, right) => /** @type {number} */ (moduleIds.get(left)) - /** @type {number} */ (moduleIds.get(right)))
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
dependencies,
|
|
342
|
+
generatedArtifacts: [path],
|
|
343
|
+
id,
|
|
344
|
+
namespace: `SemantifoldModule${moduleName(id)}`,
|
|
345
|
+
source: {
|
|
346
|
+
filename: source.filename,
|
|
347
|
+
id: source.id,
|
|
348
|
+
sha256: createHash("sha256").update(content).digest("hex")
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
})
|
|
352
|
+
const sources = prepared.program.sources.map(source => {
|
|
353
|
+
const content = sourceContent.get(source.filename)
|
|
354
|
+
|
|
355
|
+
if (content === undefined) throw new TypeError(`Preflighted iOS source '${source.filename}' lost its content.`)
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
filename: source.filename,
|
|
359
|
+
id: source.id,
|
|
360
|
+
language: source.language,
|
|
361
|
+
moduleIds: modules.filter(module => module.source.filename == source.filename).map(({id}) => id),
|
|
362
|
+
ownership: source.ownership ?? "application",
|
|
363
|
+
sha256: createHash("sha256").update(content).digest("hex")
|
|
364
|
+
}
|
|
365
|
+
})
|
|
366
|
+
const entryId = prepared.program.entryModule
|
|
367
|
+
const entryArtifact = /** @type {string} */ (prepared.modulePaths.get(entryId))
|
|
368
|
+
const entryNamespace = `SemantifoldModule${moduleName(entryId)}`
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
entry: {
|
|
372
|
+
applicationArtifact: `${prepared.configuration.sourceRoot}/Application/App.swift`,
|
|
373
|
+
bridgeArtifact: `${prepared.configuration.sourceRoot}/Application/SemantifoldBridge.swift`,
|
|
374
|
+
generatedArtifact: entryArtifact,
|
|
375
|
+
generatedFunctionIdentity: `${entryNamespace}.semantifoldEntry`,
|
|
376
|
+
moduleId: entryId
|
|
377
|
+
},
|
|
378
|
+
modules,
|
|
379
|
+
schema: "SemantifoldIosSemanticProgram",
|
|
380
|
+
sources,
|
|
381
|
+
version: 1
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Determines whether one closed semantic origin cites a source filename.
|
|
387
|
+
* @param {import("../semantic/types.js").SemanticOrigin} origin - Mapped origin.
|
|
388
|
+
* @param {string} filename - Original caller filename.
|
|
389
|
+
* @returns {boolean} Whether the source participates in the origin.
|
|
390
|
+
*/
|
|
391
|
+
function originReferencesFilename(origin, filename) {
|
|
392
|
+
if (origin.kind == "source") return origin.location.filename == filename
|
|
393
|
+
if (origin.kind == "derived") return origin.origins.some(({location}) => location.filename == filename)
|
|
394
|
+
|
|
395
|
+
return origin.relatedOrigins.some(({location}) => location.filename == filename)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Groups renderer-recorded configuration ranges without scanning generated text.
|
|
400
|
+
* @param {ConfigurationArtifactSpan[]} spans - Causal artifact spans.
|
|
401
|
+
* @param {string} field - Normalized configuration field.
|
|
402
|
+
* @returns {{path: string, ranges: {end: number, start: number}[]}[]} Stable output citations.
|
|
403
|
+
*/
|
|
404
|
+
function configurationOutputCitations(spans, field) {
|
|
405
|
+
/** @type {Map<string, {end: number, start: number}[]>} */
|
|
406
|
+
const byPath = new Map()
|
|
407
|
+
|
|
408
|
+
for (const span of spans) {
|
|
409
|
+
if (span.field != field) continue
|
|
410
|
+
const ranges = byPath.get(span.path) ?? []
|
|
411
|
+
|
|
412
|
+
ranges.push({end: span.end, start: span.start})
|
|
413
|
+
byPath.set(span.path, ranges)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return [...byPath].map(([path, ranges]) => ({path, ranges}))
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Renders the shared capture runtime and mapped per-module semantic Swift.
|
|
421
|
+
* @param {ReturnType<typeof preflightIosApplication>} prepared - Fully preflighted application.
|
|
422
|
+
* @returns {import("../semantic/types.js").GeneratedSetArtifact[]} Ordered semantic artifacts.
|
|
423
|
+
*/
|
|
424
|
+
function renderSemanticArtifacts(prepared) {
|
|
425
|
+
const paths = prepared.modulePaths
|
|
426
|
+
const declarations = new Map()
|
|
427
|
+
|
|
428
|
+
for (const module of prepared.modules) {
|
|
429
|
+
for (const declaration of module.functions) declarations.set(declaration.id, {
|
|
430
|
+
declaration,
|
|
431
|
+
moduleId: /** @type {string} */ (Reflect.get(module, "id"))
|
|
432
|
+
})
|
|
433
|
+
}
|
|
434
|
+
/** @type {import("../semantic/types.js").GeneratedSetArtifact[]} */
|
|
435
|
+
const artifacts = [{
|
|
436
|
+
content: iosSwiftRuntime,
|
|
437
|
+
contentKind: "text",
|
|
438
|
+
mediaType: "text/x-swift",
|
|
439
|
+
ownership: "generated",
|
|
440
|
+
path: "Sources/Generated/SemantifoldRuntime.swift",
|
|
441
|
+
provenance: {
|
|
442
|
+
kind: "synthetic",
|
|
443
|
+
reason: "Shared iOS application output capture and exact Swift scalar support.",
|
|
444
|
+
relatedOrigins: []
|
|
445
|
+
},
|
|
446
|
+
role: "support"
|
|
447
|
+
}]
|
|
448
|
+
|
|
449
|
+
for (const module of prepared.modules) {
|
|
450
|
+
const moduleId = /** @type {string} */ (Reflect.get(module, "id"))
|
|
451
|
+
const filename = /** @type {string} */ (paths.get(moduleId))
|
|
452
|
+
const writer = new SourceWriter({
|
|
453
|
+
filename,
|
|
454
|
+
language: "swift",
|
|
455
|
+
module,
|
|
456
|
+
program: prepared.program,
|
|
457
|
+
programPaths: paths,
|
|
458
|
+
sources: prepared.sources
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
emitSemanticModule(writer, module, moduleId, moduleId == prepared.program.entryModule, declarations)
|
|
462
|
+
const mapping = finalizeMapping(writer.finish())
|
|
463
|
+
|
|
464
|
+
artifacts.push({
|
|
465
|
+
content: mapping.generated.content,
|
|
466
|
+
contentKind: "text",
|
|
467
|
+
mediaType: "text/x-swift",
|
|
468
|
+
ownership: "generated",
|
|
469
|
+
path: filename,
|
|
470
|
+
provenance: {kind: "text", mapping, sourceMap: toSourceMapV3(mapping), sourceMapFilename: `${filename}.map`},
|
|
471
|
+
role: "source"
|
|
472
|
+
})
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return artifacts
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Produces all project paths referenced by the Xcode model.
|
|
480
|
+
* @param {ReturnType<typeof preflightIosApplication>} prepared - Prepared application.
|
|
481
|
+
* @returns {{appSources: string[], assets: string[], infoPlist: string, unitTest: string, uiTest: string, xcconfig: string}} Paths.
|
|
482
|
+
*/
|
|
483
|
+
function applicationArtifactPaths(prepared) {
|
|
484
|
+
const {productName: product, resourceRoot, sourceRoot} = prepared.configuration
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
appSources: [
|
|
488
|
+
`${sourceRoot}/Generated/SemantifoldRuntime.swift`,
|
|
489
|
+
...prepared.modulePaths.values(),
|
|
490
|
+
`${sourceRoot}/Application/App.swift`,
|
|
491
|
+
`${sourceRoot}/Application/SemantifoldBridge.swift`,
|
|
492
|
+
`${sourceRoot}/Application/SemantifoldOutputView.swift`
|
|
493
|
+
],
|
|
494
|
+
assets: [resourceRoot, ...prepared.assets.map(({path}) => path)],
|
|
495
|
+
infoPlist: "Configuration/Info.plist",
|
|
496
|
+
unitTest: `Tests/${product}Tests.swift`,
|
|
497
|
+
uiTest: `UITests/${product}UITests.swift`,
|
|
498
|
+
xcconfig: "Configuration/Base.xcconfig"
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Derives collision-checked uppercase 24-hex Xcode object IDs from normalized identities.
|
|
504
|
+
* @param {readonly string[]} identities - Complete ordered object identities.
|
|
505
|
+
* @param {(identity: string) => string} [digest] - Test-only digest seam; production uses built-in SHA-256.
|
|
506
|
+
* @returns {Map<string, string>} Object ID by full input identity.
|
|
507
|
+
*/
|
|
508
|
+
export function allocateXcodeObjectIds(identities, digest = identity => createHash("sha256").update(identity).digest("hex")) {
|
|
509
|
+
const result = new Map()
|
|
510
|
+
const fullIdentities = new Map()
|
|
511
|
+
|
|
512
|
+
for (const identity of identities) {
|
|
513
|
+
if (typeof identity != "string" || identity.length == 0 || result.has(identity)) {
|
|
514
|
+
throw new SemantifoldDiagnostic({code: "XCODE_ID_COLLISION", language: "ios",
|
|
515
|
+
message: "Xcode object identities must be unique non-empty strings."})
|
|
516
|
+
}
|
|
517
|
+
const normalized = identity.normalize("NFC")
|
|
518
|
+
const fullDigest = digest(normalized)
|
|
519
|
+
|
|
520
|
+
if (!/^[0-9a-fA-F]{64}$/u.test(fullDigest)) {
|
|
521
|
+
throw new SemantifoldDiagnostic({code: "XCODE_ID_COLLISION", language: "ios",
|
|
522
|
+
message: `Xcode object identity '${identity}' did not produce a full SHA-256 digest.`})
|
|
523
|
+
}
|
|
524
|
+
const objectId = fullDigest.slice(0, 24).toUpperCase()
|
|
525
|
+
const previous = fullIdentities.get(objectId)
|
|
526
|
+
|
|
527
|
+
if (previous !== undefined && previous != normalized) {
|
|
528
|
+
throw new SemantifoldDiagnostic({code: "XCODE_ID_COLLISION", language: "ios",
|
|
529
|
+
message: `Xcode object identities '${previous}' and '${normalized}' collide at '${objectId}'.`})
|
|
530
|
+
}
|
|
531
|
+
fullIdentities.set(objectId, normalized)
|
|
532
|
+
result.set(identity, objectId)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return result
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Renders the smallest deterministic Xcode application/unit/UI-test project model.
|
|
540
|
+
* Platform execution remains deferred until the repository has a qualified Apple lane.
|
|
541
|
+
* @param {ReturnType<typeof preflightIosApplication>} prepared - Prepared application.
|
|
542
|
+
* @param {ReturnType<typeof applicationArtifactPaths>} paths - Complete owned paths.
|
|
543
|
+
* @returns {RenderedConfigurationText} Canonical LF project file and causal configuration spans.
|
|
544
|
+
*/
|
|
545
|
+
function renderPbxProject(prepared, paths) {
|
|
546
|
+
const configuration = prepared.configuration
|
|
547
|
+
const product = configuration.productName
|
|
548
|
+
const resourceRoot = configuration.resourceRoot
|
|
549
|
+
const sourceRoot = configuration.sourceRoot
|
|
550
|
+
const identities = [
|
|
551
|
+
"project", "group:main", "group:sources", "group:generated", "group:application", "group:configuration",
|
|
552
|
+
"group:tests", "group:uitests", "group:products", "target:app", "target:tests", "target:uitests",
|
|
553
|
+
"phase:app:sources", "phase:app:resources", "phase:app:frameworks", "phase:tests:sources", "phase:tests:resources",
|
|
554
|
+
"phase:tests:frameworks", "phase:uitests:sources", "phase:uitests:resources", "phase:uitests:frameworks",
|
|
555
|
+
"product:app", "product:tests", "product:uitests", "proxy:tests:app", "proxy:uitests:app",
|
|
556
|
+
"dependency:tests:app", "dependency:uitests:app",
|
|
557
|
+
"config-list:project", "config-list:app", "config-list:tests", "config-list:uitests",
|
|
558
|
+
"config:project:debug", "config:project:release", "config:app:debug", "config:app:release",
|
|
559
|
+
"config:tests:debug", "config:tests:release", "config:uitests:debug", "config:uitests:release",
|
|
560
|
+
`file:${paths.xcconfig}`, `file:${paths.infoPlist}`, `file:${resourceRoot}`,
|
|
561
|
+
`file:${paths.unitTest}`, `file:${paths.uiTest}`,
|
|
562
|
+
...paths.appSources.flatMap(path => [`file:${path}`, `build:app:${path}`]),
|
|
563
|
+
`build:app:${resourceRoot}`, `build:tests:${paths.unitTest}`, `build:uitests:${paths.uiTest}`
|
|
564
|
+
]
|
|
565
|
+
const ids = allocateXcodeObjectIds(identities)
|
|
566
|
+
/**
|
|
567
|
+
* Looks up one planned Xcode identity.
|
|
568
|
+
* @param {string} identity - Planned identity.
|
|
569
|
+
* @returns {string} Allocated object ID.
|
|
570
|
+
*/
|
|
571
|
+
const id = identity => {
|
|
572
|
+
const value = ids.get(identity)
|
|
573
|
+
|
|
574
|
+
if (!value) throw new TypeError(`Missing planned Xcode identity '${identity}'.`)
|
|
575
|
+
|
|
576
|
+
return value
|
|
577
|
+
}
|
|
578
|
+
/** @type {Map<string, {identity: string, body: ConfigurationLine[]}[]>} */
|
|
579
|
+
const sections = new Map()
|
|
580
|
+
/**
|
|
581
|
+
* Adds an object to one PBX section.
|
|
582
|
+
* @param {string} section - PBX section.
|
|
583
|
+
* @param {string} identity - Object identity.
|
|
584
|
+
* @param {ConfigurationLine[]} body - Object fields.
|
|
585
|
+
*/
|
|
586
|
+
const add = (section, identity, body) => {
|
|
587
|
+
const entries = sections.get(section) ?? []
|
|
588
|
+
|
|
589
|
+
entries.push({body, identity})
|
|
590
|
+
sections.set(section, entries)
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Renders PBX list values.
|
|
594
|
+
* @param {string[]} values - PBX list values.
|
|
595
|
+
* @returns {string} Rendered list body.
|
|
596
|
+
*/
|
|
597
|
+
const list = values => values.map(value => `\t\t\t\t${value},`).join("\n")
|
|
598
|
+
/**
|
|
599
|
+
* Produces a stable sorted copy.
|
|
600
|
+
* @param {string[]} values - Values to sort.
|
|
601
|
+
* @returns {string[]} Stable sorted copy.
|
|
602
|
+
*/
|
|
603
|
+
const sorted = values => [...values].sort((left, right) => left < right ? -1 : left > right ? 1 : 0)
|
|
604
|
+
|
|
605
|
+
for (const path of paths.appSources) {
|
|
606
|
+
add("PBXBuildFile", `build:app:${path}`, ["isa = PBXBuildFile;", `fileRef = ${id(`file:${path}`)};`])
|
|
607
|
+
add("PBXFileReference", `file:${path}`, ["isa = PBXFileReference;", "lastKnownFileType = sourcecode.swift;",
|
|
608
|
+
["path = ", ...pbxConfigurationPath(configuration, path), ";"], "sourceTree = SOURCE_ROOT;"])
|
|
609
|
+
}
|
|
610
|
+
add("PBXBuildFile", `build:app:${resourceRoot}`, ["isa = PBXBuildFile;", `fileRef = ${id(`file:${resourceRoot}`)};`])
|
|
611
|
+
add("PBXBuildFile", `build:tests:${paths.unitTest}`, ["isa = PBXBuildFile;", `fileRef = ${id(`file:${paths.unitTest}`)};`])
|
|
612
|
+
add("PBXBuildFile", `build:uitests:${paths.uiTest}`, ["isa = PBXBuildFile;", `fileRef = ${id(`file:${paths.uiTest}`)};`])
|
|
613
|
+
add("PBXContainerItemProxy", "proxy:tests:app", ["isa = PBXContainerItemProxy;", `containerPortal = ${id("project")};`,
|
|
614
|
+
"proxyType = 1;", `remoteGlobalIDString = ${id("target:app")};`,
|
|
615
|
+
["remoteInfo = \"", configurationFragment("productName", product), "\";"]])
|
|
616
|
+
add("PBXContainerItemProxy", "proxy:uitests:app", ["isa = PBXContainerItemProxy;", `containerPortal = ${id("project")};`,
|
|
617
|
+
"proxyType = 1;", `remoteGlobalIDString = ${id("target:app")};`,
|
|
618
|
+
["remoteInfo = \"", configurationFragment("productName", product), "\";"]])
|
|
619
|
+
add("PBXFileReference", `file:${paths.xcconfig}`, ["isa = PBXFileReference;", "lastKnownFileType = text.xcconfig;",
|
|
620
|
+
`path = ${pbxString(paths.xcconfig)};`, "sourceTree = SOURCE_ROOT;"])
|
|
621
|
+
add("PBXFileReference", `file:${paths.infoPlist}`, ["isa = PBXFileReference;", "lastKnownFileType = text.plist.xml;",
|
|
622
|
+
`path = ${pbxString(paths.infoPlist)};`, "sourceTree = SOURCE_ROOT;"])
|
|
623
|
+
add("PBXFileReference", `file:${resourceRoot}`, ["isa = PBXFileReference;", "lastKnownFileType = folder.assetcatalog;",
|
|
624
|
+
["path = ", configurationFragment("resourceRoot", resourceRoot), ";"], "sourceTree = SOURCE_ROOT;"])
|
|
625
|
+
add("PBXFileReference", `file:${paths.unitTest}`, ["isa = PBXFileReference;", "lastKnownFileType = sourcecode.swift;",
|
|
626
|
+
["path = ", ...pbxConfigurationPath(configuration, paths.unitTest), ";"], "sourceTree = SOURCE_ROOT;"])
|
|
627
|
+
add("PBXFileReference", `file:${paths.uiTest}`, ["isa = PBXFileReference;", "lastKnownFileType = sourcecode.swift;",
|
|
628
|
+
["path = ", ...pbxConfigurationPath(configuration, paths.uiTest), ";"], "sourceTree = SOURCE_ROOT;"])
|
|
629
|
+
add("PBXFileReference", "product:app", ["isa = PBXFileReference;", "explicitFileType = wrapper.application;", "includeInIndex = 0;",
|
|
630
|
+
["path = \"", configurationFragment("productName", product), ".app\";"], "sourceTree = BUILT_PRODUCTS_DIR;"])
|
|
631
|
+
add("PBXFileReference", "product:tests", ["isa = PBXFileReference;", "explicitFileType = wrapper.cfbundle;", "includeInIndex = 0;",
|
|
632
|
+
["path = \"", configurationFragment("productName", product), "Tests.xctest\";"], "sourceTree = BUILT_PRODUCTS_DIR;"])
|
|
633
|
+
add("PBXFileReference", "product:uitests", ["isa = PBXFileReference;", "explicitFileType = wrapper.cfbundle;", "includeInIndex = 0;",
|
|
634
|
+
["path = \"", configurationFragment("productName", product), "UITests.xctest\";"], "sourceTree = BUILT_PRODUCTS_DIR;"])
|
|
635
|
+
|
|
636
|
+
const generatedRefs = sorted(paths.appSources.filter(path => path.startsWith(`${sourceRoot}/Generated/`))).map(path => id(`file:${path}`))
|
|
637
|
+
const applicationRefs = sorted(paths.appSources.filter(path => path.startsWith(`${sourceRoot}/Application/`))).map(path => id(`file:${path}`))
|
|
638
|
+
|
|
639
|
+
add("PBXGroup", "group:main", ["isa = PBXGroup;", `children = (\n${list([
|
|
640
|
+
id("group:configuration"), id("group:sources"), id(`file:${resourceRoot}`), id("group:tests"), id("group:uitests"),
|
|
641
|
+
id("group:products")
|
|
642
|
+
])}\n\t\t\t);`, "sourceTree = \"<group>\";"])
|
|
643
|
+
add("PBXGroup", "group:sources", ["isa = PBXGroup;", `children = (\n${list([id("group:application"), id("group:generated")])}\n\t\t\t);`,
|
|
644
|
+
["name = ", configurationFragment("sourceRoot", sourceRoot), ";"], "sourceTree = \"<group>\";"])
|
|
645
|
+
add("PBXGroup", "group:generated", ["isa = PBXGroup;", `children = (\n${list(generatedRefs)}\n\t\t\t);`,
|
|
646
|
+
"name = Generated;", "sourceTree = \"<group>\";"])
|
|
647
|
+
add("PBXGroup", "group:application", ["isa = PBXGroup;", `children = (\n${list(applicationRefs)}\n\t\t\t);`,
|
|
648
|
+
"name = Application;", "sourceTree = \"<group>\";"])
|
|
649
|
+
add("PBXGroup", "group:configuration", ["isa = PBXGroup;", `children = (\n${list([id(`file:${paths.xcconfig}`), id(`file:${paths.infoPlist}`)])}\n\t\t\t);`,
|
|
650
|
+
"name = Configuration;", "sourceTree = \"<group>\";"])
|
|
651
|
+
add("PBXGroup", "group:tests", ["isa = PBXGroup;", `children = (\n${list([id(`file:${paths.unitTest}`)])}\n\t\t\t);`,
|
|
652
|
+
"name = Tests;", "sourceTree = \"<group>\";"])
|
|
653
|
+
add("PBXGroup", "group:uitests", ["isa = PBXGroup;", `children = (\n${list([id(`file:${paths.uiTest}`)])}\n\t\t\t);`,
|
|
654
|
+
"name = UITests;", "sourceTree = \"<group>\";"])
|
|
655
|
+
add("PBXGroup", "group:products", ["isa = PBXGroup;", `children = (\n${list([id("product:app"), id("product:tests"), id("product:uitests")])}\n\t\t\t);`,
|
|
656
|
+
"name = Products;", "sourceTree = \"<group>\";"])
|
|
657
|
+
|
|
658
|
+
addNativeTarget(add, id, configuration, "target:app", "", "com.apple.product-type.application", "product:app",
|
|
659
|
+
"config-list:app", ["phase:app:sources", "phase:app:frameworks", "phase:app:resources"], [])
|
|
660
|
+
addNativeTarget(add, id, configuration, "target:tests", "Tests", "com.apple.product-type.bundle.unit-test", "product:tests",
|
|
661
|
+
"config-list:tests", ["phase:tests:sources", "phase:tests:frameworks", "phase:tests:resources"], ["dependency:tests:app"])
|
|
662
|
+
addNativeTarget(add, id, configuration, "target:uitests", "UITests", "com.apple.product-type.bundle.ui-testing", "product:uitests",
|
|
663
|
+
"config-list:uitests", ["phase:uitests:sources", "phase:uitests:frameworks", "phase:uitests:resources"], ["dependency:uitests:app"])
|
|
664
|
+
add("PBXProject", "project", ["isa = PBXProject;", "attributes = { BuildIndependentTargetsInParallel = YES; };",
|
|
665
|
+
`buildConfigurationList = ${id("config-list:project")};`, "developmentRegion = en;", "hasScannedForEncodings = 0;",
|
|
666
|
+
"knownRegions = ( en, Base, );", `mainGroup = ${id("group:main")};`, `productRefGroup = ${id("group:products")};`,
|
|
667
|
+
"projectDirPath = \"\";", "projectRoot = \"\";",
|
|
668
|
+
`targets = (\n${list([id("target:app"), id("target:tests"), id("target:uitests")])}\n\t\t\t);`])
|
|
669
|
+
|
|
670
|
+
addBuildPhase(add, id, "PBXFrameworksBuildPhase", "phase:app:frameworks", [])
|
|
671
|
+
addBuildPhase(add, id, "PBXResourcesBuildPhase", "phase:app:resources", [`build:app:${resourceRoot}`])
|
|
672
|
+
addBuildPhase(add, id, "PBXSourcesBuildPhase", "phase:app:sources", sorted(paths.appSources).map(path => `build:app:${path}`))
|
|
673
|
+
for (const target of ["tests", "uitests"]) {
|
|
674
|
+
addBuildPhase(add, id, "PBXFrameworksBuildPhase", `phase:${target}:frameworks`, [])
|
|
675
|
+
addBuildPhase(add, id, "PBXResourcesBuildPhase", `phase:${target}:resources`, [])
|
|
676
|
+
}
|
|
677
|
+
addBuildPhase(add, id, "PBXSourcesBuildPhase", "phase:tests:sources", [`build:tests:${paths.unitTest}`])
|
|
678
|
+
addBuildPhase(add, id, "PBXSourcesBuildPhase", "phase:uitests:sources", [`build:uitests:${paths.uiTest}`])
|
|
679
|
+
add("PBXTargetDependency", "dependency:tests:app", ["isa = PBXTargetDependency;", `target = ${id("target:app")};`,
|
|
680
|
+
`targetProxy = ${id("proxy:tests:app")};`])
|
|
681
|
+
add("PBXTargetDependency", "dependency:uitests:app", ["isa = PBXTargetDependency;", `target = ${id("target:app")};`,
|
|
682
|
+
`targetProxy = ${id("proxy:uitests:app")};`])
|
|
683
|
+
|
|
684
|
+
for (const owner of ["project", "app", "tests", "uitests"]) {
|
|
685
|
+
for (const variant of ["debug", "release"]) {
|
|
686
|
+
const settings = buildSettings(owner, variant, configuration, paths)
|
|
687
|
+
|
|
688
|
+
add("XCBuildConfiguration", `config:${owner}:${variant}`, ["isa = XCBuildConfiguration;",
|
|
689
|
+
`baseConfigurationReference = ${id(`file:${paths.xcconfig}`)};`, ["buildSettings = {\n", ...settings, "\n\t\t\t};"],
|
|
690
|
+
`name = ${variant == "debug" ? "Debug" : "Release"};`])
|
|
691
|
+
}
|
|
692
|
+
add("XCConfigurationList", `config-list:${owner}`, ["isa = XCConfigurationList;",
|
|
693
|
+
`buildConfigurations = (\n${list([id(`config:${owner}:debug`), id(`config:${owner}:release`)])}\n\t\t\t);`,
|
|
694
|
+
"defaultConfigurationIsVisible = 0;", "defaultConfigurationName = Release;"])
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const order = ["PBXBuildFile", "PBXContainerItemProxy", "PBXFileReference", "PBXFrameworksBuildPhase", "PBXGroup",
|
|
698
|
+
"PBXNativeTarget", "PBXProject", "PBXResourcesBuildPhase", "PBXSourcesBuildPhase", "PBXTargetDependency",
|
|
699
|
+
"XCBuildConfiguration", "XCConfigurationList"]
|
|
700
|
+
const writer = new ConfigurationTextWriter()
|
|
701
|
+
|
|
702
|
+
writer.append("// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {};\n\tobjectVersion = 56;\n\tobjects = {\n")
|
|
703
|
+
|
|
704
|
+
for (const section of order) {
|
|
705
|
+
const entries = (sections.get(section) ?? []).sort((left, right) => left.identity < right.identity ? -1 : 1)
|
|
706
|
+
|
|
707
|
+
writer.append(`\n/* Begin ${section} section */\n`)
|
|
708
|
+
for (const entry of entries) {
|
|
709
|
+
writer.append(`\t\t${id(entry.identity)} = {\n`)
|
|
710
|
+
for (const line of entry.body) {
|
|
711
|
+
writer.append("\t\t\t")
|
|
712
|
+
writer.append(line)
|
|
713
|
+
writer.append("\n")
|
|
714
|
+
}
|
|
715
|
+
writer.append("\t\t};\n")
|
|
716
|
+
}
|
|
717
|
+
writer.append(`/* End ${section} section */\n`)
|
|
718
|
+
}
|
|
719
|
+
writer.append(`\t};\n\trootObject = ${id("project")};\n}\n`)
|
|
720
|
+
|
|
721
|
+
return writer.finish()
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Adds one native target object to the project model.
|
|
726
|
+
* @param {(section: string, identity: string, body: ConfigurationLine[]) => void} add - Object sink.
|
|
727
|
+
* @param {(identity: string) => string} id - ID lookup.
|
|
728
|
+
* @param {import("../semantic/types.js").IosApplicationConfiguration} configuration - Application configuration.
|
|
729
|
+
* @param {string} identity - Target identity.
|
|
730
|
+
* @param {string} nameSuffix - Target suffix after the product name.
|
|
731
|
+
* @param {string} productType - Product type.
|
|
732
|
+
* @param {string} productReference - Product reference identity.
|
|
733
|
+
* @param {string} configurationList - Configuration list identity.
|
|
734
|
+
* @param {string[]} phases - Phase identities.
|
|
735
|
+
* @param {string[]} dependencies - Dependency identities.
|
|
736
|
+
*/
|
|
737
|
+
function addNativeTarget(add, id, configuration, identity, nameSuffix, productType, productReference, configurationList,
|
|
738
|
+
phases, dependencies) {
|
|
739
|
+
/**
|
|
740
|
+
* Renders target identity values.
|
|
741
|
+
* @param {string[]} values - Target identity values.
|
|
742
|
+
* @returns {string} Rendered target list.
|
|
743
|
+
*/
|
|
744
|
+
const lines = values => values.map(value => `\t\t\t\t${id(value)},`).join("\n")
|
|
745
|
+
const name = ["\"", configurationFragment("productName", configuration.productName), `${nameSuffix}"`]
|
|
746
|
+
|
|
747
|
+
add("PBXNativeTarget", identity, ["isa = PBXNativeTarget;", `buildConfigurationList = ${id(configurationList)};`,
|
|
748
|
+
`buildPhases = (\n${lines(phases)}\n\t\t\t);`, "buildRules = ();", `dependencies = (\n${lines(dependencies)}\n\t\t\t);`,
|
|
749
|
+
["name = ", ...name, ";"], ["productName = ", ...name, ";"], `productReference = ${id(productReference)};`,
|
|
750
|
+
`productType = ${pbxString(productType)};`])
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Adds one build phase to the project model.
|
|
755
|
+
* @param {(section: string, identity: string, body: ConfigurationLine[]) => void} add - Object sink.
|
|
756
|
+
* @param {(identity: string) => string} id - ID lookup.
|
|
757
|
+
* @param {string} section - Phase ISA.
|
|
758
|
+
* @param {string} identity - Phase identity.
|
|
759
|
+
* @param {string[]} files - Build-file identities.
|
|
760
|
+
*/
|
|
761
|
+
function addBuildPhase(add, id, section, identity, files) {
|
|
762
|
+
const lines = files.map(file => `\t\t\t\t${id(file)},`).join("\n")
|
|
763
|
+
|
|
764
|
+
add(section, identity, [`isa = ${section};`, "buildActionMask = 2147483647;", `files = (\n${lines}\n\t\t\t);`,
|
|
765
|
+
"runOnlyForDeploymentPostprocessing = 0;"])
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Renders sorted PBX build settings for one owner and variant.
|
|
770
|
+
* @param {string} owner - Configuration owner.
|
|
771
|
+
* @param {string} variant - Debug or release.
|
|
772
|
+
* @param {import("../semantic/types.js").IosApplicationConfiguration} configuration - Application configuration.
|
|
773
|
+
* @param {ReturnType<typeof applicationArtifactPaths>} paths - Planned paths.
|
|
774
|
+
* @returns {ConfigurationTextPart[]} Sorted PBX build settings.
|
|
775
|
+
*/
|
|
776
|
+
function buildSettings(owner, variant, configuration, paths) {
|
|
777
|
+
/** @type {Record<string, ConfigurationTextPart | ConfigurationTextPart[]>} */
|
|
778
|
+
const values = owner == "project" ? {
|
|
779
|
+
CODE_SIGNING_ALLOWED: "NO",
|
|
780
|
+
CODE_SIGNING_REQUIRED: "NO"
|
|
781
|
+
} : owner == "app" ? {
|
|
782
|
+
GENERATE_INFOPLIST_FILE: "NO",
|
|
783
|
+
INFOPLIST_FILE: pbxString(paths.infoPlist),
|
|
784
|
+
IPHONEOS_DEPLOYMENT_TARGET: configurationFragment("deploymentTarget", configuration.deploymentTarget),
|
|
785
|
+
PRODUCT_BUNDLE_IDENTIFIER: configurationFragment("bundleIdentifier", configuration.bundleIdentifier),
|
|
786
|
+
PRODUCT_MODULE_NAME: configurationFragment("moduleName", configuration.moduleName),
|
|
787
|
+
PRODUCT_NAME: configurationFragment("productName", configuration.productName),
|
|
788
|
+
SDKROOT: "iphoneos",
|
|
789
|
+
SUPPORTED_PLATFORMS: pbxString("iphoneos iphonesimulator"),
|
|
790
|
+
TARGETED_DEVICE_FAMILY: "1"
|
|
791
|
+
} : owner == "tests" ? {
|
|
792
|
+
BUNDLE_LOADER: pbxString("$(TEST_HOST)"),
|
|
793
|
+
PRODUCT_BUNDLE_IDENTIFIER: [configurationFragment("bundleIdentifier", configuration.bundleIdentifier), ".tests"],
|
|
794
|
+
PRODUCT_MODULE_NAME: [configurationFragment("moduleName", configuration.moduleName), "Tests"],
|
|
795
|
+
PRODUCT_NAME: "$(TARGET_NAME)",
|
|
796
|
+
TEST_HOST: ["\"$(BUILT_PRODUCTS_DIR)/", configurationFragment("productName", configuration.productName),
|
|
797
|
+
".app/", configurationFragment("productName", configuration.productName), "\""]
|
|
798
|
+
} : {
|
|
799
|
+
PRODUCT_BUNDLE_IDENTIFIER: [configurationFragment("bundleIdentifier", configuration.bundleIdentifier), ".uitests"],
|
|
800
|
+
PRODUCT_MODULE_NAME: [configurationFragment("moduleName", configuration.moduleName), "UITests"],
|
|
801
|
+
PRODUCT_NAME: "$(TARGET_NAME)",
|
|
802
|
+
TEST_TARGET_NAME: configurationFragment("productName", configuration.productName)
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (variant == "release") values.SWIFT_OPTIMIZATION_LEVEL = pbxString("-O")
|
|
806
|
+
|
|
807
|
+
/** @type {ConfigurationTextPart[]} */
|
|
808
|
+
const output = []
|
|
809
|
+
|
|
810
|
+
for (const [index, key] of Object.keys(values).sort().entries()) {
|
|
811
|
+
const value = values[key]
|
|
812
|
+
|
|
813
|
+
if (index > 0) output.push("\n")
|
|
814
|
+
output.push(`\t\t\t\t${key} = `)
|
|
815
|
+
output.push(...(Array.isArray(value) ? value : [value]))
|
|
816
|
+
output.push(";")
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
return output
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Quotes one PBX scalar.
|
|
824
|
+
* @param {string} value - PBX scalar.
|
|
825
|
+
* @returns {string} Safely quoted PBX scalar.
|
|
826
|
+
*/
|
|
827
|
+
function pbxString(value) {
|
|
828
|
+
return JSON.stringify(value)
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Renders a quoted PBX path while retaining causality for its configured component.
|
|
833
|
+
* @param {import("../semantic/types.js").IosApplicationConfiguration} configuration - Application configuration.
|
|
834
|
+
* @param {string} path - Planned artifact path.
|
|
835
|
+
* @returns {ConfigurationTextPart[]} Quoted PBX path fragments.
|
|
836
|
+
*/
|
|
837
|
+
function pbxConfigurationPath(configuration, path) {
|
|
838
|
+
if (path.startsWith(`${configuration.sourceRoot}/`)) {
|
|
839
|
+
return ["\"", configurationFragment("sourceRoot", configuration.sourceRoot), path.slice(configuration.sourceRoot.length), "\""]
|
|
840
|
+
}
|
|
841
|
+
if (path == configuration.resourceRoot) {
|
|
842
|
+
return ["\"", configurationFragment("resourceRoot", configuration.resourceRoot), "\""]
|
|
843
|
+
}
|
|
844
|
+
if (path == `Tests/${configuration.productName}Tests.swift`) {
|
|
845
|
+
return ["\"Tests/", configurationFragment("productName", configuration.productName), "Tests.swift\""]
|
|
846
|
+
}
|
|
847
|
+
if (path == `UITests/${configuration.productName}UITests.swift`) {
|
|
848
|
+
return ["\"UITests/", configurationFragment("productName", configuration.productName), "UITests.swift\""]
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
return [pbxString(path)]
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Renders the shared scheme XML.
|
|
856
|
+
* @param {ReturnType<typeof preflightIosApplication>} prepared - Prepared application.
|
|
857
|
+
* @returns {RenderedConfigurationText} Shared scheme XML and causal configuration spans.
|
|
858
|
+
*/
|
|
859
|
+
function renderSharedScheme(prepared) {
|
|
860
|
+
const product = prepared.configuration.productName
|
|
861
|
+
const appId = allocateXcodeObjectIds(["target:app"]).get("target:app")
|
|
862
|
+
const testId = allocateXcodeObjectIds(["target:tests"]).get("target:tests")
|
|
863
|
+
const uiTestId = allocateXcodeObjectIds(["target:uitests"]).get("target:uitests")
|
|
864
|
+
/**
|
|
865
|
+
* Renders one scheme buildable reference.
|
|
866
|
+
* @param {string} nameSuffix - Blueprint suffix after product name.
|
|
867
|
+
* @param {string} buildableSuffix - Product filename suffix.
|
|
868
|
+
* @param {string | undefined} id - Object ID.
|
|
869
|
+
* @returns {ConfigurationTextPart[]} XML reference.
|
|
870
|
+
*/
|
|
871
|
+
const reference = (nameSuffix, buildableSuffix, id) => [`<BuildableReference
|
|
872
|
+
BuildableIdentifier = "primary"
|
|
873
|
+
BlueprintIdentifier = "${id}"
|
|
874
|
+
BuildableName = "`, configurationFragment("productName", product), `${buildableSuffix}"
|
|
875
|
+
BlueprintName = "`, configurationFragment("productName", product), `${nameSuffix}"
|
|
876
|
+
ReferencedContainer = "container:`, configurationFragment("productName", product), `.xcodeproj">
|
|
877
|
+
</BuildableReference>`]
|
|
878
|
+
const writer = new ConfigurationTextWriter()
|
|
879
|
+
|
|
880
|
+
writer.append(`<?xml version="1.0" encoding="UTF-8"?>
|
|
881
|
+
<Scheme version="1.7">
|
|
882
|
+
<BuildAction parallelizeBuildables="YES" buildImplicitDependencies="YES">
|
|
883
|
+
<BuildActionEntries>
|
|
884
|
+
<BuildActionEntry buildForTesting="YES" buildForRunning="YES" buildForProfiling="YES" buildForArchiving="YES" buildForAnalyzing="YES">
|
|
885
|
+
`)
|
|
886
|
+
writer.append(reference("", ".app", appId))
|
|
887
|
+
writer.append(`
|
|
888
|
+
</BuildActionEntry>
|
|
889
|
+
</BuildActionEntries>
|
|
890
|
+
</BuildAction>
|
|
891
|
+
<TestAction buildConfiguration="Debug" selectedDebuggerIdentifier="Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier="Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv="YES">
|
|
892
|
+
<Testables>
|
|
893
|
+
<TestableReference skipped="NO">
|
|
894
|
+
`)
|
|
895
|
+
writer.append(reference("Tests", "Tests.xctest", testId))
|
|
896
|
+
writer.append(`
|
|
897
|
+
</TestableReference>
|
|
898
|
+
<TestableReference skipped="NO">
|
|
899
|
+
`)
|
|
900
|
+
writer.append(reference("UITests", "UITests.xctest", uiTestId))
|
|
901
|
+
writer.append(`
|
|
902
|
+
</TestableReference>
|
|
903
|
+
</Testables>
|
|
904
|
+
</TestAction>
|
|
905
|
+
<LaunchAction buildConfiguration="Debug" selectedDebuggerIdentifier="Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier="Xcode.DebuggerFoundation.Launcher.LLDB" launchStyle="0" useCustomWorkingDirectory="NO" ignoresPersistentStateOnLaunch="NO" debugDocumentVersioning="YES" debugServiceExtension="internal" allowLocationSimulation="YES">
|
|
906
|
+
<BuildableProductRunnable runnableDebuggingMode="0">
|
|
907
|
+
`)
|
|
908
|
+
writer.append(reference("", ".app", appId))
|
|
909
|
+
writer.append(`
|
|
910
|
+
</BuildableProductRunnable>
|
|
911
|
+
</LaunchAction>
|
|
912
|
+
<ProfileAction buildConfiguration="Release" shouldUseLaunchSchemeArgsEnv="YES" savedToolIdentifier="" useCustomWorkingDirectory="NO" debugDocumentVersioning="YES">
|
|
913
|
+
<BuildableProductRunnable runnableDebuggingMode="0">
|
|
914
|
+
`)
|
|
915
|
+
writer.append(reference("", ".app", appId))
|
|
916
|
+
writer.append(`
|
|
917
|
+
</BuildableProductRunnable>
|
|
918
|
+
</ProfileAction>
|
|
919
|
+
<AnalyzeAction buildConfiguration="Debug"/>
|
|
920
|
+
<ArchiveAction buildConfiguration="Release" revealArchiveInOrganizer="YES"/>
|
|
921
|
+
</Scheme>
|
|
922
|
+
`)
|
|
923
|
+
|
|
924
|
+
return writer.finish()
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Renders the closed unsigned base configuration.
|
|
929
|
+
* @param {import("../semantic/types.js").IosApplicationConfiguration} configuration - Application configuration.
|
|
930
|
+
* @returns {RenderedConfigurationText} Base xcconfig and causal configuration spans.
|
|
931
|
+
*/
|
|
932
|
+
function renderBaseConfiguration(configuration) {
|
|
933
|
+
const writer = new ConfigurationTextWriter()
|
|
934
|
+
|
|
935
|
+
writer.append(`CODE_SIGNING_ALLOWED = NO
|
|
936
|
+
CODE_SIGNING_REQUIRED = NO
|
|
937
|
+
GENERATE_INFOPLIST_FILE = NO
|
|
938
|
+
IPHONEOS_DEPLOYMENT_TARGET = `)
|
|
939
|
+
writer.append(configurationFragment("deploymentTarget", configuration.deploymentTarget))
|
|
940
|
+
writer.append("\nPRODUCT_BUNDLE_IDENTIFIER = ")
|
|
941
|
+
writer.append(configurationFragment("bundleIdentifier", configuration.bundleIdentifier))
|
|
942
|
+
writer.append("\nPRODUCT_MODULE_NAME = ")
|
|
943
|
+
writer.append(configurationFragment("moduleName", configuration.moduleName))
|
|
944
|
+
writer.append("\nPRODUCT_NAME = ")
|
|
945
|
+
writer.append(configurationFragment("productName", configuration.productName))
|
|
946
|
+
writer.append(`
|
|
947
|
+
SDKROOT = iphoneos
|
|
948
|
+
SUPPORTED_PLATFORMS = iphoneos iphonesimulator
|
|
949
|
+
TARGETED_DEVICE_FAMILY = 1
|
|
950
|
+
`)
|
|
951
|
+
|
|
952
|
+
return writer.finish()
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* Renders the allowlisted application property list.
|
|
957
|
+
* @param {import("../semantic/types.js").IosApplicationConfiguration} configuration - Application configuration.
|
|
958
|
+
* @returns {RenderedConfigurationText} Property-list XML and causal configuration spans.
|
|
959
|
+
*/
|
|
960
|
+
function renderInfoPlist(configuration) {
|
|
961
|
+
const writer = new ConfigurationTextWriter()
|
|
962
|
+
|
|
963
|
+
writer.append(`<?xml version="1.0" encoding="UTF-8"?>
|
|
964
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
965
|
+
<plist version="1.0">
|
|
966
|
+
<dict>
|
|
967
|
+
<key>CFBundleDisplayName</key>
|
|
968
|
+
<string>`)
|
|
969
|
+
writer.append(configurationFragment("displayName", xmlEscape(configuration.displayName)))
|
|
970
|
+
writer.append(`</string>
|
|
971
|
+
<key>CFBundleExecutable</key>
|
|
972
|
+
<string>$(EXECUTABLE_NAME)</string>
|
|
973
|
+
<key>CFBundleIdentifier</key>
|
|
974
|
+
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
975
|
+
<key>CFBundleInfoDictionaryVersion</key>
|
|
976
|
+
<string>6.0</string>
|
|
977
|
+
<key>CFBundleName</key>
|
|
978
|
+
<string>$(PRODUCT_NAME)</string>
|
|
979
|
+
<key>CFBundlePackageType</key>
|
|
980
|
+
<string>APPL</string>
|
|
981
|
+
<key>CFBundleShortVersionString</key>
|
|
982
|
+
<string>1.0</string>
|
|
983
|
+
<key>CFBundleVersion</key>
|
|
984
|
+
<string>1</string>
|
|
985
|
+
<key>LSRequiresIPhoneOS</key>
|
|
986
|
+
<true/>
|
|
987
|
+
<key>UILaunchScreen</key>
|
|
988
|
+
<dict/>
|
|
989
|
+
</dict>
|
|
990
|
+
</plist>
|
|
991
|
+
`)
|
|
992
|
+
|
|
993
|
+
return writer.finish()
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* Renders pure-logic XCTest source.
|
|
998
|
+
* @param {import("../semantic/types.js").IosApplicationConfiguration} configuration - Application configuration.
|
|
999
|
+
* @returns {RenderedConfigurationText} XCTest source and causal configuration spans.
|
|
1000
|
+
*/
|
|
1001
|
+
function renderUnitTests(configuration) {
|
|
1002
|
+
const writer = new ConfigurationTextWriter()
|
|
1003
|
+
|
|
1004
|
+
writer.append("import XCTest\n@testable import ")
|
|
1005
|
+
writer.append(configurationFragment("moduleName", configuration.moduleName))
|
|
1006
|
+
writer.append("\n\nfinal class ")
|
|
1007
|
+
writer.append(configurationFragment("productName", configuration.productName))
|
|
1008
|
+
writer.append(`Tests: XCTestCase {
|
|
1009
|
+
func testSemanticOutputIsDeterministic() {
|
|
1010
|
+
XCTAssertEqual(SemantifoldBridge.run(), SemantifoldBridge.run())
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
func testOutputSinkPreservesScalarLines() {
|
|
1014
|
+
let output = SemantifoldOutputSink()
|
|
1015
|
+
output.write(Int64(7))
|
|
1016
|
+
output.write(true)
|
|
1017
|
+
output.write("é😀")
|
|
1018
|
+
XCTAssertEqual(output.lines, ["7", "true", "é😀"])
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
`)
|
|
1022
|
+
|
|
1023
|
+
return writer.finish()
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Renders the accessibility-route XCUI source.
|
|
1028
|
+
* @param {import("../semantic/types.js").IosApplicationConfiguration} configuration - Application configuration.
|
|
1029
|
+
* @returns {RenderedConfigurationText} XCUI source and causal configuration spans.
|
|
1030
|
+
*/
|
|
1031
|
+
function renderUiTests(configuration) {
|
|
1032
|
+
const writer = new ConfigurationTextWriter()
|
|
1033
|
+
|
|
1034
|
+
writer.append("import XCTest\n\nfinal class ")
|
|
1035
|
+
writer.append(configurationFragment("productName", configuration.productName))
|
|
1036
|
+
writer.append(`UITests: XCTestCase {
|
|
1037
|
+
func testOutputViewIsAccessible() {
|
|
1038
|
+
let application = XCUIApplication()
|
|
1039
|
+
application.launch()
|
|
1040
|
+
XCTAssertTrue(application.staticTexts["semantifold-output"].waitForExistence(timeout: 5))
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
`)
|
|
1044
|
+
|
|
1045
|
+
return writer.finish()
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Escapes XML text and attribute content.
|
|
1050
|
+
* @param {string} value - XML text or attribute value.
|
|
1051
|
+
* @returns {string} Escaped value.
|
|
1052
|
+
*/
|
|
1053
|
+
function xmlEscape(value) {
|
|
1054
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">")
|
|
1055
|
+
.replaceAll("\"", """).replaceAll("'", "'")
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* Creates one synthetic text artifact candidate.
|
|
1060
|
+
* @param {string} path - Artifact path.
|
|
1061
|
+
* @param {import("../semantic/types.js").GeneratedArtifactRole} role - Artifact role.
|
|
1062
|
+
* @param {string} content - Exact LF content.
|
|
1063
|
+
* @param {string} reason - Provenance reason.
|
|
1064
|
+
* @returns {import("../semantic/types.js").GeneratedSetArtifact} Artifact candidate.
|
|
1065
|
+
*/
|
|
1066
|
+
function syntheticText(path, role, content, reason) {
|
|
1067
|
+
return {
|
|
1068
|
+
content,
|
|
1069
|
+
contentKind: "text",
|
|
1070
|
+
mediaType: path.endsWith(".swift") ? "text/x-swift" : path.endsWith(".json") ? "application/json" :
|
|
1071
|
+
path.endsWith(".plist") || path.endsWith(".xcscheme") ? "application/xml" : "text/plain",
|
|
1072
|
+
ownership: "generated",
|
|
1073
|
+
path,
|
|
1074
|
+
provenance: {kind: "synthetic", reason, relatedOrigins: []},
|
|
1075
|
+
role
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/**
|
|
1080
|
+
* Creates a synthetic artifact and transfers its renderer-owned configuration spans to the manifest ledger.
|
|
1081
|
+
* @param {string} path - Artifact path.
|
|
1082
|
+
* @param {import("../semantic/types.js").GeneratedArtifactRole} role - Artifact role.
|
|
1083
|
+
* @param {RenderedConfigurationText} rendered - Rendered content and causal spans.
|
|
1084
|
+
* @param {ConfigurationArtifactSpan[]} spans - Application-wide causal span ledger.
|
|
1085
|
+
* @param {string} reason - Provenance reason.
|
|
1086
|
+
* @returns {import("../semantic/types.js").GeneratedSetArtifact} Artifact candidate.
|
|
1087
|
+
*/
|
|
1088
|
+
function configuredSyntheticText(path, role, rendered, spans, reason) {
|
|
1089
|
+
spans.push(...rendered.configurationSpans.map(span => ({...span, path})))
|
|
1090
|
+
|
|
1091
|
+
return syntheticText(path, role, rendered.content, reason)
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Serializes recursively sorted canonical JSON with an LF.
|
|
1096
|
+
* @param {unknown} value - JSON value.
|
|
1097
|
+
* @returns {string} Canonical JSON with LF.
|
|
1098
|
+
*/
|
|
1099
|
+
function stringifyCanonicalJson(value) {
|
|
1100
|
+
return `${JSON.stringify(sortJson(value), null, 2)}\n`
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
/**
|
|
1104
|
+
* Recursively sorts JSON object keys without reordering arrays.
|
|
1105
|
+
* @param {unknown} value - JSON value.
|
|
1106
|
+
* @returns {unknown} Recursively key-sorted JSON.
|
|
1107
|
+
*/
|
|
1108
|
+
function sortJson(value) {
|
|
1109
|
+
if (Array.isArray(value)) return value.map(sortJson)
|
|
1110
|
+
if (!isPlainObject(value)) return value
|
|
1111
|
+
|
|
1112
|
+
return Object.fromEntries(Object.keys(value).sort().map(key => [key, sortJson(value[key])]))
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/**
|
|
1116
|
+
* Validates and snapshots the strict baseline application configuration.
|
|
1117
|
+
* Identity and deployment fields intentionally have no defaults without Apple-lane evidence.
|
|
1118
|
+
* @param {unknown} candidate - Candidate configuration.
|
|
1119
|
+
* @returns {import("../semantic/types.js").IosApplicationConfiguration} Normalized configuration.
|
|
1120
|
+
*/
|
|
1121
|
+
function normalizeConfiguration(candidate) {
|
|
1122
|
+
if (!isPlainObject(candidate) || Object.keys(candidate).some(key => !configurationFields.has(key))) {
|
|
1123
|
+
invalidConfiguration("iOS application configuration must be a closed plain object.")
|
|
1124
|
+
}
|
|
1125
|
+
const productName = candidate.productName
|
|
1126
|
+
const moduleName = candidate.moduleName
|
|
1127
|
+
const organizationPrefix = candidate.organizationPrefix
|
|
1128
|
+
const bundleIdentifier = candidate.bundleIdentifier
|
|
1129
|
+
const deploymentTarget = candidate.deploymentTarget
|
|
1130
|
+
const displayName = candidate.displayName
|
|
1131
|
+
|
|
1132
|
+
if (typeof productName != "string" || !/^[A-Z][A-Za-z0-9]*$/u.test(productName)) {
|
|
1133
|
+
invalidConfiguration("Product name must be an ASCII upper-camel identifier.")
|
|
1134
|
+
}
|
|
1135
|
+
if (typeof moduleName != "string" || !/^[A-Z][A-Za-z0-9]*$/u.test(moduleName)) {
|
|
1136
|
+
invalidConfiguration("Module name must be an ASCII upper-camel identifier.")
|
|
1137
|
+
}
|
|
1138
|
+
if (typeof organizationPrefix != "string" || !isReverseDns(organizationPrefix, 2)) {
|
|
1139
|
+
invalidConfiguration("Organization prefix must be a lowercase reverse-DNS identity with at least two segments.")
|
|
1140
|
+
}
|
|
1141
|
+
if (typeof bundleIdentifier != "string" || !isReverseDns(bundleIdentifier, 3) ||
|
|
1142
|
+
!bundleIdentifier.startsWith(`${organizationPrefix}.`)) {
|
|
1143
|
+
invalidConfiguration("Bundle identifier must extend the exact organization prefix as lowercase reverse DNS.")
|
|
1144
|
+
}
|
|
1145
|
+
if (typeof deploymentTarget != "string" || !/^[1-9][0-9]*\.(?:0|[1-9][0-9]*)$/u.test(deploymentTarget)) {
|
|
1146
|
+
invalidConfiguration("Deployment target must be a caller-supplied canonical major.minor version.")
|
|
1147
|
+
}
|
|
1148
|
+
if (typeof displayName != "string" || displayName.length == 0 || displayName.trim() != displayName ||
|
|
1149
|
+
[...displayName].length > 64 || !hasOnlyUnicodeScalars(displayName) || /\p{Cc}/u.test(displayName)) {
|
|
1150
|
+
invalidConfiguration("Display name must be a non-empty single-line Unicode scalar string of at most 64 characters.")
|
|
1151
|
+
}
|
|
1152
|
+
const sourceRoot = candidate.sourceRoot ?? "Sources"
|
|
1153
|
+
const resourceRoot = candidate.resourceRoot ?? "Assets.xcassets"
|
|
1154
|
+
const lifecycle = candidate.lifecycle ?? "swiftui"
|
|
1155
|
+
|
|
1156
|
+
if (sourceRoot != "Sources" || !isSafeArtifactPath(sourceRoot)) invalidConfiguration("The supported source root is exactly 'Sources'.")
|
|
1157
|
+
if (resourceRoot != "Assets.xcassets" || !isSafeArtifactPath(resourceRoot)) {
|
|
1158
|
+
invalidConfiguration("The supported resource root is exactly 'Assets.xcassets'.")
|
|
1159
|
+
}
|
|
1160
|
+
if (lifecycle != "swiftui") invalidConfiguration("The supported application lifecycle is exactly 'swiftui'.")
|
|
1161
|
+
const infoPlist = candidate.infoPlist ?? {}
|
|
1162
|
+
|
|
1163
|
+
if (!isPlainObject(infoPlist) || Object.keys(infoPlist).length > 0) {
|
|
1164
|
+
invalidConfiguration("The baseline caller Info.plist extension allowlist is empty.")
|
|
1165
|
+
}
|
|
1166
|
+
const permissions = emptyConfigurationSet(candidate.permissions, "permissions")
|
|
1167
|
+
const entitlements = emptyConfigurationSet(candidate.entitlements, "entitlements")
|
|
1168
|
+
const privacyDeclarations = emptyConfigurationSet(candidate.privacyDeclarations, "privacy declarations")
|
|
1169
|
+
const capabilities = emptyConfigurationSet(candidate.capabilities, "capabilities")
|
|
1170
|
+
|
|
1171
|
+
return {
|
|
1172
|
+
bundleIdentifier,
|
|
1173
|
+
capabilities,
|
|
1174
|
+
deploymentTarget,
|
|
1175
|
+
displayName,
|
|
1176
|
+
entitlements,
|
|
1177
|
+
infoPlist: {},
|
|
1178
|
+
lifecycle: "swiftui",
|
|
1179
|
+
moduleName,
|
|
1180
|
+
organizationPrefix,
|
|
1181
|
+
permissions,
|
|
1182
|
+
privacyDeclarations,
|
|
1183
|
+
productName,
|
|
1184
|
+
resourceRoot: "Assets.xcassets",
|
|
1185
|
+
sourceRoot: "Sources"
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/**
|
|
1190
|
+
* Validates one optional closed empty configuration set.
|
|
1191
|
+
* @param {unknown} candidate - Optional closed set.
|
|
1192
|
+
* @param {string} label - Diagnostic label.
|
|
1193
|
+
* @returns {never[]} Empty snapshot.
|
|
1194
|
+
*/
|
|
1195
|
+
function emptyConfigurationSet(candidate, label) {
|
|
1196
|
+
if (candidate === undefined) return []
|
|
1197
|
+
if (!isDenseArray(candidate) || candidate.length != 0) invalidConfiguration(`The baseline ${label} set must be empty.`)
|
|
1198
|
+
|
|
1199
|
+
return []
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* Tests one canonical lowercase reverse-DNS identity.
|
|
1204
|
+
* @param {string} value - Candidate identity.
|
|
1205
|
+
* @param {number} segments - Minimum segments.
|
|
1206
|
+
* @returns {boolean} Validity.
|
|
1207
|
+
*/
|
|
1208
|
+
function isReverseDns(value, segments) {
|
|
1209
|
+
const parts = value.split(".")
|
|
1210
|
+
|
|
1211
|
+
return parts.length >= segments && parts.every(part => /^[a-z][a-z0-9-]*$/u.test(part) && !part.endsWith("-"))
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* Validates, hashes, snapshots, and canonically sorts caller assets.
|
|
1216
|
+
* @param {unknown} candidate - Candidate asset array.
|
|
1217
|
+
* @param {string} resourceRoot - Exact resource root.
|
|
1218
|
+
* @returns {import("../semantic/types.js").IosApplicationAssetInput[]} Normalized assets.
|
|
1219
|
+
*/
|
|
1220
|
+
function normalizeAssets(candidate, resourceRoot) {
|
|
1221
|
+
if (candidate === undefined) return []
|
|
1222
|
+
if (!isDenseArray(candidate)) invalidAsset("Application assets must be a dense array.")
|
|
1223
|
+
/** @type {import("../semantic/types.js").IosApplicationAssetInput[]} */
|
|
1224
|
+
const assets = []
|
|
1225
|
+
|
|
1226
|
+
for (let index = 0; index < candidate.length; index += 1) {
|
|
1227
|
+
const asset = candidate[index]
|
|
1228
|
+
|
|
1229
|
+
if (!isPlainObject(asset) || Object.keys(asset).some(key => !assetFields.has(key)) ||
|
|
1230
|
+
Object.keys(asset).length != assetFields.size) invalidAsset(`Asset ${index} must use the closed asset schema.`)
|
|
1231
|
+
const path = asset.path
|
|
1232
|
+
const content = asset.content
|
|
1233
|
+
const mediaType = asset.mediaType
|
|
1234
|
+
const sha256 = asset.sha256
|
|
1235
|
+
|
|
1236
|
+
if (!isSafeArtifactPath(path) || !path.startsWith(`${resourceRoot}/`) || path == `${resourceRoot}/Contents.json`) {
|
|
1237
|
+
invalidAsset(`Asset ${index} requires a safe path below '${resourceRoot}'.`)
|
|
1238
|
+
}
|
|
1239
|
+
if (typeof content != "string" && !(content instanceof Uint8Array) ||
|
|
1240
|
+
typeof content == "string" && (content.length == 0 || !hasOnlyUnicodeScalars(content)) ||
|
|
1241
|
+
content instanceof Uint8Array && content.byteLength == 0) invalidAsset(`Asset '${path}' requires non-empty exact content.`)
|
|
1242
|
+
if (typeof mediaType != "string" || !mediaTypePattern.test(mediaType) || /[\r\n]/u.test(mediaType)) {
|
|
1243
|
+
invalidAsset(`Asset '${path}' requires an explicit valid media type.`)
|
|
1244
|
+
}
|
|
1245
|
+
const snapshot = typeof content == "string" ? content : new Uint8Array(content)
|
|
1246
|
+
const actualHash = createHash("sha256").update(snapshot).digest("hex")
|
|
1247
|
+
|
|
1248
|
+
if (typeof sha256 != "string" || !/^[0-9a-f]{64}$/u.test(sha256) || sha256 != actualHash) {
|
|
1249
|
+
invalidAsset(`Asset '${path}' SHA-256 does not match its exact content.`)
|
|
1250
|
+
}
|
|
1251
|
+
assets.push({content: snapshot, mediaType, path, sha256})
|
|
1252
|
+
}
|
|
1253
|
+
assets.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0)
|
|
1254
|
+
|
|
1255
|
+
return assets
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* Creates an exact text or binary asset artifact.
|
|
1260
|
+
* @param {import("../semantic/types.js").IosApplicationAssetInput} asset - Validated asset.
|
|
1261
|
+
* @returns {import("../semantic/types.js").GeneratedSetArtifact} Artifact candidate.
|
|
1262
|
+
*/
|
|
1263
|
+
function assetArtifact(asset) {
|
|
1264
|
+
if (typeof asset.content == "string") return {
|
|
1265
|
+
content: asset.content,
|
|
1266
|
+
contentKind: "text",
|
|
1267
|
+
mediaType: asset.mediaType,
|
|
1268
|
+
ownership: "generated",
|
|
1269
|
+
path: asset.path,
|
|
1270
|
+
provenance: {kind: "synthetic", reason: `Exact caller-provided iOS asset (sha256:${asset.sha256}).`, relatedOrigins: []},
|
|
1271
|
+
role: "resource"
|
|
1272
|
+
}
|
|
1273
|
+
const content = new Uint8Array(asset.content)
|
|
1274
|
+
|
|
1275
|
+
return {
|
|
1276
|
+
content,
|
|
1277
|
+
contentKind: "binary",
|
|
1278
|
+
mediaType: asset.mediaType,
|
|
1279
|
+
ownership: "generated",
|
|
1280
|
+
path: asset.path,
|
|
1281
|
+
provenance: {kind: "bytes", mapping: createByteMapping({
|
|
1282
|
+
byteLength: content.byteLength,
|
|
1283
|
+
path: asset.path,
|
|
1284
|
+
ranges: [{
|
|
1285
|
+
generated: {end: content.byteLength, start: 0},
|
|
1286
|
+
origin: {kind: "synthetic", reason: `Exact caller-provided iOS asset (sha256:${asset.sha256}).`, relatedOrigins: []},
|
|
1287
|
+
role: "asset"
|
|
1288
|
+
}]
|
|
1289
|
+
})},
|
|
1290
|
+
role: "resource"
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/**
|
|
1295
|
+
* Throws a configuration diagnostic.
|
|
1296
|
+
* @param {string} message - Failure detail.
|
|
1297
|
+
* @returns {never} Always throws.
|
|
1298
|
+
*/
|
|
1299
|
+
function invalidConfiguration(message) {
|
|
1300
|
+
throw new SemantifoldDiagnostic({code: "INVALID_APPLICATION_CONFIGURATION", language: "ios", message})
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* Throws an asset diagnostic.
|
|
1305
|
+
* @param {string} message - Failure detail.
|
|
1306
|
+
* @returns {never} Always throws.
|
|
1307
|
+
*/
|
|
1308
|
+
function invalidAsset(message) {
|
|
1309
|
+
throw new SemantifoldDiagnostic({code: "INVALID_APPLICATION_ASSET", language: "ios", message})
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* Throws an application-path diagnostic.
|
|
1314
|
+
* @param {string} message - Failure detail.
|
|
1315
|
+
* @returns {never} Always throws.
|
|
1316
|
+
*/
|
|
1317
|
+
function invalidPath(message) {
|
|
1318
|
+
throw new SemantifoldDiagnostic({code: "INVALID_APPLICATION_PATH", language: "ios", message})
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/**
|
|
1322
|
+
* Emits one backend-namespaced semantic module.
|
|
1323
|
+
* @param {SourceWriter} writer - Source-aware generated Swift writer.
|
|
1324
|
+
* @param {import("../semantic/types.js").SemanticModule} module - Prepared semantic module.
|
|
1325
|
+
* @param {string} moduleId - Stable project module identity.
|
|
1326
|
+
* @param {boolean} entry - Whether this module owns the semantic entry.
|
|
1327
|
+
* @param {Map<string | undefined, {declaration: import("../semantic/types.js").FunctionDeclaration, moduleId: string}>} declarations - Resolved functions.
|
|
1328
|
+
*/
|
|
1329
|
+
function emitSemanticModule(writer, module, moduleId, entry, declarations) {
|
|
1330
|
+
const namespace = `SemantifoldModule${moduleName(moduleId)}`
|
|
1331
|
+
|
|
1332
|
+
writer.synthetic(`enum ${namespace} {\n`, "iOS semantic module namespace", [module], [""])
|
|
1333
|
+
module.functions.forEach((declaration, functionIndex) => {
|
|
1334
|
+
const path = `/functions/${functionIndex}`
|
|
1335
|
+
|
|
1336
|
+
writer.synthetic(" static ", "iOS namespaced function scaffold", [declaration], [path])
|
|
1337
|
+
writer.mapped("func", {mappingKind: "anchor", node: declaration, path})
|
|
1338
|
+
writer.synthetic(" ", "declaration spacing", [declaration], [path])
|
|
1339
|
+
writer.mapped(declaration.name, {mappingKind: "exact", node: declaration, path, role: "name"})
|
|
1340
|
+
writer.synthetic("(", "function parameter scaffold", [declaration], [path])
|
|
1341
|
+
declaration.parameters.forEach((parameter, parameterIndex) => {
|
|
1342
|
+
const parameterPath = `${path}/parameters/${parameterIndex}`
|
|
1343
|
+
|
|
1344
|
+
if (parameterIndex) writer.synthetic(", ", "parameter separator", [parameter], [parameterPath])
|
|
1345
|
+
writer.synthetic("_ ", "unlabeled Swift parameter scaffold", [parameter], [parameterPath])
|
|
1346
|
+
writer.mapped(parameter.name, {mappingKind: "exact", node: parameter, path: parameterPath, role: "name"})
|
|
1347
|
+
writer.synthetic(": ", "parameter annotation scaffold", [parameter], [parameterPath])
|
|
1348
|
+
writer.mapped(emitScalarType("swift", parameter.type), {mappingKind: "exact", node: parameter.type,
|
|
1349
|
+
path: `${parameterPath}/type`, role: "type"})
|
|
1350
|
+
})
|
|
1351
|
+
writer.synthetic(", _ semantifold_output: SemantifoldOutputSink) -> ", "shared iOS output sink parameter", [declaration], [path])
|
|
1352
|
+
writer.mapped(emitScalarType("swift", declaration.returnType), {mappingKind: "exact", node: declaration.returnType,
|
|
1353
|
+
path: `${path}/returnType`, role: "type"})
|
|
1354
|
+
writer.synthetic(" {\n", "Swift function body scaffold", [declaration], [path])
|
|
1355
|
+
emitBlock(writer, declaration.body, " ", `${path}/body`, declarations)
|
|
1356
|
+
writer.synthetic(" }\n", "Swift function body scaffold", [declaration], [path])
|
|
1357
|
+
})
|
|
1358
|
+
if (entry) {
|
|
1359
|
+
writer.synthetic(" static func semantifoldEntry() -> [String] {\n", "iOS semantic entry scaffold", [module.entryPoint], ["/entryPoint"])
|
|
1360
|
+
writer.synthetic(" let semantifold_output = SemantifoldOutputSink()\n", "shared iOS output sink allocation",
|
|
1361
|
+
[module.entryPoint], ["/entryPoint"])
|
|
1362
|
+
emitBlock(writer, module.entryPoint.body, " ", "/entryPoint/body", declarations)
|
|
1363
|
+
writer.synthetic(" return semantifold_output.lines\n", "iOS semantic output return", [module.entryPoint], ["/entryPoint"])
|
|
1364
|
+
writer.synthetic(" }\n", "iOS semantic entry scaffold", [module.entryPoint], ["/entryPoint"])
|
|
1365
|
+
}
|
|
1366
|
+
writer.synthetic("}\n", "iOS semantic module namespace", [module], [""])
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
/**
|
|
1370
|
+
* Emits a validated ordered semantic block.
|
|
1371
|
+
* @param {SourceWriter} writer - Source writer.
|
|
1372
|
+
* @param {import("../semantic/types.js").Block} block - Semantic block.
|
|
1373
|
+
* @param {string} indent - Current indentation.
|
|
1374
|
+
* @param {string} path - Block occurrence path.
|
|
1375
|
+
* @param {Map<string | undefined, {declaration: import("../semantic/types.js").FunctionDeclaration, moduleId: string}>} declarations - Resolved functions.
|
|
1376
|
+
*/
|
|
1377
|
+
function emitBlock(writer, block, indent, path, declarations) {
|
|
1378
|
+
block.statements.forEach((statement, index) => emitStatement(writer, statement, indent, `${path}/statements/${index}`, declarations))
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
/**
|
|
1382
|
+
* Emits one Swift statement through the shared capture sink.
|
|
1383
|
+
* @param {SourceWriter} writer - Source writer.
|
|
1384
|
+
* @param {import("../semantic/types.js").Statement} statement - Validated statement.
|
|
1385
|
+
* @param {string} indent - Current indentation.
|
|
1386
|
+
* @param {string} path - Statement occurrence path.
|
|
1387
|
+
* @param {Map<string | undefined, {declaration: import("../semantic/types.js").FunctionDeclaration, moduleId: string}>} declarations - Resolved functions.
|
|
1388
|
+
*/
|
|
1389
|
+
function emitStatement(writer, statement, indent, path, declarations) {
|
|
1390
|
+
writer.synthetic(indent, "indentation", [statement], [path])
|
|
1391
|
+
if (statement.kind == "LocalDeclaration") {
|
|
1392
|
+
writer.mapped(statement.mutable ? "var" : "let", {mappingKind: "anchor", node: statement, path})
|
|
1393
|
+
writer.synthetic(" ", "declaration spacing", [statement], [path])
|
|
1394
|
+
writer.mapped(statement.name, {mappingKind: "exact", node: statement, path, role: "name"})
|
|
1395
|
+
writer.synthetic(": ", "local annotation scaffold", [statement], [path])
|
|
1396
|
+
writer.mapped(emitScalarType("swift", statement.type), {mappingKind: "exact", node: statement.type,
|
|
1397
|
+
path: `${path}/type`, role: "type"})
|
|
1398
|
+
writer.synthetic(" = ", "assignment spacing", [statement], [path])
|
|
1399
|
+
emitExpression(writer, statement.initializer, `${path}/initializer`, declarations)
|
|
1400
|
+
writer.synthetic("\n", "line break", [statement], [path])
|
|
1401
|
+
if (statement.mutable) writer.synthetic(`${indent}semantifold_keep_mutable(&${statement.name})\n`,
|
|
1402
|
+
"Swift mutable-local warning marker", [statement], [path])
|
|
1403
|
+
return
|
|
1404
|
+
}
|
|
1405
|
+
if (statement.kind == "AssignmentStatement") {
|
|
1406
|
+
writer.mapped(statement.target.name, {mappingKind: "exact", node: statement.target, path: `${path}/target`, role: "name"})
|
|
1407
|
+
writer.synthetic(" = ", "assignment spacing", [statement], [path])
|
|
1408
|
+
emitExpression(writer, statement.expression, `${path}/expression`, declarations)
|
|
1409
|
+
writer.synthetic("\n", "line break", [statement], [path])
|
|
1410
|
+
return
|
|
1411
|
+
}
|
|
1412
|
+
if (statement.kind == "ReturnStatement") {
|
|
1413
|
+
if (!statement.expression) throw new TypeError("iOS Swift bare return reached emission.")
|
|
1414
|
+
writer.mapped("return", {mappingKind: "anchor", node: statement, path})
|
|
1415
|
+
writer.synthetic(" ", "return spacing", [statement], [path])
|
|
1416
|
+
emitExpression(writer, statement.expression, `${path}/expression`, declarations)
|
|
1417
|
+
writer.synthetic("\n", "line break", [statement], [path])
|
|
1418
|
+
return
|
|
1419
|
+
}
|
|
1420
|
+
if (statement.kind == "PrintStatement") {
|
|
1421
|
+
writer.synthetic("semantifold_output.write(", "iOS output capture", [statement], [path])
|
|
1422
|
+
emitExpression(writer, statement.expression, `${path}/expression`, declarations)
|
|
1423
|
+
writer.synthetic(")\n", "iOS output capture", [statement], [path])
|
|
1424
|
+
return
|
|
1425
|
+
}
|
|
1426
|
+
if (statement.kind != "IfStatement") throw new TypeError("Unsupported iOS Swift statement reached emission.")
|
|
1427
|
+
writer.mapped("if", {mappingKind: "anchor", node: statement, path})
|
|
1428
|
+
writer.synthetic(" ", "conditional spacing", [statement], [path])
|
|
1429
|
+
emitExpression(writer, statement.condition, `${path}/condition`, declarations)
|
|
1430
|
+
writer.synthetic(" {\n", "Swift conditional scaffold", [statement.consequent], [`${path}/consequent`])
|
|
1431
|
+
emitBlock(writer, statement.consequent, `${indent} `, `${path}/consequent`, declarations)
|
|
1432
|
+
writer.synthetic(`${indent}}`, "Swift conditional scaffold", [statement.consequent], [`${path}/consequent`])
|
|
1433
|
+
if (statement.alternate) {
|
|
1434
|
+
writer.synthetic(" else {\n", "Swift alternate scaffold", [statement.alternate], [`${path}/alternate`])
|
|
1435
|
+
emitBlock(writer, statement.alternate, `${indent} `, `${path}/alternate`, declarations)
|
|
1436
|
+
writer.synthetic(`${indent}}`, "Swift alternate scaffold", [statement.alternate], [`${path}/alternate`])
|
|
1437
|
+
}
|
|
1438
|
+
writer.synthetic("\n", "line break", [statement], [path])
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
/**
|
|
1442
|
+
* Emits one validated Tasks 001-004 expression and threads the shared sink through calls.
|
|
1443
|
+
* @param {SourceWriter} writer - Source writer.
|
|
1444
|
+
* @param {import("../semantic/types.js").Expression} expression - Semantic expression.
|
|
1445
|
+
* @param {string} path - Expression occurrence path.
|
|
1446
|
+
* @param {Map<string | undefined, {declaration: import("../semantic/types.js").FunctionDeclaration, moduleId: string}>} declarations - Resolved functions.
|
|
1447
|
+
*/
|
|
1448
|
+
function emitExpression(writer, expression, path, declarations) {
|
|
1449
|
+
if (expression.kind == "IdentifierExpression") {
|
|
1450
|
+
writer.mapped(expression.name, {mappingKind: "exact", node: expression, path, role: "name"})
|
|
1451
|
+
return
|
|
1452
|
+
}
|
|
1453
|
+
if (expression.kind == "IntegerLiteral") {
|
|
1454
|
+
writer.mapped(String(expression.value), {mappingKind: "exact", node: expression, path, role: "literal"})
|
|
1455
|
+
return
|
|
1456
|
+
}
|
|
1457
|
+
if (expression.kind == "BooleanLiteral") {
|
|
1458
|
+
writer.mapped(expression.value ? "true" : "false", {mappingKind: "exact", node: expression, path, role: "literal"})
|
|
1459
|
+
return
|
|
1460
|
+
}
|
|
1461
|
+
if (expression.kind == "StringLiteral") {
|
|
1462
|
+
writer.mapped(emitStringLiteral("swift", expression.value), {mappingKind: "exact", node: expression, path, role: "literal"})
|
|
1463
|
+
return
|
|
1464
|
+
}
|
|
1465
|
+
if (expression.kind == "CallExpression") {
|
|
1466
|
+
const resolved = declarations.get(expression.resolution?.declarationId)
|
|
1467
|
+
|
|
1468
|
+
if (!resolved) throw new TypeError(`Unknown validated iOS function '${expression.resolution?.declarationId}'.`)
|
|
1469
|
+
writer.mapped(`SemantifoldModule${moduleName(resolved.moduleId)}.${resolved.declaration.name}`, {
|
|
1470
|
+
mappingKind: "exact", name: expression.callee, node: expression, path, role: "callee"
|
|
1471
|
+
})
|
|
1472
|
+
writer.synthetic("(", "iOS semantic function call", [expression], [path])
|
|
1473
|
+
expression.arguments.forEach((argument, index) => {
|
|
1474
|
+
if (index) writer.synthetic(", ", "argument separator", [expression], [path])
|
|
1475
|
+
emitExpression(writer, argument, `${path}/arguments/${index}`, declarations)
|
|
1476
|
+
})
|
|
1477
|
+
writer.synthetic(", semantifold_output)", "shared iOS output sink argument", [expression], [path])
|
|
1478
|
+
return
|
|
1479
|
+
}
|
|
1480
|
+
if (expression.kind == "UnaryExpression") {
|
|
1481
|
+
writer.mapped("(", {mappingKind: "anchor", node: expression, path})
|
|
1482
|
+
writer.mapped(unaryOperations[expression.operation], {mappingKind: "exact", node: expression, path, role: "operator"})
|
|
1483
|
+
emitExpression(writer, expression.operand, `${path}/operand`, declarations)
|
|
1484
|
+
writer.mapped(")", {mappingKind: "anchor", node: expression, path})
|
|
1485
|
+
return
|
|
1486
|
+
}
|
|
1487
|
+
if (expression.kind != "BinaryExpression") throw new TypeError("Unsupported iOS Swift expression reached emission.")
|
|
1488
|
+
if (expression.operation == "StringEqual" || expression.operation == "StringNotEqual") {
|
|
1489
|
+
writer.mapped(expression.operation == "StringEqual" ? "semantifold_string_equal" : "semantifold_string_not_equal",
|
|
1490
|
+
{mappingKind: "exact", node: expression, path, role: "operator"})
|
|
1491
|
+
writer.synthetic("(", "Swift scalar-equality helper call", [expression], [path])
|
|
1492
|
+
emitExpression(writer, expression.left, `${path}/left`, declarations)
|
|
1493
|
+
writer.synthetic(", ", "Swift scalar-equality argument separator", [expression], [path])
|
|
1494
|
+
emitExpression(writer, expression.right, `${path}/right`, declarations)
|
|
1495
|
+
writer.synthetic(")", "Swift scalar-equality helper call", [expression], [path])
|
|
1496
|
+
return
|
|
1497
|
+
}
|
|
1498
|
+
writer.mapped("(", {mappingKind: "anchor", node: expression, path})
|
|
1499
|
+
emitExpression(writer, expression.left, `${path}/left`, declarations)
|
|
1500
|
+
writer.synthetic(" ", "operator spacing", [expression], [path])
|
|
1501
|
+
writer.mapped(binaryOperations[expression.operation], {mappingKind: "exact", node: expression, path, role: "operator"})
|
|
1502
|
+
writer.synthetic(" ", "operator spacing", [expression], [path])
|
|
1503
|
+
emitExpression(writer, expression.right, `${path}/right`, declarations)
|
|
1504
|
+
writer.mapped(")", {mappingKind: "anchor", node: expression, path})
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
/**
|
|
1508
|
+
* Converts a validated logical module identity to its Swift namespace suffix.
|
|
1509
|
+
* @param {string} id - Logical module identity.
|
|
1510
|
+
* @returns {string} Deterministic Swift namespace suffix and filename.
|
|
1511
|
+
*/
|
|
1512
|
+
function moduleName(id) {
|
|
1513
|
+
return id.split(/[._-]+/u).map(part => `${part[0].toUpperCase()}${part.slice(1)}`).join("")
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
/**
|
|
1517
|
+
* Clones and normalizes one provenance-bearing semantic module into a one-module project.
|
|
1518
|
+
* @param {import("../semantic/types.js").SemanticModule | undefined} candidate - Candidate module.
|
|
1519
|
+
* @returns {import("../semantic/types.js").SemanticProgram} Normalized program.
|
|
1520
|
+
*/
|
|
1521
|
+
function normalizeSingleModule(candidate) {
|
|
1522
|
+
if (!isPlainObject(candidate)) unsupportedCapability("ios", "missing or invalid module", undefined)
|
|
1523
|
+
const module = structuredClone(candidate)
|
|
1524
|
+
const provenanceSources = module.provenance?.sources
|
|
1525
|
+
|
|
1526
|
+
if (!Array.isArray(provenanceSources) || provenanceSources.length != 1 || provenanceSources[0].content === null ||
|
|
1527
|
+
provenanceSources[0].filename != module.location?.filename || typeof provenanceSources[0].language != "string") {
|
|
1528
|
+
unsupportedCapability("ios", "single-module source ownership", module.location)
|
|
1529
|
+
}
|
|
1530
|
+
rekeyModuleDeclarations(module, "main")
|
|
1531
|
+
const exports = [
|
|
1532
|
+
...(module.records ?? []).map(declaration => applicationExport(declaration, "record")),
|
|
1533
|
+
...(module.classes ?? []).map(declaration => applicationExport(declaration, "class")),
|
|
1534
|
+
...(module.errors ?? []).map(declaration => applicationExport(declaration, "error")),
|
|
1535
|
+
...module.functions.map(declaration => applicationExport(declaration, "function"))
|
|
1536
|
+
]
|
|
1537
|
+
const programModule = /** @type {import("../semantic/types.js").SemanticProgramModule} */ ({
|
|
1538
|
+
...module,
|
|
1539
|
+
exports,
|
|
1540
|
+
id: "main",
|
|
1541
|
+
imports: [],
|
|
1542
|
+
sourceFilename: provenanceSources[0].filename
|
|
1543
|
+
})
|
|
1544
|
+
|
|
1545
|
+
return {
|
|
1546
|
+
entryModule: "main",
|
|
1547
|
+
kind: "Program",
|
|
1548
|
+
modules: [programModule],
|
|
1549
|
+
sources: structuredClone(provenanceSources)
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
/**
|
|
1554
|
+
* Creates a normalized export for a single-module declaration without inventing a source range.
|
|
1555
|
+
* @param {{id?: string, location: import("../semantic/types.js").SourceLocation, name: string}} declaration - Exported declaration.
|
|
1556
|
+
* @param {import("../semantic/types.js").SemanticDeclarationKind} symbolKind - Declaration namespace.
|
|
1557
|
+
* @returns {import("../semantic/types.js").SemanticExport} Synthetic project edge anchored to its declaration.
|
|
1558
|
+
*/
|
|
1559
|
+
function applicationExport(declaration, symbolKind) {
|
|
1560
|
+
return {
|
|
1561
|
+
declarationId: /** @type {string} */ (declaration.id),
|
|
1562
|
+
exportedName: declaration.name,
|
|
1563
|
+
kind: "Export",
|
|
1564
|
+
location: declaration.location,
|
|
1565
|
+
symbolKind
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
/**
|
|
1570
|
+
* Prefixes declaration identities and typed references in a cloned single module.
|
|
1571
|
+
* @param {import("../semantic/types.js").SemanticModule} module - Cloned module.
|
|
1572
|
+
* @param {string} moduleId - Normalized module identity.
|
|
1573
|
+
* @returns {void}
|
|
1574
|
+
*/
|
|
1575
|
+
function rekeyModuleDeclarations(module, moduleId) {
|
|
1576
|
+
const declarations = [...module.records ?? [], ...module.classes ?? [], ...module.errors ?? [], ...module.functions]
|
|
1577
|
+
const replacements = new Map(declarations
|
|
1578
|
+
.filter(declaration => typeof declaration.id == "string")
|
|
1579
|
+
.map(declaration => [declaration.id, `${moduleId}#${declaration.id}`]))
|
|
1580
|
+
|
|
1581
|
+
for (const declaration of declarations) {
|
|
1582
|
+
if (typeof declaration.id == "string") declaration.id = replacements.get(declaration.id)
|
|
1583
|
+
}
|
|
1584
|
+
const seen = new Set()
|
|
1585
|
+
const identityFields = new Set(["classId", "declarationId", "field", "method", "parameterId"])
|
|
1586
|
+
/**
|
|
1587
|
+
* Rewrites declaration identities in one cloned semantic subtree.
|
|
1588
|
+
* @param {unknown} value - Candidate semantic subtree.
|
|
1589
|
+
* @returns {void}
|
|
1590
|
+
*/
|
|
1591
|
+
function visit(value) {
|
|
1592
|
+
if (!value || typeof value != "object" || seen.has(value)) return
|
|
1593
|
+
seen.add(value)
|
|
1594
|
+
if (!Array.isArray(value)) {
|
|
1595
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
1596
|
+
if (identityFields.has(key) && typeof nested == "string" && replacements.has(nested)) {
|
|
1597
|
+
Reflect.set(value, key, replacements.get(nested))
|
|
1598
|
+
} else visit(nested)
|
|
1599
|
+
}
|
|
1600
|
+
} else for (const nested of value) visit(nested)
|
|
1601
|
+
}
|
|
1602
|
+
visit(module)
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
/**
|
|
1606
|
+
* Tests for a plain request object.
|
|
1607
|
+
* @param {unknown} value - Candidate request.
|
|
1608
|
+
* @returns {value is Record<string, unknown>} Whether it is a plain object.
|
|
1609
|
+
*/
|
|
1610
|
+
function isPlainObject(value) {
|
|
1611
|
+
return Boolean(value) && typeof value == "object" && !Array.isArray(value) && Object.getPrototypeOf(value) == Object.prototype
|
|
1612
|
+
}
|