@greenpandastudios/aug-cli 0.19.0 → 0.20.1
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 +9 -6
- package/THIRD_PARTY_NOTICES.md +4 -0
- package/bin/aug.mjs +12 -2
- package/docs/about.md +27 -0
- package/docs/api/crypto.md +103 -69
- package/docs/api/io.md +35 -33
- package/docs/api/json.md +3 -3
- package/docs/api/memory.md +23 -19
- package/docs/api/time.md +10 -9
- package/docs/api/web.md +33 -38
- package/docs/compatibility.md +3 -3
- package/docs/contributing-benchmarks.md +38 -0
- package/docs/dev-containers.md +95 -0
- package/docs/diagnostics.md +15 -5
- package/docs/docker.md +174 -12
- package/docs/editor.md +35 -0
- package/docs/example-projects.json +206 -24
- package/docs/examples/approved-design/counters.md +34 -126
- package/docs/examples/approved-design/dependencies/august/0.20.1/io/contracts.md +178 -0
- package/docs/examples/approved-design/domain/app.md +13 -77
- package/docs/examples/approved-design/domain/export.md +5 -19
- package/docs/examples/approved-design/domain/models.md +4 -23
- package/docs/examples/approved-design/domain/numbers.md +24 -114
- package/docs/examples/approved-design/index.md +16 -7
- package/docs/examples/approved-design/main.md +19 -96
- package/docs/examples/benchmark/index.md +6 -5
- package/docs/examples/benchmark/main.md +7 -35
- package/docs/examples/cli-args/index.md +6 -5
- package/docs/examples/cli-args/main.md +6 -31
- package/docs/examples/collections/index.md +6 -5
- package/docs/examples/collections/main.md +7 -35
- package/docs/examples/collections-benchmark/index.md +6 -5
- package/docs/examples/collections-benchmark/main.md +7 -35
- package/docs/examples/cpu-benchmark/index.md +6 -5
- package/docs/examples/cpu-benchmark/main.md +5 -24
- package/docs/examples/developer-workflow/calculator.md +39 -186
- package/docs/examples/developer-workflow/dependencies/august/0.20.1/io/contracts.md +178 -0
- package/docs/examples/developer-workflow/index.md +16 -7
- package/docs/examples/developer-workflow/logging/console.md +10 -56
- package/docs/examples/developer-workflow/logging/export.md +4 -14
- package/docs/examples/developer-workflow/logging/logger.md +7 -43
- package/docs/examples/developer-workflow/main.md +17 -69
- package/docs/examples/drop/index.md +6 -5
- package/docs/examples/drop/main.md +7 -27
- package/docs/examples/drop/resource.md +7 -30
- package/docs/examples/errors/errors.md +7 -27
- package/docs/examples/errors/index.md +6 -5
- package/docs/examples/errors/main.md +7 -28
- package/docs/examples/ffi/index.md +6 -5
- package/docs/examples/ffi/main.md +6 -20
- package/docs/examples/ffi/native.md +8 -34
- package/docs/examples/generic-di/dependencies/august/0.20.1/io/contracts.md +178 -0
- package/docs/examples/generic-di/index.md +6 -5
- package/docs/examples/generic-di/main.md +9 -39
- package/docs/examples/generic-di/types.md +23 -92
- package/docs/examples/generics/index.md +6 -5
- package/docs/examples/generics/main.md +10 -47
- package/docs/examples/generics/types.md +25 -100
- package/docs/examples/hello/app/export.md +4 -12
- package/docs/examples/hello/app/greeter.md +13 -91
- package/docs/examples/hello/dependencies/august/0.20.1/io/contracts.md +178 -0
- package/docs/examples/hello/index.md +17 -6
- package/docs/examples/hello/logging/console.md +10 -54
- package/docs/examples/hello/logging/export.md +4 -14
- package/docs/examples/hello/logging/logger.md +7 -45
- package/docs/examples/hello/main.md +9 -39
- package/docs/examples/http-benchmark/index.md +6 -5
- package/docs/examples/http-benchmark/main.md +6 -20
- package/docs/examples/http-benchmark/routes.md +9 -31
- package/docs/examples/index.md +46 -29
- package/docs/examples/interceptors/app.md +20 -139
- package/docs/examples/interceptors/dependencies/august/0.20.1/io/contracts.md +178 -0
- package/docs/examples/interceptors/index.md +6 -5
- package/docs/examples/interceptors/interceptors.md +24 -148
- package/docs/examples/interceptors/logging.md +13 -76
- package/docs/examples/interceptors/main.md +10 -65
- package/docs/examples/json-benchmark/data.md +4 -20
- package/docs/examples/json-benchmark/dependencies/packages/@git/url_2d3c37c690c0fa115be1/0.0.0-git.a39fc582565d4fca40be4f75fa304d71adc69301/contracts.md +67 -0
- package/docs/examples/json-benchmark/index.md +7 -5
- package/docs/examples/json-benchmark/main-yaml.md +20 -0
- package/docs/examples/json-benchmark/main.md +10 -49
- package/docs/examples/new-syntax/console.md +10 -52
- package/docs/examples/new-syntax/dependencies/august/0.20.1/io/contracts.md +178 -0
- package/docs/examples/new-syntax/greeter.md +13 -71
- package/docs/examples/new-syntax/index.md +6 -5
- package/docs/examples/new-syntax/logger.md +7 -43
- package/docs/examples/new-syntax/main.md +10 -53
- package/docs/examples/new-syntax/math.md +6 -24
- package/docs/examples/oidc-login/client/contracts.md +10 -61
- package/docs/examples/oidc-login/client/endpoints.md +32 -154
- package/docs/examples/oidc-login/client/export.md +5 -23
- package/docs/examples/oidc-login/client/login.md +234 -344
- package/docs/examples/oidc-login/client/logout.md +38 -143
- package/docs/examples/oidc-login/client/protocol.md +183 -304
- package/docs/examples/oidc-login/client/session.md +41 -149
- package/docs/examples/oidc-login/client/views.md +13 -58
- package/docs/examples/oidc-login/common/export.md +6 -26
- package/docs/examples/oidc-login/common/headers.md +42 -72
- package/docs/examples/oidc-login/common/keys.md +40 -164
- package/docs/examples/oidc-login/common/settings.md +24 -40
- package/docs/examples/oidc-login/common/views.md +7 -28
- package/docs/examples/oidc-login/dependencies/packages/@git/url_0eb7c89453c87681ed15/0.0.0-git.a39fc582565d4fca40be4f75fa304d71adc69301/store.md +205 -0
- package/docs/examples/oidc-login/dependencies/packages/@git/url_2d3c37c690c0fa115be1/0.0.0-git.a39fc582565d4fca40be4f75fa304d71adc69301/contracts.md +67 -0
- package/docs/examples/oidc-login/dependencies/packages/@git/url_897efafd565158fc4908/0.0.0-git.a39fc582565d4fca40be4f75fa304d71adc69301/contracts.md +267 -0
- package/docs/examples/oidc-login/dependencies/packages/@git/url_9ef654c66d34ab8f5527/0.0.0-git.b14a0f9aa41f1ce58bd51133bcdc424033e40d40/contracts.md +415 -0
- package/docs/examples/oidc-login/dependencies/packages/@git/url_9ef654c66d34ab8f5527/0.0.0-git.b14a0f9aa41f1ce58bd51133bcdc424033e40d40/jose.md +271 -0
- package/docs/examples/oidc-login/dependencies/packages/@git/url_c092cd151499c4e1d8a1/0.0.0-git.a39fc582565d4fca40be4f75fa304d71adc69301/contracts.md +89 -0
- package/docs/examples/oidc-login/index.md +8 -7
- package/docs/examples/oidc-login/main-yaml.md +7 -0
- package/docs/examples/oidc-login/main.md +26 -153
- package/docs/examples/oidc-login/provider/authorization.md +232 -275
- package/docs/examples/oidc-login/provider/contracts.md +24 -146
- package/docs/examples/oidc-login/provider/credentials.md +24 -57
- package/docs/examples/oidc-login/provider/discovery.md +48 -122
- package/docs/examples/oidc-login/provider/export.md +11 -37
- package/docs/examples/oidc-login/provider/token.md +159 -237
- package/docs/examples/oidc-login/provider/userinfo.md +47 -102
- package/docs/examples/oidc-login/provider/views.md +13 -53
- package/docs/examples/ownership/counter.md +17 -63
- package/docs/examples/ownership/index.md +6 -5
- package/docs/examples/ownership/main.md +7 -30
- package/docs/examples/ownership-transfer/dependencies/august/0.20.1/io/contracts.md +178 -0
- package/docs/examples/ownership-transfer/index.md +6 -5
- package/docs/examples/ownership-transfer/main.md +10 -52
- package/docs/examples/ownership-transfer/resource.md +15 -67
- package/docs/examples/packages-app/dependencies/packages/@example/aug-math/0.1.0/arithmetic.md +12 -50
- package/docs/examples/packages-app/index.md +7 -6
- package/docs/examples/packages-app/main.md +7 -25
- package/docs/examples/packages-math/aug-package-json.md +1 -1
- package/docs/examples/packages-math/index.md +7 -6
- package/docs/examples/packages-math/src/arithmetic.md +12 -50
- package/docs/examples/packages-math/src/export.md +4 -12
- package/docs/examples/startup-benchmark/index.md +6 -5
- package/docs/examples/startup-benchmark/main.md +5 -17
- package/docs/examples/visibility/counter.md +19 -67
- package/docs/examples/visibility/index.md +6 -5
- package/docs/examples/visibility/main.md +7 -31
- package/docs/examples/weather-api/forecasts.md +168 -0
- package/docs/examples/weather-api/index.md +42 -0
- package/docs/examples/weather-api/main-yaml.md +21 -0
- package/docs/examples/weather-api/main.md +65 -0
- package/docs/examples.json +10 -0
- package/docs/getting-started.md +105 -2
- package/docs/grammar.md +6 -4
- package/docs/guides/change-a-module.md +64 -0
- package/docs/guides/index.md +27 -0
- package/docs/index.md +51 -29
- package/docs/language-conformance.md +1 -1
- package/docs/language-constructs.md +22 -22
- package/docs/language-design-audit.md +1 -1
- package/docs/learn/data-and-errors.md +77 -0
- package/docs/learn/index.md +30 -0
- package/docs/learn/modules-and-dependencies.md +73 -0
- package/docs/learn/state-and-tests.md +71 -0
- package/docs/learn/values-and-functions.md +62 -0
- package/docs/lesson-failures.json +8 -0
- package/docs/maintaining-docs.md +16 -4
- package/docs/packages.md +72 -126
- package/docs/performance.md +13 -41
- package/docs/production-readiness.md +5 -4
- package/docs/public/downloads/approved-design.zip +0 -0
- package/docs/public/downloads/benchmark.zip +0 -0
- package/docs/public/downloads/cli-args.zip +0 -0
- package/docs/public/downloads/collections-benchmark.zip +0 -0
- package/docs/public/downloads/collections.zip +0 -0
- package/docs/public/downloads/cpu-benchmark.zip +0 -0
- package/docs/public/downloads/developer-workflow.zip +0 -0
- package/docs/public/downloads/drop.zip +0 -0
- package/docs/public/downloads/errors.zip +0 -0
- package/docs/public/downloads/ffi.zip +0 -0
- package/docs/public/downloads/generic-di.zip +0 -0
- package/docs/public/downloads/generics.zip +0 -0
- package/docs/public/downloads/hello.zip +0 -0
- package/docs/public/downloads/http-benchmark.zip +0 -0
- package/docs/public/downloads/interceptors.zip +0 -0
- package/docs/public/downloads/json-benchmark.zip +0 -0
- package/docs/public/downloads/new-syntax.zip +0 -0
- package/docs/public/downloads/oidc-login.zip +0 -0
- package/docs/public/downloads/ownership-transfer.zip +0 -0
- package/docs/public/downloads/ownership.zip +0 -0
- package/docs/public/downloads/packages-app.zip +0 -0
- package/docs/public/downloads/packages-math.zip +0 -0
- package/docs/public/downloads/startup-benchmark.zip +0 -0
- package/docs/public/downloads/visibility.zip +0 -0
- package/docs/public/downloads/weather-api.zip +0 -0
- package/docs/reference.md +23 -17
- package/docs/releasing.md +29 -15
- package/docs/research/code-to-natural-language.md +108 -0
- package/docs/research/ecosystem-workflow.md +41 -0
- package/docs/research/wiki-editorial-design.md +71 -0
- package/docs/roadmap.md +1 -1
- package/docs/specifications.md +27 -11
- package/docs/testing.md +19 -5
- package/docs/tooling.md +25 -12
- package/docs/weather-api.md +65 -0
- package/docs/web.md +50 -18
- package/docs/writing-docs.md +49 -0
- package/examples/{hello/.aug-spec/august/0.19.0 → approved-design/.aug-spec/august/0.20.1}/io/contracts.aug +4 -3
- package/examples/approved-design/.aug-spec/august/0.20.1/io/contracts.aug.md +82 -0
- package/examples/approved-design/.aug-spec/manifest.json +3 -3
- package/examples/approved-design/counters.aug +5 -4
- package/examples/approved-design/counters.aug.md +25 -132
- package/examples/approved-design/domain/app.aug +2 -1
- package/examples/approved-design/domain/app.aug.md +10 -80
- package/examples/approved-design/domain/export.aug +1 -0
- package/examples/approved-design/domain/export.aug.md +4 -20
- package/examples/approved-design/domain/models.aug +1 -0
- package/examples/approved-design/domain/models.aug.md +3 -25
- package/examples/approved-design/domain/numbers.aug +3 -2
- package/examples/approved-design/domain/numbers.aug.md +19 -116
- package/examples/approved-design/main.aug +1 -0
- package/examples/approved-design/main.aug.md +12 -95
- package/examples/benchmark/.aug-spec/manifest.json +1 -1
- package/examples/benchmark/main.aug +1 -0
- package/examples/benchmark/main.aug.md +6 -36
- package/examples/cli-args/.aug-spec/manifest.json +1 -1
- package/examples/cli-args/main.aug +1 -0
- package/examples/cli-args/main.aug.md +5 -32
- package/examples/collections/.aug-spec/manifest.json +1 -1
- package/examples/collections/main.aug +1 -0
- package/examples/collections/main.aug.md +6 -36
- package/examples/{generic-di/.aug-spec/august/0.19.0 → developer-workflow/.aug-spec/august/0.20.1}/io/contracts.aug +4 -3
- package/examples/developer-workflow/.aug-spec/august/0.20.1/io/contracts.aug.md +82 -0
- package/examples/developer-workflow/.aug-spec/manifest.json +3 -3
- package/examples/developer-workflow/calculator.aug +3 -2
- package/examples/developer-workflow/calculator.aug.md +28 -189
- package/examples/developer-workflow/logging/console.aug +2 -1
- package/examples/developer-workflow/logging/console.aug.md +7 -57
- package/examples/developer-workflow/logging/export.aug +1 -0
- package/examples/developer-workflow/logging/export.aug.md +3 -15
- package/examples/developer-workflow/logging/logger.aug +1 -0
- package/examples/developer-workflow/logging/logger.aug.md +6 -46
- package/examples/developer-workflow/main.aug +1 -0
- package/examples/developer-workflow/main.aug.md +10 -68
- package/examples/drop/.aug-spec/manifest.json +1 -1
- package/examples/drop/main.aug +1 -0
- package/examples/drop/main.aug.md +6 -28
- package/examples/drop/resource.aug +1 -0
- package/examples/drop/resource.aug.md +6 -34
- package/examples/errors/.aug-spec/manifest.json +1 -1
- package/examples/errors/errors.aug +2 -1
- package/examples/errors/errors.aug.md +4 -27
- package/examples/errors/main.aug +1 -0
- package/examples/errors/main.aug.md +6 -29
- package/examples/ffi/.aug-spec/manifest.json +1 -1
- package/examples/ffi/main.aug +1 -0
- package/examples/ffi/main.aug.md +5 -21
- package/examples/ffi/native.aug +2 -1
- package/examples/ffi/native.aug.md +5 -35
- package/examples/{approved-design/.aug-spec/august/0.19.0 → generic-di/.aug-spec/august/0.20.1}/io/contracts.aug +4 -3
- package/examples/generic-di/.aug-spec/august/0.20.1/io/contracts.aug.md +82 -0
- package/examples/generic-di/.aug-spec/manifest.json +3 -3
- package/examples/generic-di/main.aug +1 -0
- package/examples/generic-di/main.aug.md +8 -40
- package/examples/generic-di/types.aug +3 -2
- package/examples/generic-di/types.aug.md +18 -97
- package/examples/generics/.aug-spec/manifest.json +1 -1
- package/examples/generics/main.aug +1 -0
- package/examples/generics/main.aug.md +9 -48
- package/examples/generics/types.aug +4 -3
- package/examples/generics/types.aug.md +18 -104
- package/examples/{developer-workflow/.aug-spec/august/0.19.0 → hello/.aug-spec/august/0.20.1}/io/contracts.aug +4 -3
- package/examples/hello/.aug-spec/august/0.20.1/io/contracts.aug.md +82 -0
- package/examples/hello/.aug-spec/manifest.json +3 -3
- package/examples/hello/app/export.aug +1 -0
- package/examples/hello/app/export.aug.md +3 -13
- package/examples/hello/app/greeter.aug +2 -1
- package/examples/hello/app/greeter.aug.md +10 -94
- package/examples/hello/logging/console.aug +2 -1
- package/examples/hello/logging/console.aug.md +7 -55
- package/examples/hello/logging/export.aug +1 -0
- package/examples/hello/logging/export.aug.md +3 -15
- package/examples/hello/logging/logger.aug +1 -0
- package/examples/hello/logging/logger.aug.md +6 -48
- package/examples/hello/main.aug +1 -0
- package/examples/hello/main.aug.md +8 -40
- package/examples/interceptors/.aug-spec/august/0.20.1/io/contracts.aug +37 -0
- package/examples/interceptors/.aug-spec/august/0.20.1/io/contracts.aug.md +82 -0
- package/examples/interceptors/.aug-spec/manifest.json +3 -3
- package/examples/interceptors/app.aug +3 -2
- package/examples/interceptors/app.aug.md +15 -141
- package/examples/interceptors/interceptors.aug +3 -2
- package/examples/interceptors/interceptors.aug.md +19 -152
- package/examples/interceptors/logging.aug +2 -1
- package/examples/interceptors/logging.aug.md +10 -79
- package/examples/interceptors/main.aug +1 -0
- package/examples/interceptors/main.aug.md +9 -66
- package/examples/new-syntax/.aug-spec/august/0.20.1/io/contracts.aug +37 -0
- package/examples/new-syntax/.aug-spec/august/0.20.1/io/contracts.aug.md +82 -0
- package/examples/new-syntax/.aug-spec/manifest.json +3 -3
- package/examples/new-syntax/console.aug +2 -1
- package/examples/new-syntax/console.aug.md +7 -53
- package/examples/new-syntax/greeter.aug +2 -1
- package/examples/new-syntax/greeter.aug.md +10 -74
- package/examples/new-syntax/logger.aug +1 -0
- package/examples/new-syntax/logger.aug.md +6 -46
- package/examples/new-syntax/main.aug +1 -0
- package/examples/new-syntax/main.aug.md +9 -54
- package/examples/new-syntax/math.aug +2 -1
- package/examples/new-syntax/math.aug.md +3 -24
- package/examples/oidc-login/.aug-spec/manifest.json +13 -13
- package/examples/oidc-login/.aug-spec/packages/@git/url_0eb7c89453c87681ed15/0.0.0-git.a39fc582565d4fca40be4f75fa304d71adc69301/store.aug.md +71 -0
- package/examples/oidc-login/.aug-spec/packages/@git/url_2d3c37c690c0fa115be1/0.0.0-git.a39fc582565d4fca40be4f75fa304d71adc69301/contracts.aug.md +17 -0
- package/examples/oidc-login/.aug-spec/packages/@git/url_897efafd565158fc4908/0.0.0-git.a39fc582565d4fca40be4f75fa304d71adc69301/contracts.aug.md +125 -0
- package/examples/oidc-login/.aug-spec/{august/0.19.0/crypto → packages/@git/url_9ef654c66d34ab8f5527/0.0.0-git.b14a0f9aa41f1ce58bd51133bcdc424033e40d40}/contracts.aug +12 -11
- package/examples/oidc-login/.aug-spec/packages/@git/url_9ef654c66d34ab8f5527/0.0.0-git.b14a0f9aa41f1ce58bd51133bcdc424033e40d40/contracts.aug.md +244 -0
- package/examples/oidc-login/.aug-spec/{august/0.19.0/crypto → packages/@git/url_9ef654c66d34ab8f5527/0.0.0-git.b14a0f9aa41f1ce58bd51133bcdc424033e40d40}/jose.aug +6 -5
- package/examples/oidc-login/.aug-spec/packages/@git/url_9ef654c66d34ab8f5527/0.0.0-git.b14a0f9aa41f1ce58bd51133bcdc424033e40d40/jose.aug.md +71 -0
- package/examples/oidc-login/.aug-spec/packages/@git/url_c092cd151499c4e1d8a1/0.0.0-git.a39fc582565d4fca40be4f75fa304d71adc69301/contracts.aug.md +30 -0
- package/examples/oidc-login/aug.lock.json +100 -0
- package/examples/oidc-login/client/contracts.aug +1 -0
- package/examples/oidc-login/client/contracts.aug.md +9 -66
- package/examples/oidc-login/client/endpoints.aug +6 -5
- package/examples/oidc-login/client/endpoints.aug.md +13 -145
- package/examples/oidc-login/client/export.aug +1 -0
- package/examples/oidc-login/client/export.aug.md +4 -24
- package/examples/oidc-login/client/login.aug +7 -6
- package/examples/oidc-login/client/login.aug.md +25 -303
- package/examples/oidc-login/client/logout.aug +5 -4
- package/examples/oidc-login/client/logout.aug.md +13 -135
- package/examples/oidc-login/client/protocol.aug +7 -6
- package/examples/oidc-login/client/protocol.aug.md +36 -283
- package/examples/oidc-login/client/session.aug +5 -4
- package/examples/oidc-login/client/session.aug.md +12 -139
- package/examples/oidc-login/client/views.aug +3 -2
- package/examples/oidc-login/client/views.aug.md +8 -57
- package/examples/oidc-login/common/export.aug +1 -0
- package/examples/oidc-login/common/export.aug.md +5 -27
- package/examples/oidc-login/common/headers.aug +4 -3
- package/examples/oidc-login/common/headers.aug.md +9 -67
- package/examples/oidc-login/common/keys.aug +6 -5
- package/examples/oidc-login/common/keys.aug.md +29 -165
- package/examples/oidc-login/common/settings.aug +2 -1
- package/examples/oidc-login/common/settings.aug.md +5 -39
- package/examples/oidc-login/common/views.aug +2 -1
- package/examples/oidc-login/common/views.aug.md +4 -28
- package/examples/oidc-login/main.aug +6 -16
- package/examples/oidc-login/main.aug.md +17 -146
- package/examples/oidc-login/main.yaml +7 -0
- package/examples/oidc-login/provider/authorization.aug +7 -6
- package/examples/oidc-login/provider/authorization.aug.md +23 -238
- package/examples/oidc-login/provider/contracts.aug +1 -0
- package/examples/oidc-login/provider/contracts.aug.md +23 -158
- package/examples/oidc-login/provider/credentials.aug +3 -2
- package/examples/oidc-login/provider/credentials.aug.md +9 -53
- package/examples/oidc-login/provider/discovery.aug +4 -3
- package/examples/oidc-login/provider/discovery.aug.md +11 -118
- package/examples/oidc-login/provider/export.aug +1 -0
- package/examples/oidc-login/provider/export.aug.md +6 -34
- package/examples/oidc-login/provider/token.aug +6 -5
- package/examples/oidc-login/provider/token.aug.md +16 -204
- package/examples/oidc-login/provider/userinfo.aug +4 -3
- package/examples/oidc-login/provider/userinfo.aug.md +10 -92
- package/examples/oidc-login/provider/views.aug +3 -2
- package/examples/oidc-login/provider/views.aug.md +8 -52
- package/examples/ownership/.aug-spec/manifest.json +1 -1
- package/examples/ownership/counter.aug +3 -2
- package/examples/ownership/counter.aug.md +12 -66
- package/examples/ownership/main.aug +1 -0
- package/examples/ownership/main.aug.md +6 -31
- package/examples/ownership-transfer/.aug-spec/august/0.20.1/io/contracts.aug +37 -0
- package/examples/ownership-transfer/.aug-spec/august/0.20.1/io/contracts.aug.md +82 -0
- package/examples/ownership-transfer/.aug-spec/manifest.json +3 -3
- package/examples/ownership-transfer/main.aug +1 -0
- package/examples/ownership-transfer/main.aug.md +9 -53
- package/examples/ownership-transfer/resource.aug +2 -1
- package/examples/ownership-transfer/resource.aug.md +12 -71
- package/examples/packages/app/.aug-spec/manifest.json +1 -1
- package/examples/packages/app/.aug-spec/packages/@example/aug-math/0.1.0/arithmetic.aug +2 -1
- package/examples/packages/app/.aug-spec/packages/@example/aug-math/0.1.0/arithmetic.aug.md +9 -51
- package/examples/packages/app/main.aug +1 -0
- package/examples/packages/app/main.aug.md +6 -26
- package/examples/packages/math/.aug-spec/manifest.json +1 -1
- package/examples/packages/math/aug-package.json +1 -1
- package/examples/packages/math/src/arithmetic.aug +2 -1
- package/examples/packages/math/src/arithmetic.aug.md +9 -51
- package/examples/packages/math/src/export.aug +1 -0
- package/examples/packages/math/src/export.aug.md +3 -13
- package/examples/visibility/.aug-spec/manifest.json +1 -1
- package/examples/visibility/counter.aug +4 -3
- package/examples/visibility/counter.aug.md +12 -68
- package/examples/visibility/main.aug +1 -0
- package/examples/visibility/main.aug.md +6 -32
- package/examples/weather-api/.aug-spec/manifest.json +8 -0
- package/examples/weather-api/AGENTS.md +17 -0
- package/examples/weather-api/README.md +19 -0
- package/examples/weather-api/forecasts.aug +47 -0
- package/examples/weather-api/forecasts.aug.md +32 -0
- package/examples/weather-api/main.aug +4 -0
- package/examples/weather-api/main.aug.md +15 -0
- package/examples/weather-api/main.yaml +5 -0
- package/examples/weather-api/weather.http +5 -0
- package/package.json +3 -4
- package/scripts/bootstrap-native.mjs +181 -106
- package/scripts/native-setup.mjs +98 -0
- package/scripts/native-toolchain.mjs +28 -0
- package/src/builtins.js +1 -1
- package/src/checker.js +259 -72
- package/src/cli.js +114 -20
- package/src/codegen.js +1 -1
- package/src/config.js +1 -1
- package/src/contracts.js +10 -0
- package/src/editor.js +71 -16
- package/src/fixes.js +90 -2
- package/src/formatter.js +19 -8
- package/src/git-packages.js +110 -0
- package/src/help.js +24 -23
- package/src/http-contracts.js +14 -0
- package/src/http-policies.js +5 -5
- package/src/libraries.js +1 -1
- package/src/lsp.js +14 -5
- package/src/native.js +19 -13
- package/src/navigation.js +4 -2
- package/src/openapi.js +5 -4
- package/src/package-locking.js +45 -0
- package/src/package-manager.js +251 -67
- package/src/parser.js +13 -5
- package/src/policies.js +3 -3
- package/src/project-init.js +78 -4
- package/src/project.js +9 -7
- package/src/semantic.js +46 -2
- package/src/snippets.js +59 -0
- package/src/spec-hints.js +56 -0
- package/src/spec-tree.js +241 -0
- package/src/spec.js +626 -303
- package/docs/examples/approved-design/dependencies/august/0.19.0/io/contracts.md +0 -395
- package/docs/examples/developer-workflow/dependencies/august/0.19.0/io/contracts.md +0 -395
- package/docs/examples/generic-di/dependencies/august/0.19.0/io/contracts.md +0 -395
- package/docs/examples/hello/dependencies/august/0.19.0/io/contracts.md +0 -395
- package/docs/examples/interceptors/dependencies/august/0.19.0/io/contracts.md +0 -395
- package/docs/examples/json-benchmark/dependencies/august/0.19.0/json/contracts.md +0 -103
- package/docs/examples/new-syntax/dependencies/august/0.19.0/io/contracts.md +0 -395
- package/docs/examples/oidc-login/dependencies/august/0.19.0/crypto/contracts.md +0 -925
- package/docs/examples/oidc-login/dependencies/august/0.19.0/crypto/jose.md +0 -434
- package/docs/examples/oidc-login/dependencies/august/0.19.0/json/contracts.md +0 -103
- package/docs/examples/oidc-login/dependencies/august/0.19.0/memory/store.md +0 -374
- package/docs/examples/oidc-login/dependencies/august/0.19.0/time/contracts.md +0 -150
- package/docs/examples/oidc-login/dependencies/august/0.19.0/web/contracts.md +0 -532
- package/docs/examples/ownership-transfer/dependencies/august/0.19.0/io/contracts.md +0 -395
- package/examples/approved-design/.aug-spec/august/0.19.0/io/contracts.aug.md +0 -316
- package/examples/developer-workflow/.aug-spec/august/0.19.0/io/contracts.aug.md +0 -316
- package/examples/generic-di/.aug-spec/august/0.19.0/io/contracts.aug.md +0 -316
- package/examples/hello/.aug-spec/august/0.19.0/io/contracts.aug.md +0 -316
- package/examples/interceptors/.aug-spec/august/0.19.0/io/contracts.aug +0 -36
- package/examples/interceptors/.aug-spec/august/0.19.0/io/contracts.aug.md +0 -316
- package/examples/new-syntax/.aug-spec/august/0.19.0/io/contracts.aug +0 -36
- package/examples/new-syntax/.aug-spec/august/0.19.0/io/contracts.aug.md +0 -316
- package/examples/oidc-login/.aug-spec/august/0.19.0/crypto/contracts.aug.md +0 -791
- package/examples/oidc-login/.aug-spec/august/0.19.0/crypto/jose.aug.md +0 -266
- package/examples/oidc-login/.aug-spec/august/0.19.0/json/contracts.aug.md +0 -55
- package/examples/oidc-login/.aug-spec/august/0.19.0/memory/store.aug.md +0 -250
- package/examples/oidc-login/.aug-spec/august/0.19.0/time/contracts.aug.md +0 -96
- package/examples/oidc-login/.aug-spec/august/0.19.0/web/contracts.aug.md +0 -420
- package/examples/ownership-transfer/.aug-spec/august/0.19.0/io/contracts.aug +0 -36
- package/examples/ownership-transfer/.aug-spec/august/0.19.0/io/contracts.aug.md +0 -316
- /package/examples/oidc-login/.aug-spec/{august/0.19.0/memory → packages/@git/url_0eb7c89453c87681ed15/0.0.0-git.a39fc582565d4fca40be4f75fa304d71adc69301}/store.aug +0 -0
- /package/examples/oidc-login/.aug-spec/{august/0.19.0/json → packages/@git/url_2d3c37c690c0fa115be1/0.0.0-git.a39fc582565d4fca40be4f75fa304d71adc69301}/contracts.aug +0 -0
- /package/examples/oidc-login/.aug-spec/{august/0.19.0/web → packages/@git/url_897efafd565158fc4908/0.0.0-git.a39fc582565d4fca40be4f75fa304d71adc69301}/contracts.aug +0 -0
- /package/examples/oidc-login/.aug-spec/{august/0.19.0/time → packages/@git/url_c092cd151499c4e1d8a1/0.0.0-git.a39fc582565d4fca40be4f75fa304d71adc69301}/contracts.aug +0 -0
package/src/spec.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import { callableResult, callableErrors } from "./contracts.js";
|
|
1
2
|
import { createHash } from 'node:crypto';
|
|
2
3
|
import { existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
4
|
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
4
5
|
import { fieldsOf, typeName } from "./ast.js";
|
|
5
|
-
import { builtinFunctions, builtinProperties, collectionOperations, operationType } from "./builtins.js";
|
|
6
|
+
import { builtinFunctions, builtinProperties, builtinTypes, collectionOperations, operationType } from "./builtins.js";
|
|
6
7
|
import { callableDocumentation } from "./documentation.js";
|
|
7
8
|
import { javadocBefore } from "./javadoc.js";
|
|
8
9
|
import { libraryChild, libraryRelative } from "./libraries.js";
|
|
9
10
|
import { compilerVersion } from "./package-manager.js";
|
|
10
11
|
import { tyName } from "./types.js";
|
|
12
|
+
import { action, attempt, branch, choice, coordinate, flow, loop, paragraph, renderSpecTree, scope, section, sequence, step } from "./spec-tree.js";
|
|
13
|
+
import { specHint } from "./spec-hints.js";
|
|
11
14
|
const generated = '<!-- Generated by aug spec. Edit the August source, then regenerate. -->';
|
|
12
15
|
const copied = '// Generated by aug spec. This is a copy of the installed dependency source.\n';
|
|
13
16
|
const code = (value) => { const text = value.replaceAll('\n', '\\n'); const fence = '`'.repeat(1 + Math.max(0, ...(text.match(/`+/g) ?? []).map(part => part.length))); return fence + (text.startsWith('`') ? ' ' : '') + text + (text.endsWith('`') ? ' ' : '') + fence; };
|
|
@@ -16,13 +19,15 @@ const anchor = (name) => 'symbol-' + name;
|
|
|
16
19
|
const hash = (text) => createHash('sha256').update(text).digest('hex');
|
|
17
20
|
const compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
18
21
|
const unreachable = (node) => { throw new Error(`No specification renderer for ${node.kind}`); };
|
|
19
|
-
|
|
22
|
+
const plain = (text) => text.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replaceAll('`', '');
|
|
23
|
+
/** Plan prose, managed source pointers, and versioned dependency copies without writes or execution. */
|
|
20
24
|
export function generateSpecs(checked, options = {}) {
|
|
21
25
|
if (checked.diagnostics.some(issue => issue.severity !== 'warning'))
|
|
22
26
|
throw new Error('Fix compiler errors before generating specifications');
|
|
23
27
|
const project = checked.project;
|
|
24
28
|
const owned = options.files ?? [...project.files.values()].filter(file => !file.builtin && !file.package);
|
|
25
29
|
const own = new Set(owned.map(file => file.path));
|
|
30
|
+
const hints = owned.map(specHint), offsets = new Map(hints.map(hint => [hint.file.path, hint.lineOffset]));
|
|
26
31
|
const docs = new Map(), sources = new Map();
|
|
27
32
|
for (const file of project.files.values()) {
|
|
28
33
|
if (own.has(file.path)) {
|
|
@@ -38,13 +43,13 @@ export function generateSpecs(checked, options = {}) {
|
|
|
38
43
|
sources.set(file.path, source);
|
|
39
44
|
}
|
|
40
45
|
const queue = [...owned].sort((a, b) => compare(a.path, b.path));
|
|
41
|
-
const seen = new Set(), outputs =
|
|
46
|
+
const seen = new Set(), outputs = hints.map(hint => ({ path: hint.file.path, text: hint.text, source: hint.file.path, kind: 'source-hint' }));
|
|
42
47
|
for (let index = 0; index < queue.length; index++) {
|
|
43
48
|
const file = queue[index];
|
|
44
49
|
if (seen.has(file.path))
|
|
45
50
|
continue;
|
|
46
51
|
seen.add(file.path);
|
|
47
|
-
const writer = new SpecWriter(checked, file, docs, sources, own, path => {
|
|
52
|
+
const writer = new SpecWriter(checked, file, docs, sources, own, offsets, path => {
|
|
48
53
|
const dependency = project.files.get(path);
|
|
49
54
|
if (dependency && !seen.has(path))
|
|
50
55
|
queue.push(dependency);
|
|
@@ -61,17 +66,19 @@ class SpecWriter {
|
|
|
61
66
|
docs;
|
|
62
67
|
sources;
|
|
63
68
|
own;
|
|
69
|
+
offsets;
|
|
64
70
|
enqueue;
|
|
65
71
|
used = new Map();
|
|
66
72
|
builtins = new Map();
|
|
67
73
|
properties = new Map();
|
|
68
74
|
locals = new Map();
|
|
69
|
-
constructor(checked, file, docs, sources, own, enqueue) {
|
|
75
|
+
constructor(checked, file, docs, sources, own, offsets, enqueue) {
|
|
70
76
|
this.checked = checked;
|
|
71
77
|
this.file = file;
|
|
72
78
|
this.docs = docs;
|
|
73
79
|
this.sources = sources;
|
|
74
80
|
this.own = own;
|
|
81
|
+
this.offsets = offsets;
|
|
75
82
|
this.enqueue = enqueue;
|
|
76
83
|
}
|
|
77
84
|
definition(name) { return this.checked.project.scopes.get(this.file.path)?.get(name); }
|
|
@@ -99,95 +106,166 @@ class SpecWriter {
|
|
|
99
106
|
return `[${code(label)}](${url(relative(dirname(this.docs.get(this.file.path)), this.docs.get(def.file)))}#${encodeURIComponent(anchor(name))})`;
|
|
100
107
|
}
|
|
101
108
|
source(span) {
|
|
102
|
-
return `[source](${url(relative(dirname(this.docs.get(this.file.path)), this.sources.get(span.file)))}#L${span.line + (this.own.has(span.file) ? 0 : 1)})`;
|
|
109
|
+
return `[source](${url(relative(dirname(this.docs.get(this.file.path)), this.sources.get(span.file)))}#L${span.line + (this.own.has(span.file) ? this.offsets.get(span.file) ?? 0 : 1)})`;
|
|
103
110
|
}
|
|
104
|
-
heading(name, span, level
|
|
105
|
-
return
|
|
111
|
+
heading(name, span, level, children, role) {
|
|
112
|
+
return section(code(name) + (role ? ' · ' + role : ''), level, children, anchor(name), this.source(span));
|
|
106
113
|
}
|
|
107
114
|
type(type, file = this.file.path) {
|
|
108
|
-
const
|
|
115
|
+
const enclosing = this.checked.project.files.get(file)?.items.filter(item => 'typeParams' in item && item.span.start <= type.span.start && item.span.end >= type.span.end) ?? [];
|
|
116
|
+
const parameter = enclosing.some(item => 'typeParams' in item && (item.typeParams.includes(type.name) || 'methods' in item && item.methods.some(method => method.span.start <= type.span.start && method.span.end >= type.span.end && method.typeParams.includes(type.name))));
|
|
117
|
+
const def = parameter ? undefined : this.checked.project.scopes.get(file)?.get(type.name);
|
|
109
118
|
this.use(def);
|
|
110
119
|
type.args.forEach(arg => this.type(arg, file));
|
|
111
120
|
return def ? this.link(def, def.name, typeName(type)) : code(typeName(type));
|
|
112
121
|
}
|
|
113
|
-
notes(
|
|
114
|
-
|
|
115
|
-
|
|
122
|
+
notes(doc) {
|
|
123
|
+
if (!doc)
|
|
124
|
+
return '';
|
|
125
|
+
const prose = doc.markdown.split(/(?:^|\n\n)\*\*(?:Returns|Parameters|Throws|Deprecated:|See also:)/)[0].trim();
|
|
126
|
+
const extra = (doc.tags ?? []).filter(tag => tag.name === 'deprecated' || tag.name === 'see').map(tag => tag.name === 'deprecated' ? `Deprecated: ${tag.value}` : `See also: ${tag.value}`);
|
|
127
|
+
return [prose, ...extra].filter(Boolean).join(' ');
|
|
116
128
|
}
|
|
117
|
-
generics(node) {
|
|
118
|
-
return node.typeParams.length ? '
|
|
129
|
+
generics(node, file = this.file.path) {
|
|
130
|
+
return node.typeParams.length ? 'The type parameters are ' + coordinate(node.typeParams.map(name => code(name) +
|
|
119
131
|
(node.typeVariance?.[name] ? ` (${node.typeVariance[name] === 'out' ? 'produces values' : 'accepts values'})` : '') +
|
|
120
|
-
(node.typeConstraints?.[name]?.length ? ` must satisfy ${node.typeConstraints[name].map(type => this.type(type))
|
|
132
|
+
(node.typeConstraints?.[name]?.length ? ` which must satisfy ${coordinate(node.typeConstraints[name].map(type => this.type(type, file)))}` : ''))) + '.' : '';
|
|
121
133
|
}
|
|
122
|
-
inputs(params, fields = false, file = this.file.path) {
|
|
134
|
+
inputs(params, fields = false, file = this.file.path, descriptions = new Map()) {
|
|
123
135
|
if (!params.length)
|
|
124
136
|
return '';
|
|
125
|
-
|
|
137
|
+
if (fields && params.length === 1 && params[0].injected) {
|
|
138
|
+
const param = params[0];
|
|
126
139
|
this.locals.set(param.name, param.type);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
140
|
+
return 'The ' + code(param.name) + ' dependency is injected as ' + this.type(param.type, file) + ' and stored ' + (param.mutable ? 'mutably' : 'read-only') +
|
|
141
|
+
(param.name.startsWith('_') ? ' and privately' : '') + (param.ownership === 'own' ? ' with ownership transferred' : param.ownership === 'borrow' ? ' with permission to mutate it' : '') +
|
|
142
|
+
(descriptions.has(param.name) ? ' (' + descriptions.get(param.name).replace(/[.!?]$/, '') + ')' : '') + '.';
|
|
143
|
+
}
|
|
144
|
+
const describe = (group) => {
|
|
145
|
+
const param = group[0];
|
|
146
|
+
group.forEach(param => this.locals.set(param.name, param.type));
|
|
147
|
+
const name = coordinate(group.map(param => code(param.label ?? param.name))), type = this.type(param.type, file);
|
|
148
|
+
const simple = { int: group.length === 1 ? 'an integer' : 'integers', float: group.length === 1 ? 'a number' : 'numbers', string: group.length === 1 ? 'a string' : 'strings', bool: group.length === 1 ? 'a boolean' : 'booleans' };
|
|
149
|
+
let sentence = param.injected ? coordinate(group.map(param => code(param.name))) + ' (' + type + ')' : name + ' as ' + (simple[param.type.name] && !param.type.optional ? simple[param.type.name] : type);
|
|
150
|
+
if (param.source)
|
|
151
|
+
sentence += ` from the HTTP ${param.source.kind}${param.source.name ? ' ' + code(param.source.name) : ''}`;
|
|
134
152
|
if (param.ownership === 'own')
|
|
135
|
-
|
|
153
|
+
sentence += ' with ownership transferred';
|
|
136
154
|
else if (param.ownership === 'borrow')
|
|
137
|
-
|
|
155
|
+
sentence += ' with permission to mutate it during the call';
|
|
138
156
|
if (fields)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
157
|
+
sentence += `, kept ${param.mutable ? 'mutable' : 'read-only'}${param.name.startsWith('_') ? ' and private' : ''}${param.label && param.label !== param.name ? ' as ' + code(param.name) : ''}`;
|
|
158
|
+
const description = descriptions.get(param.name);
|
|
159
|
+
if (description)
|
|
160
|
+
sentence += ' (' + description.replace(/[.!?]$/, '').replace(/^The /, 'the ') + ')';
|
|
161
|
+
return sentence;
|
|
162
|
+
};
|
|
163
|
+
const groups = (injected) => this.parameterGroups(params.filter(param => param.injected === injected), fields, descriptions).map(describe);
|
|
164
|
+
const supplied = groups(false), injected = groups(true);
|
|
165
|
+
const queries = params.filter(param => !param.injected && param.source?.kind === 'query');
|
|
166
|
+
const onlyQueries = queries.length && queries.length === params.filter(param => !param.injected).length;
|
|
167
|
+
const aliases = queries.filter(param => param.source?.name && param.source.name !== param.name);
|
|
168
|
+
const typedQueries = queries.filter(param => param.type.name !== 'string');
|
|
169
|
+
const suppliedText = onlyQueries ? 'It reads ' + coordinate(queries.map(param => code(param.source?.name ?? param.label ?? param.name))) + ' from the HTTP query.' +
|
|
170
|
+
(aliases.length ? ' ' + coordinate(aliases.map(param => code(param.source.name) + ' is called ' + code(param.name) + ' here')) + '.' : '') +
|
|
171
|
+
(typedQueries.length ? ' It parses ' + coordinate(typedQueries.map(param => code(param.name) + ' as ' + this.type(param.type, file))) + '.' : '') :
|
|
172
|
+
supplied.length ? 'It takes ' + coordinate(supplied) + '.' : '';
|
|
173
|
+
return [suppliedText, injected.length ? 'It gets ' + coordinate(injected) + ' from dependency injection.' : '',
|
|
174
|
+
params.some(param => param.type.optional) ? 'Omitted optional inputs are null.' : ''].filter(Boolean).join(' ');
|
|
175
|
+
}
|
|
176
|
+
parameterGroups(params, fields = false, descriptions = new Map()) {
|
|
177
|
+
const groups = [];
|
|
178
|
+
const key = (value) => JSON.stringify([typeName(value.type), value.source, value.ownership, fields && value.mutable, fields && value.name.startsWith('_')]);
|
|
179
|
+
const alias = (value) => fields && value.label && value.label !== value.name;
|
|
180
|
+
for (const param of params) {
|
|
181
|
+
const previous = groups.at(-1)?.[0];
|
|
182
|
+
if (previous && key(previous) === key(param) && !descriptions.has(param.name) && !descriptions.has(previous.name) && !alias(param) && !alias(previous))
|
|
183
|
+
groups.at(-1).push(param);
|
|
184
|
+
else
|
|
185
|
+
groups.push([param]);
|
|
186
|
+
}
|
|
187
|
+
return groups;
|
|
142
188
|
}
|
|
143
|
-
contract(method) {
|
|
189
|
+
contract(method, documentation) {
|
|
144
190
|
const effects = this.checked.effectContracts.get(method);
|
|
145
191
|
const layers = this.checked.interceptorPlans.get(method) ?? [];
|
|
146
|
-
const
|
|
192
|
+
for (const type of method.throws)
|
|
193
|
+
this.type(type, method.span.file);
|
|
194
|
+
const errors = callableErrors(this.checked, method);
|
|
195
|
+
for (const error of this.checked.callableContracts.get(method)?.errors ?? [])
|
|
196
|
+
this.use(error.def);
|
|
147
197
|
const changes = effects?.changes ?? method.changes ?? [];
|
|
148
198
|
const uses = [...(effects?.uses.values() ?? method.uses ?? [])];
|
|
149
|
-
|
|
150
|
-
|
|
199
|
+
const result = [];
|
|
200
|
+
const generics = this.generics(method), inputs = this.inputs(method.params, false, method.span.file, documentation?.parameters);
|
|
201
|
+
if (generics)
|
|
202
|
+
result.push(paragraph(generics));
|
|
203
|
+
if (inputs)
|
|
204
|
+
result.push(paragraph(inputs));
|
|
205
|
+
const facts = [];
|
|
206
|
+
const returnNote = documentation?.tags?.find(tag => tag.name === 'return' || tag.name === 'returns')?.value ??
|
|
207
|
+
documentation?.markdown.match(/\*\*Returns\*\* ([^\n]+)/)?.[1];
|
|
208
|
+
// Implementations explain their actual returns below. Contracts need the
|
|
209
|
+
// result type; repeating it before every return adds noise to the narrative.
|
|
210
|
+
const resultType = callableResult(this.checked, method);
|
|
211
|
+
const reference = (type) => ({ name: type.name, args: type.args.map(reference), nullable: type.nullable, optional: type.optional, span: method.returns.span });
|
|
212
|
+
const returns = reference(resultType);
|
|
213
|
+
this.use(resultType.def);
|
|
214
|
+
this.type(returns, method.span.file);
|
|
215
|
+
if (returns.name !== 'void' && (!method.body || returnNote || method.returnOwnership === 'own'))
|
|
216
|
+
facts.push(`It returns ${(method.returnOwnership === 'own' ? 'ownership of ' : '') + this.type(returns, method.span.file)}` +
|
|
217
|
+
(returnNote ? ` — ${returnNote.replace(/[.!?]$/, '')}` : '') + '.');
|
|
151
218
|
if (changes.length)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
219
|
+
facts.push('It may change ' + coordinate(changes.map(code)) + '.');
|
|
220
|
+
const capabilities = uses.map(effect => {
|
|
221
|
+
const def = 'capability' in effect ? effect.capability?.def : undefined;
|
|
222
|
+
const target = def ?? this.definition(effect.source);
|
|
223
|
+
const operation = target && 'methods' in target.node ? target.node.methods.find(method => method.name === effect.operation) : undefined;
|
|
224
|
+
this.use(target, operation);
|
|
225
|
+
return target ? this.link(target, operation ? `${target.name}.${operation.name}` : target.name, `${effect.source}.${effect.operation}`) : code(`${effect.source}.${effect.operation}`);
|
|
226
|
+
});
|
|
227
|
+
if (capabilities.length && !method.body)
|
|
228
|
+
facts.push('It can call ' + coordinate(capabilities) + '.');
|
|
229
|
+
if (errors.length && !method.endpoint)
|
|
230
|
+
facts.push('Failures can raise ' + coordinate(errors.map(name => {
|
|
231
|
+
const note = documentation?.tags?.find(tag => (tag.name === 'throws' || tag.name === 'exception') && tag.value.startsWith(name + ' '))?.value.slice(name.length).trim() ??
|
|
232
|
+
documentation?.markdown.split('**Throws**\n')[1]?.split('\n\n')[0]?.split('\n').find(line => line.startsWith('- ' + code(name) + ': '))?.slice(('- ' + code(name) + ': ').length);
|
|
233
|
+
const def = this.checked.project.scopes.get(method.span.file)?.get(name);
|
|
234
|
+
return (def ? this.link(def) : code(name)) + (note ? ` (${note.replace(/[.!?]$/, '').replace(/^When\b/, 'when')})` : '');
|
|
235
|
+
})) + '.');
|
|
236
|
+
if (facts.length)
|
|
237
|
+
result.push(paragraph(facts.join(' ')));
|
|
163
238
|
if (method.endpoint) {
|
|
164
239
|
const endpoint = method.endpoint;
|
|
165
|
-
|
|
166
|
-
(
|
|
167
|
-
|
|
240
|
+
if (endpoint.streams)
|
|
241
|
+
result.push(paragraph(`It streams ${this.type(method.returns)} items.`));
|
|
242
|
+
else if (endpoint.status !== 200)
|
|
243
|
+
result.push(paragraph(`A plain response body uses HTTP status ${endpoint.status}.`));
|
|
168
244
|
if (endpoint.errors.length)
|
|
169
|
-
|
|
245
|
+
result.push(paragraph('The handler responds with ' + coordinate(endpoint.errors.map(error => `HTTP ${error.status} for ${this.type(error.type)}`)) + '.'));
|
|
246
|
+
const unmapped = errors.filter(name => !endpoint.errors.some(error => typeName(error.type) === name));
|
|
247
|
+
if (unmapped.length)
|
|
248
|
+
result.push(paragraph('It can also raise ' + coordinate(unmapped.map(code)) + '.'));
|
|
170
249
|
}
|
|
171
|
-
return
|
|
250
|
+
return result;
|
|
172
251
|
}
|
|
173
252
|
layers(node) {
|
|
174
253
|
const layers = this.checked.interceptorPlans.get(node) ?? [];
|
|
175
254
|
const policies = node.kind === 'function' ? this.checked.httpPolicies.get(node) ?? [] : [];
|
|
176
255
|
if (!layers.length && !policies.length)
|
|
177
|
-
return
|
|
178
|
-
|
|
179
|
-
policies.forEach(
|
|
180
|
-
|
|
181
|
-
(node.kind === 'function' && policy.dependencies.length ? ' Use ' + policy.dependencies.map(index => code(node.params[index].name)).join(', ') + '.' : '')
|
|
256
|
+
return [];
|
|
257
|
+
const steps = [];
|
|
258
|
+
policies.forEach(policy => {
|
|
259
|
+
steps.push(this.policy(policy) +
|
|
260
|
+
(node.kind === 'function' && policy.dependencies.length ? ' Use ' + policy.dependencies.map(index => code(node.params[index].name)).join(', ') + '.' : ''));
|
|
182
261
|
});
|
|
183
|
-
layers.forEach(
|
|
262
|
+
layers.forEach(layer => {
|
|
184
263
|
this.use(layer.definition, layer.around);
|
|
185
|
-
|
|
186
|
-
'
|
|
187
|
-
(layer.annotation.mappings.length ? 'Map ' + layer.annotation.mappings.map(mapping => code(mapping.source) + ' to ' + code(mapping.name)).join(', ') + '. ' : '') +
|
|
188
|
-
'Unselected inputs pass through.\n';
|
|
264
|
+
steps.push(`Call ${this.link(layer.definition, `${layer.definition.name}.around`)}.` +
|
|
265
|
+
(layer.annotation.mappings.length ? ' Map ' + layer.annotation.mappings.map(mapping => code(mapping.source) + ' to ' + code(mapping.name)).join(', ') + '.' : ''));
|
|
189
266
|
});
|
|
190
|
-
return
|
|
267
|
+
return [paragraph('Layers run in the declared order. ' + steps.join(' ')),
|
|
268
|
+
...(policies.length ? [paragraph('HTTP policies run before decoding; custom interceptors run after it.')] : [])];
|
|
191
269
|
}
|
|
192
270
|
policy(policy) {
|
|
193
271
|
const options = policy.options;
|
|
@@ -202,10 +280,12 @@ class SpecWriter {
|
|
|
202
280
|
case 'Compress': return 'Negotiate gzip. Respect existing Content-Encoding and bodyless statuses. Encode stream items as complete concatenated gzip members.';
|
|
203
281
|
}
|
|
204
282
|
}
|
|
205
|
-
receiver(expr) {
|
|
206
|
-
const checked = this.checked.expressionTypes.get(expr)
|
|
207
|
-
if (checked)
|
|
208
|
-
return checked;
|
|
283
|
+
receiver(expr, operation) {
|
|
284
|
+
const checked = this.checked.expressionTypes.get(expr);
|
|
285
|
+
if (checked?.def)
|
|
286
|
+
return checked.def;
|
|
287
|
+
if (checked?.kind === 'param')
|
|
288
|
+
return checked.bounds?.find(bound => bound.def && (!operation || this.method(bound.def, operation)))?.def;
|
|
209
289
|
const ref = expr.kind === 'name' ? this.locals.get(expr.name) : undefined;
|
|
210
290
|
if (ref)
|
|
211
291
|
return this.definition(ref.name);
|
|
@@ -216,7 +296,7 @@ class SpecWriter {
|
|
|
216
296
|
if (def?.node.kind === 'class')
|
|
217
297
|
return def;
|
|
218
298
|
if (def?.node.kind === 'function')
|
|
219
|
-
return this.
|
|
299
|
+
return callableResult(this.checked, def.node).def;
|
|
220
300
|
}
|
|
221
301
|
return undefined;
|
|
222
302
|
}
|
|
@@ -246,57 +326,196 @@ class SpecWriter {
|
|
|
246
326
|
return def ? this.link(def) : code(expr.name);
|
|
247
327
|
}
|
|
248
328
|
case 'member': {
|
|
249
|
-
const def = this.receiver(expr.object);
|
|
329
|
+
const def = this.receiver(expr.object, expr.name);
|
|
250
330
|
this.use(def, undefined, expr.name);
|
|
251
331
|
const name = expr.name, receiverType = this.checked.expressionTypes.get(expr.object)?.name;
|
|
252
332
|
const property = receiverType && builtinProperties[receiverType]?.find(property => property.name === name);
|
|
253
333
|
if (property)
|
|
254
334
|
this.properties.set(receiverType + '.' + name, { ...property, type: this.checked.expressionTypes.get(expr) ? tyName(this.checked.expressionTypes.get(expr)) : property.type });
|
|
255
|
-
|
|
335
|
+
const object = this.expression(expr.object), path = this.memberPath(expr);
|
|
336
|
+
return path ? code(path) : `${code(name)} of ${object}`;
|
|
256
337
|
}
|
|
257
338
|
case 'unary': {
|
|
258
339
|
if (expr.op === '!' && expr.value.kind === 'binary' && (expr.value.op === '==' || expr.value.op === '!='))
|
|
259
340
|
return `${this.expression(expr.value.left, true)} ${expr.value.op === '==' ? 'does not equal' : 'equals'} ${this.expression(expr.value.right, true)}`;
|
|
341
|
+
if (expr.op === '!' && expr.value.kind === 'call')
|
|
342
|
+
return this.predicate(expr.value, false);
|
|
260
343
|
return `${expr.op === '!' ? 'not' : 'the negative of'} (${this.expression(expr.value)})`;
|
|
261
344
|
}
|
|
262
345
|
case 'binary': {
|
|
263
346
|
const words = { '+': 'plus', '-': 'minus', '*': 'times', '/': 'divided by', '==': 'equals', '!=': 'does not equal', '<': 'is less than', '>': 'is greater than', '<=': 'is at most', '>=': 'is at least', '&&': 'and', '||': 'or' };
|
|
264
347
|
if (!words[expr.op])
|
|
265
348
|
throw new Error(`No specification renderer for operator ${expr.op}`);
|
|
349
|
+
if (expr.right.kind === 'literal' && expr.right.value === null && ['==', '!='].includes(expr.op))
|
|
350
|
+
return this.expression(expr.left) + (expr.op === '==' ? ' is null' : ' is not null');
|
|
351
|
+
if (expr.right.kind === 'literal' && expr.right.value === 0 && ['>', '<'].includes(expr.op))
|
|
352
|
+
return this.expression(expr.left) + (expr.op === '>' ? ' is positive' : ' is negative');
|
|
266
353
|
const textParts = this.stringParts(expr);
|
|
267
|
-
if (expr.op === '+' && textParts.length >=
|
|
268
|
-
|
|
269
|
-
|
|
354
|
+
if (expr.op === '+' && textParts.length >= 2) {
|
|
355
|
+
const text = textParts.map(part => part.kind === 'literal' && typeof part.value === 'string' ?
|
|
356
|
+
part.value.replaceAll('{', '{{').replaceAll('}', '}}') : '{' + plain(this.expression(part)) + '}').join('');
|
|
357
|
+
return 'the text ' + code(text);
|
|
358
|
+
}
|
|
359
|
+
const child = (value) => this.expression(value, value.kind === 'binary' && !(['&&', '||'].includes(expr.op) &&
|
|
360
|
+
(value.op === expr.op || ['==', '!=', '<', '>', '<=', '>='].includes(value.op))));
|
|
361
|
+
const result = `${child(expr.left)} ${words[expr.op]} ${child(expr.right)}`;
|
|
270
362
|
return nested ? `(${result})` : result;
|
|
271
363
|
}
|
|
272
364
|
case 'collection': {
|
|
273
365
|
if (expr.collection === 'Map')
|
|
274
366
|
return 'a map with ' + expr.items.filter((_, index) => index % 2 === 0).map((key, index) => `${this.expression(key)} mapped to ${this.expression(expr.items[index * 2 + 1])}`).join('; ');
|
|
367
|
+
const first = expr.items[0];
|
|
368
|
+
if (expr.collection === 'List' && expr.items.length >= 3 && first?.kind === 'call' && first.callee.kind === 'name' && !this.locals.has(first.callee.name)) {
|
|
369
|
+
const name = first.callee.name, def = this.definition(name);
|
|
370
|
+
const labels = first.argLabels;
|
|
371
|
+
if (def?.node.kind === 'class' && def.node.record && labels.every(Boolean) && expr.items.every(item => item.kind === 'call' && item.callee.kind === 'name' && item.callee.name === name &&
|
|
372
|
+
item.typeArgs.map(typeName).join(',') === first.typeArgs.map(typeName).join(',') &&
|
|
373
|
+
item.args.every(arg => arg.kind === 'literal') && item.argLabels.length === labels.length &&
|
|
374
|
+
item.argLabels.every((label, index) => label === labels[index]))) {
|
|
375
|
+
this.call(first);
|
|
376
|
+
const rows = expr.items.map(item => code('(' + item.args.map(arg => arg.kind === 'literal' ? arg.numericText ?? JSON.stringify(arg.value) : '').join(', ') + ')'));
|
|
377
|
+
return `a list of ${expr.items.length} ${this.link(def)} records, with ${code('(' + labels.join(', ') + ')')} values of ${coordinate(rows)}, in that order`;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
275
380
|
return `a ${expr.collection === 'empty' ? 'context-typed empty collection' : expr.collection.toLowerCase()}` + (expr.items.length ? ' containing ' + expr.items.map(item => this.expression(item)).join(', ') : ' with no items');
|
|
276
381
|
}
|
|
277
382
|
case 'resolve': return `the instance provided for ${code(expr.name)}${expr.typeArgs.length ? ' with type arguments ' + expr.typeArgs.map(type => this.type(type)).join(', ') : ''}`;
|
|
278
|
-
case 'call':
|
|
279
|
-
|
|
280
|
-
|
|
383
|
+
case 'call': {
|
|
384
|
+
const action = this.call(expr);
|
|
385
|
+
const known = this.knownValue(expr);
|
|
386
|
+
if (known)
|
|
387
|
+
return known;
|
|
388
|
+
if (expr.callee.kind === 'member') {
|
|
389
|
+
const receiver = expr.callee.object, type = this.checked.expressionTypes.get(receiver)?.name;
|
|
390
|
+
if (!this.receiver(receiver) && ['string', 'Bytes', 'List', 'Set', 'Map', 'Tuple'].includes(type ?? '') && !expr.args.length) {
|
|
391
|
+
const value = this.expression(receiver);
|
|
392
|
+
if (expr.callee.name === 'bytes' && type === 'string')
|
|
393
|
+
return 'the UTF-8 bytes of ' + value;
|
|
394
|
+
if (expr.callee.name === 'base64url' && type === 'Bytes')
|
|
395
|
+
return 'the URL-safe base64 encoding of ' + value;
|
|
396
|
+
if (expr.callee.name === 'length')
|
|
397
|
+
return (type === 'string' || type === 'Bytes' ? 'the byte length of ' : 'the number of elements in ') + value;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const name = expr.callee.kind === 'name' ? expr.callee.name : '';
|
|
401
|
+
return action.startsWith('construct ') ? (/^[AEIOU]/i.test(name) ? 'an ' : 'a ') + action.slice('construct '.length) :
|
|
402
|
+
action.startsWith('call ') ? action.slice('call '.length) : action;
|
|
403
|
+
}
|
|
404
|
+
case 'start': return `a child task running ${this.expression(expr.call)} with its inputs captured now`;
|
|
405
|
+
case 'wait': return `the result of waiting for ${expr.tasks.map(task => this.expression(task)).join(' and ')}${expr.tasks.length > 1 ? ' in input order' : ''}; propagate failures`;
|
|
281
406
|
case 'handle': {
|
|
282
|
-
const plan = this.checked.actions.get(expr);
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
407
|
+
const plan = this.checked.actions.get(expr), endpoint = plan?.endpoint.node.kind === 'function' ? plan.endpoint.node.endpoint : undefined;
|
|
408
|
+
const captures = expr.call.kind === 'call' ? expr.call.args.filter(value => value.kind !== 'formInput').map(value => this.expression(value)) : [];
|
|
409
|
+
return endpoint ? `a form action that sends ${code(endpoint.method + ' ' + endpoint.path)} to ${this.link(plan.endpoint)} on submission` +
|
|
410
|
+
(captures.length ? ' with captured ' + coordinate(captures) : '') : `a form action for ${this.expression(expr.call)}`;
|
|
286
411
|
}
|
|
287
|
-
case 'formInput': return 'the
|
|
412
|
+
case 'formInput': return 'the submitted form data';
|
|
288
413
|
case 'markupText': return code(expr.text);
|
|
289
414
|
case 'markup': {
|
|
290
415
|
const def = this.definition(expr.tag);
|
|
291
416
|
this.use(def);
|
|
292
|
-
|
|
417
|
+
const elements = { p: 'a paragraph', h1: 'a heading', h2: 'a heading', button: 'a button', a: 'a link' };
|
|
418
|
+
return (def ? 'the server component ' + this.link(def) : elements[expr.tag] ?? 'the HTML element ' + code(expr.tag || 'fragment')) +
|
|
293
419
|
(expr.attributes.length ? ' with ' + expr.attributes.map(attribute => `${code(attribute.name)} = ${this.expression(attribute.value)}`).join(', ') : '') +
|
|
294
420
|
(expr.children.length ? ' containing ' + expr.children.map(child => this.expression(child)).join(', ') : '') +
|
|
295
|
-
'
|
|
421
|
+
' with escaped text';
|
|
296
422
|
}
|
|
297
423
|
default: return unreachable(expr);
|
|
298
424
|
}
|
|
299
425
|
}
|
|
426
|
+
memberPath(expr) {
|
|
427
|
+
if (expr.kind === 'name' && !this.definition(expr.name))
|
|
428
|
+
return expr.name;
|
|
429
|
+
if (expr.kind === 'name' && this.locals.has(expr.name))
|
|
430
|
+
return expr.name;
|
|
431
|
+
if (expr.kind === 'member') {
|
|
432
|
+
const object = this.memberPath(expr.object);
|
|
433
|
+
return object ? object + '.' + expr.name : undefined;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
argument(expr, label) {
|
|
437
|
+
const explicit = expr.args.findIndex((arg, index) => (expr.argLabels[index] ?? (arg.kind === 'name' ? arg.name : '')) === label);
|
|
438
|
+
if (explicit >= 0)
|
|
439
|
+
return expr.args[explicit];
|
|
440
|
+
const plan = this.checked.callPlans.get(expr);
|
|
441
|
+
const def = expr.callee.kind === 'name' ? this.definition(expr.callee.name) : expr.callee.kind === 'member' ? this.receiver(expr.callee.object, expr.callee.name) : undefined;
|
|
442
|
+
const method = def?.node.kind === 'function' ? def.node : expr.callee.kind === 'member' && def ? this.method(def, expr.callee.name) : undefined;
|
|
443
|
+
const params = method?.params ?? (def?.node.kind === 'class' ? def.node.fields : []);
|
|
444
|
+
const slot = params.findIndex(param => (param.label ?? param.name) === label);
|
|
445
|
+
const index = slot >= 0 ? plan?.sourceIndices[slot] : undefined;
|
|
446
|
+
return index !== undefined && index >= 0 ? expr.args[index] : undefined;
|
|
447
|
+
}
|
|
448
|
+
/** Interpret only compiler built-ins and the canonical standard-library contracts. */
|
|
449
|
+
standardOperation(expr) {
|
|
450
|
+
if (expr.callee.kind !== 'member')
|
|
451
|
+
return;
|
|
452
|
+
const def = this.receiver(expr.callee.object, expr.callee.name);
|
|
453
|
+
if (!def || !this.checked.project.files.get(def.file)?.builtin)
|
|
454
|
+
return;
|
|
455
|
+
return libraryRelative(this.checked.project.libraries, def.file).replaceAll('\\', '/') + ':' + def.name + '.' + expr.callee.name;
|
|
456
|
+
}
|
|
457
|
+
knownValue(expr) {
|
|
458
|
+
const arg = (label) => { const value = this.argument(expr, label); return value ? this.expression(value) : undefined; };
|
|
459
|
+
const receiver = expr.callee.kind === 'member' ? this.expression(expr.callee.object) : '';
|
|
460
|
+
switch (this.standardOperation(expr)) {
|
|
461
|
+
case 'crypto/contracts.aug:Crypto.random':
|
|
462
|
+
case 'crypto/contracts.aug:GnuTlsCrypto.random':
|
|
463
|
+
return `${arg('size')} random bytes from ${receiver}`;
|
|
464
|
+
case 'crypto/contracts.aug:Crypto.decodeBase64url':
|
|
465
|
+
case 'crypto/contracts.aug:GnuTlsCrypto.decodeBase64url':
|
|
466
|
+
return `${arg('input')} decoded as URL-safe base64 by ${receiver}`;
|
|
467
|
+
case 'time/contracts.aug:Clock.now':
|
|
468
|
+
case 'time/contracts.aug:SystemClock.now': return 'the current time from ' + receiver;
|
|
469
|
+
case 'memory/store.aug:ExpiringStore.take':
|
|
470
|
+
case 'memory/store.aug:MemoryStore.take':
|
|
471
|
+
return `the live value removed from ${receiver} under ${arg('key')}, using ${arg('now')} as the current time`;
|
|
472
|
+
case 'memory/store.aug:ExpiringStore.get':
|
|
473
|
+
case 'memory/store.aug:MemoryStore.get':
|
|
474
|
+
return `the live value in ${receiver} under ${arg('key')}, using ${arg('now')} as the current time`;
|
|
475
|
+
}
|
|
476
|
+
if (expr.callee.kind === 'member' && !this.receiver(expr.callee.object)) {
|
|
477
|
+
const type = this.checked.expressionTypes.get(expr.callee.object)?.name;
|
|
478
|
+
if (expr.callee.name === 'get' && type === 'List')
|
|
479
|
+
return `the item at index ${arg('index')} in ${receiver}`;
|
|
480
|
+
if (expr.callee.name === 'get' && type === 'Map')
|
|
481
|
+
return `the value under ${arg('key')} in ${receiver}`;
|
|
482
|
+
if (expr.callee.name === 'contains' && type === 'Map')
|
|
483
|
+
return `whether ${receiver} contains the key ${arg('key')}`;
|
|
484
|
+
if (expr.callee.name === 'contains' && type === 'Set')
|
|
485
|
+
return `whether ${receiver} contains ${arg('value')}`;
|
|
486
|
+
if (expr.callee.name === 'with' && type === 'Headers')
|
|
487
|
+
return `${receiver} with the header ${arg('name')} set to ${arg('value')}`;
|
|
488
|
+
}
|
|
489
|
+
if (expr.callee.kind === 'name') {
|
|
490
|
+
const def = this.definition(expr.callee.name);
|
|
491
|
+
if (def?.file && this.checked.project.files.get(def.file)?.builtin && libraryRelative(this.checked.project.libraries, def.file).replaceAll('\\', '/') === 'web/contracts.aug' && def.name === 'urlEncode')
|
|
492
|
+
return 'URL-encoded ' + arg('input');
|
|
493
|
+
if (this.checked.expressionTypes.get(expr)?.name === 'Html') {
|
|
494
|
+
const message = this.argument(expr, 'message');
|
|
495
|
+
if (message && expr.args.length === 1)
|
|
496
|
+
return this.expression(expr.callee) + ' showing ' + this.expression(message);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (expr.callee.kind === 'name' && !this.definition(expr.callee.name) && expr.callee.name === 'HttpResponse') {
|
|
500
|
+
const body = arg('body'), status = arg('status') ?? '200', headers = arg('headers');
|
|
501
|
+
return `HTTP ${status.replaceAll('`', '')} with ${body ?? 'an empty body'}` + (headers ? ' and ' + headers + ' headers' : '');
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
predicate(expr, expected = true) {
|
|
505
|
+
this.call(expr);
|
|
506
|
+
if (expr.callee.kind === 'member' && !this.receiver(expr.callee.object) &&
|
|
507
|
+
this.checked.expressionTypes.get(expr.callee.object)?.name === 'string' && expr.callee.name === 'isToken') {
|
|
508
|
+
const min = this.argument(expr, 'min'), max = this.argument(expr, 'max');
|
|
509
|
+
return this.expression(expr.callee.object) + ` is ${expected ? '' : 'not '}a URL-safe ASCII token` +
|
|
510
|
+
(min && max ? ' with ' + this.expression(min) + ' to ' + this.expression(max) + ' characters' : '');
|
|
511
|
+
}
|
|
512
|
+
const operation = this.standardOperation(expr);
|
|
513
|
+
if (operation === 'crypto/contracts.aug:Crypto.equal' || operation === 'crypto/contracts.aug:GnuTlsCrypto.equal') {
|
|
514
|
+
const left = this.argument(expr, 'left'), right = this.argument(expr, 'right');
|
|
515
|
+
return this.expression(left) + ' and ' + this.expression(right) + (expected ? ' match' : ' differ') + ' when compared by ' + this.expression(expr.callee.kind === 'member' ? expr.callee.object : expr.callee);
|
|
516
|
+
}
|
|
517
|
+
return this.expression(expr) + ` returns ${expected ? 'true' : 'false'}`;
|
|
518
|
+
}
|
|
300
519
|
call(expr) {
|
|
301
520
|
if (expr.callee.kind === 'name' && ['List', 'Set', 'Map'].includes(expr.callee.name) && !this.definition(expr.callee.name)) {
|
|
302
521
|
const name = expr.callee.name, types = expr.typeArgs.map(type => this.type(type));
|
|
@@ -305,13 +524,14 @@ class SpecWriter {
|
|
|
305
524
|
name.toLowerCase() + ' of ' + (types[0] ?? 'any');
|
|
306
525
|
return items.length ? 'a ' + label + ' containing ' + items.join(', ') : 'an empty ' + label;
|
|
307
526
|
}
|
|
308
|
-
let target, method;
|
|
527
|
+
let target, method, constructs = false;
|
|
309
528
|
if (expr.callee.kind === 'name') {
|
|
310
529
|
const name = expr.callee.name;
|
|
311
530
|
const def = this.definition(expr.callee.name);
|
|
312
531
|
this.use(def);
|
|
313
532
|
if (def?.node.kind === 'class' && def.file !== this.file.path)
|
|
314
533
|
this.used.get(def.id).constructed = true;
|
|
534
|
+
constructs = def?.node.kind === 'class' || !def && Object.hasOwn(builtinTypes, name);
|
|
315
535
|
method = def?.node.kind === 'function' ? def.node : undefined;
|
|
316
536
|
const builtin = !def && builtinFunctions.find(operation => operation.name === name);
|
|
317
537
|
if (builtin)
|
|
@@ -320,7 +540,7 @@ class SpecWriter {
|
|
|
320
540
|
}
|
|
321
541
|
else if (expr.callee.kind === 'member') {
|
|
322
542
|
const name = expr.callee.name;
|
|
323
|
-
const def = this.receiver(expr.callee.object);
|
|
543
|
+
const def = this.receiver(expr.callee.object, expr.callee.name);
|
|
324
544
|
method = def && this.method(def, expr.callee.name);
|
|
325
545
|
this.use(def, method);
|
|
326
546
|
const receiver = this.checked.expressionTypes.get(expr.callee.object);
|
|
@@ -335,7 +555,10 @@ class SpecWriter {
|
|
|
335
555
|
// An inherited operation links to the interface that actually declares it.
|
|
336
556
|
const owner = method && [...this.checked.project.definitions.values()].find(def => 'methods' in def.node && def.node.methods.includes(method));
|
|
337
557
|
this.use(owner, method);
|
|
338
|
-
|
|
558
|
+
const object = this.expression(expr.callee.object), path = this.memberPath(expr.callee);
|
|
559
|
+
target = owner ? this.link(owner, `${owner.name}.${method.name}`, path ?? expr.callee.name) : code(path ?? expr.callee.name);
|
|
560
|
+
if (!path)
|
|
561
|
+
target += ' on ' + object;
|
|
339
562
|
}
|
|
340
563
|
else
|
|
341
564
|
target = this.expression(expr.callee);
|
|
@@ -346,12 +569,19 @@ class SpecWriter {
|
|
|
346
569
|
const inputs = expr.args.map((arg, index) => {
|
|
347
570
|
const slot = plan?.sourceIndices.indexOf(index);
|
|
348
571
|
const label = expr.argLabels[index] ?? (slot !== undefined && slot >= 0 ? parameters[slot]?.label ?? parameters[slot]?.name : arg.kind === 'name' ? arg.name : undefined);
|
|
349
|
-
|
|
572
|
+
const same = arg.kind === 'name' && arg.name === label || arg.kind === 'member' && arg.name === label;
|
|
573
|
+
const value = this.expression(arg);
|
|
574
|
+
return label && !same ? code(label) + (arg.kind === 'literal' ? ' ' : ' from ') + value : value;
|
|
575
|
+
});
|
|
576
|
+
let text = `${constructs ? 'construct' : 'call'} ${target}` + (expr.typeArgs.length ? ' for ' + coordinate(expr.typeArgs.map(type => this.type(type))) : '') + (inputs.length ? ' with ' + coordinate(inputs) : '');
|
|
577
|
+
const injected = parameters.flatMap((param, index) => {
|
|
578
|
+
if (!param.injected)
|
|
579
|
+
return [];
|
|
580
|
+
const source = plan?.injectionSources?.[index] ?? plan?.bindingKeys[index] ?? typeName(param.type);
|
|
581
|
+
return [code(source) + (source === param.name ? '' : ' for ' + code(param.name))];
|
|
350
582
|
});
|
|
351
|
-
let text = `call ${target}` + (expr.typeArgs.length ? ' with type arguments ' + expr.typeArgs.map(type => this.type(type)).join(', ') : '') + (inputs.length ? ' with ' + inputs.join('; ') : '');
|
|
352
|
-
const injected = parameters.flatMap((param, index) => param.injected ? [`${code(param.name)} from ${code(plan?.injectionSources?.[index] ?? plan?.bindingKeys[index] ?? typeName(param.type))}`] : []);
|
|
353
583
|
if (injected.length)
|
|
354
|
-
text += '
|
|
584
|
+
text += ' using injected ' + coordinate(injected);
|
|
355
585
|
return text;
|
|
356
586
|
}
|
|
357
587
|
stringParts(expr) {
|
|
@@ -359,21 +589,55 @@ class SpecWriter {
|
|
|
359
589
|
return [...this.stringParts(expr.left), ...this.stringParts(expr.right)];
|
|
360
590
|
return [expr];
|
|
361
591
|
}
|
|
362
|
-
joinedText(target, value
|
|
363
|
-
|
|
364
|
-
if (parts.length < 4)
|
|
592
|
+
joinedText(target, value) {
|
|
593
|
+
if (this.stringParts(value).length < 2)
|
|
365
594
|
return;
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
595
|
+
return step(`It builds ${target} as ${this.expression(value)}.`);
|
|
596
|
+
}
|
|
597
|
+
headerFields(value) {
|
|
598
|
+
let current = value;
|
|
599
|
+
const calls = [];
|
|
600
|
+
while (current.kind === 'call' && current.callee.kind === 'member' && current.callee.name === 'with' &&
|
|
601
|
+
this.checked.expressionTypes.get(current.callee.object)?.name === 'Headers') {
|
|
602
|
+
const labels = current.args.map((arg, index) => current.kind === 'call' ?
|
|
603
|
+
current.argLabels[index] ?? (arg.kind === 'name' ? arg.name : '') : ''), name = labels.indexOf('name'), value = labels.indexOf('value');
|
|
604
|
+
if (name < 0 || value < 0)
|
|
605
|
+
return;
|
|
606
|
+
calls.unshift(current);
|
|
607
|
+
current = current.callee.object;
|
|
608
|
+
}
|
|
609
|
+
if (calls.length < 3)
|
|
610
|
+
return;
|
|
611
|
+
const operation = collectionOperations.Headers?.find(operation => operation.name === 'with');
|
|
612
|
+
if (operation)
|
|
613
|
+
this.builtins.set('Headers.with', operation);
|
|
614
|
+
return { base: current, fields: calls.map(call => {
|
|
615
|
+
const labels = call.args.map((arg, index) => call.argLabels[index] ?? (arg.kind === 'name' ? arg.name : ''));
|
|
616
|
+
return `${this.expression(call.args[labels.indexOf('name')])} to ${this.expression(call.args[labels.indexOf('value')])}`;
|
|
617
|
+
}) };
|
|
618
|
+
}
|
|
619
|
+
statements(body) {
|
|
620
|
+
const result = [];
|
|
621
|
+
for (let index = 0; index < body.length; index++) {
|
|
622
|
+
const stmt = body[index], nodes = this.statement(stmt), tail = body[index + 1];
|
|
623
|
+
// A terminal guard and final return are exact alternatives, even without a written else.
|
|
624
|
+
if (stmt.kind === 'if' && !stmt.otherwise.length && index === body.length - 2 && tail?.kind === 'return' &&
|
|
625
|
+
stmt.then.at(-1)?.kind === 'return' && nodes[0].kind === 'branch') {
|
|
626
|
+
nodes[0].otherwise = this.statement(tail);
|
|
627
|
+
index++;
|
|
628
|
+
}
|
|
629
|
+
result.push(...nodes);
|
|
630
|
+
}
|
|
631
|
+
return result;
|
|
369
632
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
633
|
+
statement(stmt) {
|
|
634
|
+
const nodes = this.explainStatement(stmt);
|
|
635
|
+
// Source identities survive sentence aggregation. Two identical calls are still two facts.
|
|
636
|
+
nodes[0].source = `${stmt.span.file}:${stmt.span.start}:${stmt.span.end}`;
|
|
637
|
+
return nodes;
|
|
374
638
|
}
|
|
375
|
-
|
|
376
|
-
const
|
|
639
|
+
explainStatement(stmt) {
|
|
640
|
+
const nested = (body) => this.statements(body);
|
|
377
641
|
switch (stmt.kind) {
|
|
378
642
|
case 'assign': {
|
|
379
643
|
if (stmt.target.kind === 'name') {
|
|
@@ -386,42 +650,133 @@ class SpecWriter {
|
|
|
386
650
|
this.locals.set(stmt.target.name, { name: def.name, args: [], nullable: false, span: stmt.span });
|
|
387
651
|
}
|
|
388
652
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
653
|
+
if (stmt.declaredType)
|
|
654
|
+
this.type(stmt.declaredType);
|
|
655
|
+
const target = this.expression(stmt.target) + (stmt.declaredType && !['int', 'float', 'string', 'bool'].includes(stmt.declaredType.name) ? ' of type ' + this.type(stmt.declaredType) : '');
|
|
656
|
+
const headers = this.headerFields(stmt.value);
|
|
657
|
+
const arithmetic = stmt.value.kind === 'binary' && ['+', '-'].includes(stmt.value.op) && stmt.value.left.kind === 'name' && stmt.target.kind === 'name' && stmt.target.name === stmt.value.left.name && ['int', 'float', 'c_int'].includes(this.checked.expressionTypes.get(stmt.value)?.name ?? '');
|
|
658
|
+
const headerLead = headers ? `set ${target} from ${this.expression(headers.base)} by adding these header fields in order` : undefined;
|
|
659
|
+
const getter = stmt.value.kind === 'call' && !stmt.value.args.length && stmt.value.callee.kind === 'name' && this.definition(stmt.value.callee.name)?.node.kind === 'function';
|
|
660
|
+
const explanation = headers ? sequence('It ' + headerLead.replace(/^set /, 'sets '), headers.fields, headerLead) :
|
|
661
|
+
getter ? step('It gets ' + target + ' from ' + this.expression(stmt.value) + '.') :
|
|
662
|
+
this.joinedText(target, stmt.value) ?? (arithmetic && stmt.value.kind === 'binary' ? action(stmt.value.op === '+' ? 'increase' : 'decrease', `${target} by ${this.expression(stmt.value.right)}`) : action('set', `${target} to ${this.expression(stmt.value)}`));
|
|
663
|
+
return stmt.ownership === 'own' ? [explanation, step(`${target} owns this value.`)] : [explanation];
|
|
664
|
+
}
|
|
665
|
+
case 'destructure': return [action('split', `${this.expression(stmt.value)} into ${coordinate(stmt.names.map(code))} in order`)];
|
|
666
|
+
case 'expr': {
|
|
667
|
+
if (stmt.expr.kind === 'literal' && stmt.expr.value === null)
|
|
668
|
+
return [action('continue', 'without an operation')];
|
|
669
|
+
if (stmt.expr.kind === 'call') {
|
|
670
|
+
const call = this.call(stmt.expr);
|
|
671
|
+
if (stmt.expr.callee.kind === 'name' && !this.definition(stmt.expr.callee.name)) {
|
|
672
|
+
if (stmt.expr.callee.name === 'print')
|
|
673
|
+
return [step('It prints ' + this.expression(stmt.expr.args[0]) + '.')];
|
|
674
|
+
if (stmt.expr.callee.name === 'assert')
|
|
675
|
+
return [step('The test requires ' + this.condition(stmt.expr.args[0]) + '.')];
|
|
676
|
+
}
|
|
677
|
+
if (stmt.expr.callee.kind === 'member') {
|
|
678
|
+
const receiver = stmt.expr.callee.object, def = this.receiver(receiver, stmt.expr.callee.name), method = def && this.method(def, stmt.expr.callee.name);
|
|
679
|
+
const type = this.checked.expressionTypes.get(receiver)?.name;
|
|
680
|
+
const arg = (name) => this.expression(this.argument(stmt.expr, name));
|
|
681
|
+
if (!def) {
|
|
682
|
+
if (type === 'List' && stmt.expr.callee.name === 'append')
|
|
683
|
+
return [step('It appends ' + arg('value') + ' to ' + this.expression(receiver) + '.')];
|
|
684
|
+
if (type === 'Set' && stmt.expr.callee.name === 'add')
|
|
685
|
+
return [step('It adds ' + arg('value') + ' to ' + this.expression(receiver) + '.')];
|
|
686
|
+
if (type === 'Map' && stmt.expr.callee.name === 'set')
|
|
687
|
+
return [step('It stores ' + arg('value') + ' in ' + this.expression(receiver) + ' under ' + arg('key') + '.')];
|
|
688
|
+
if (type === 'Map' && stmt.expr.callee.name === 'take')
|
|
689
|
+
return [step('It removes the key ' + arg('key') + ' from ' + this.expression(receiver) + '.')];
|
|
690
|
+
}
|
|
691
|
+
if (method && callableResult(this.checked, method).name === 'void' && method.params.filter(param => !param.injected).length === 1 && stmt.expr.args.length === 1) {
|
|
692
|
+
const owner = [...this.checked.project.definitions.values()].find(owner => 'methods' in owner.node && owner.node.methods.includes(method));
|
|
693
|
+
const target = this.link(owner, owner.name + '.' + method.name, this.memberPath(stmt.expr.callee) ?? method.name);
|
|
694
|
+
const plan = this.checked.callPlans.get(stmt.expr);
|
|
695
|
+
const injected = method.params.flatMap((param, index) => param.injected ? [code(plan?.injectionSources?.[index] ?? plan?.bindingKeys[index] ?? typeName(param.type))] : []);
|
|
696
|
+
return [step('It passes ' + this.expression(stmt.expr.args[0]) + ' to ' + target + (injected.length ? ', using injected ' + coordinate(injected) : '') + '.')];
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
const stored = this.standardOperation(stmt.expr);
|
|
700
|
+
if (stored === 'memory/store.aug:ExpiringStore.put' || stored === 'memory/store.aug:MemoryStore.put') {
|
|
701
|
+
const arg = (name) => this.expression(this.argument(stmt.expr, name));
|
|
702
|
+
return [step(`It stores ${arg('value')} in ${this.expression(stmt.expr.callee.kind === 'member' ? stmt.expr.callee.object : stmt.expr.callee)} under ${arg('key')}, expiring at ${arg('expires')}. The current time for this write is ${arg('now')}.`)];
|
|
703
|
+
}
|
|
704
|
+
if (call.startsWith('call '))
|
|
705
|
+
return [action('call', call.slice(5))];
|
|
706
|
+
if (call.startsWith('construct '))
|
|
707
|
+
return [action('construct', call.slice(10))];
|
|
708
|
+
}
|
|
709
|
+
return [action('evaluate', this.expression(stmt.expr))];
|
|
392
710
|
}
|
|
393
|
-
case 'destructure': return lead + `Split ${this.expression(stmt.value)} into ${stmt.names.map(code).join(', ')}, in that order.\n`;
|
|
394
|
-
case 'expr': return lead + (stmt.expr.kind === 'literal' && stmt.expr.value === null ? 'Continue without another operation' : stmt.expr.kind === 'call' ? 'Call ' + this.call(stmt.expr).slice(5) : 'Evaluate ' + this.expression(stmt.expr)) + '.\n';
|
|
395
711
|
case 'return': {
|
|
396
712
|
if (!stmt.value)
|
|
397
|
-
return
|
|
398
|
-
const
|
|
399
|
-
|
|
713
|
+
return [action('return', 'without a value')];
|
|
714
|
+
const headers = this.headerFields(stmt.value);
|
|
715
|
+
if (headers) {
|
|
716
|
+
const lead = `return headers starting with ${this.expression(headers.base)} and adding these fields in order`;
|
|
717
|
+
return [sequence('It ' + lead.replace(/^return /, 'returns '), headers.fields, lead)];
|
|
718
|
+
}
|
|
719
|
+
return [action('return', this.expression(stmt.value))];
|
|
400
720
|
}
|
|
401
|
-
case 'throw': return
|
|
402
|
-
case 'yield': return
|
|
403
|
-
case 'if':
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
case '
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
721
|
+
case 'throw': return [action('fail', 'with ' + this.expression(stmt.value))];
|
|
722
|
+
case 'yield': return [action('send', this.expression(stmt.value) + ' as the next stream item')];
|
|
723
|
+
case 'if': {
|
|
724
|
+
const node = branch(this.condition(stmt.test), nested(stmt.then), nested(stmt.otherwise));
|
|
725
|
+
if (node.kind === 'branch' && !stmt.otherwise.length && stmt.then.length === 1 && stmt.then[0].kind === 'throw' &&
|
|
726
|
+
stmt.then[0].value.kind === 'call' && stmt.then[0].value.args.length === 0)
|
|
727
|
+
node.requirement = this.requirement(stmt.test);
|
|
728
|
+
return [node];
|
|
729
|
+
}
|
|
730
|
+
case 'while': return [loop('While ' + this.condition(stmt.test), nested(stmt.body), 'Repeat this loop while its condition remains true.')];
|
|
731
|
+
case 'for': return [loop('For each ' + coordinate(stmt.names.map(code)) + ' in a snapshot of ' + this.expression(stmt.iterable), nested(stmt.body), 'Repeat these steps for each remaining item in the snapshot.')];
|
|
732
|
+
case 'match': {
|
|
733
|
+
const value = this.expression(stmt.value);
|
|
734
|
+
const terminal = (body) => ['return', 'throw'].includes(body.at(-1)?.kind ?? '');
|
|
735
|
+
// A null guard that exits is followed only by the non-null path. This is
|
|
736
|
+
// a sequential explanation, not a nested tour through match syntax.
|
|
737
|
+
if (stmt.cases.length === 2 && stmt.cases[0].pattern === 'null' && terminal(stmt.cases[0].body) && stmt.cases[1].pattern === 'some') {
|
|
738
|
+
const read = stmt.value.kind === 'call', second = stmt.cases[1];
|
|
739
|
+
const first = branch(read ? 'no value is found' : value + ' is null', nested(stmt.cases[0].body));
|
|
740
|
+
return [...(read ? [step('It obtains ' + value + '.')] : []), first,
|
|
741
|
+
step('The non-null ' + (read ? 'result' : value) + ' becomes ' + code(second.name) + '.'), ...nested(second.body)];
|
|
742
|
+
}
|
|
743
|
+
return [choice(value, stmt.cases.map(clause => {
|
|
744
|
+
const condition = clause.pattern === 'else' ? 'Otherwise' : clause.pattern === 'some' ? 'If ' + value + ' is not null, using ' + code(clause.name) + ' for it' :
|
|
745
|
+
clause.pattern === 'type' ? 'If ' + value + ' satisfies ' + this.type(clause.type) + ', using ' + code(clause.name) + ' for it' :
|
|
746
|
+
clause.pattern === 'literal' ? 'If ' + value + ' equals ' + this.expression(clause.literal) : 'If ' + value + ' is null';
|
|
747
|
+
return { condition, children: nested(clause.body) };
|
|
748
|
+
}))];
|
|
749
|
+
}
|
|
750
|
+
case 'try': return [attempt(nested(stmt.body), stmt.catches.map(clause => ({ error: this.type(clause.type), name: code(clause.name), children: nested(clause.body) })), stmt.always ? nested(stmt.always) : undefined)];
|
|
751
|
+
case 'unsafe': return [scope('Within an unsafe block', nested(stmt.body), 'Native operations must satisfy their declared C contracts.')];
|
|
752
|
+
case 'borrow': return [scope('With temporary permission to change ' + code(stmt.name), nested(stmt.body), '')];
|
|
753
|
+
case 'scope': return [scope('Within a task and ownership scope', nested(stmt.body), 'On leaving this scope, join its child tasks and release its local values.')];
|
|
754
|
+
case 'lock': return [scope('While holding the lock on ' + this.expression(stmt.value) + ' as mutable ' + code(stmt.name), nested(stmt.body), 'Release this lock when the block exits, including on return or failure.')];
|
|
755
|
+
case 'freeze': return [action('freeze', this.expression(stmt.value) + ' as ' + code(stmt.name)), step('The immutable value is shared without copying it.')];
|
|
756
|
+
case 'serve': return [action('serve', coordinate(stmt.names.map(name => { const def = this.definition(name); return def ? this.link(def) : code(name); })) + ' on port ' + this.expression(stmt.port))];
|
|
420
757
|
default: return unreachable(stmt);
|
|
421
758
|
}
|
|
422
759
|
}
|
|
423
760
|
condition(expr) {
|
|
424
|
-
return this.expression(expr) + (['binary', 'unary'].includes(expr.kind) ? '' : ' is true');
|
|
761
|
+
return expr.kind === 'call' ? this.predicate(expr) : this.expression(expr) + (['binary', 'unary'].includes(expr.kind) ? '' : ' is true');
|
|
762
|
+
}
|
|
763
|
+
requirement(expr, nested = false) {
|
|
764
|
+
if (expr.kind === 'unary' && expr.op === '!')
|
|
765
|
+
return this.condition(expr.value);
|
|
766
|
+
if (expr.kind === 'binary') {
|
|
767
|
+
if (expr.op === '||' || expr.op === '&&') {
|
|
768
|
+
if (expr.op === '&&' && expr.left.kind === 'binary' && expr.right.kind === 'binary' && expr.left.op === '!=' && expr.right.op === '!=' &&
|
|
769
|
+
this.memberPath(expr.left.left) === this.memberPath(expr.right.left) && this.memberPath(expr.left.left))
|
|
770
|
+
return this.expression(expr.left.left) + ' is either ' + this.expression(expr.left.right) + ' or ' + this.expression(expr.right.right);
|
|
771
|
+
const child = (value) => this.requirement(value, value.kind === 'binary' && ['&&', '||'].includes(value.op) && value.op !== expr.op);
|
|
772
|
+
const text = child(expr.left) + (expr.op === '||' ? ' and ' : ' or ') + child(expr.right);
|
|
773
|
+
return nested ? '(' + text + ')' : text;
|
|
774
|
+
}
|
|
775
|
+
const inverse = { '==': 'does not equal', '!=': 'equals', '<': 'is at least', '>': 'is at most', '<=': 'is greater than', '>=': 'is less than' };
|
|
776
|
+
if (inverse[expr.op])
|
|
777
|
+
return this.expression(expr.left, true) + ' ' + inverse[expr.op] + ' ' + this.expression(expr.right, true);
|
|
778
|
+
}
|
|
779
|
+
return expr.kind === 'call' ? this.predicate(expr, false) : this.expression(expr) + ' is false';
|
|
425
780
|
}
|
|
426
781
|
binding(binding) {
|
|
427
782
|
const info = this.checked.bindings.find(info => info.declaration === binding);
|
|
@@ -429,241 +784,200 @@ class SpecWriter {
|
|
|
429
784
|
this.use(def);
|
|
430
785
|
const lifetime = binding.lifetime ?? info?.lifetime;
|
|
431
786
|
const key = binding.key + (binding.keyTypeArgs.length ? '<' + binding.keyTypeArgs.map(type => typeName(type)).join(', ') + '>' : '');
|
|
432
|
-
return (
|
|
433
|
-
(lifetime === 'shared' ? '
|
|
434
|
-
(binding.sharedMutation ? '
|
|
435
|
-
(info?.dependencies.length ? '
|
|
787
|
+
return (`${code(key)} is provided by ${def ? this.link(def, def.name, typeName(binding.target)) : this.type(binding.target)}. ` +
|
|
788
|
+
(lifetime === 'shared' ? 'The same instance is shared. ' : lifetime === 'scoped' ? 'Each scope shares one instance. ' : lifetime === 'fresh' ? 'Each resolve creates a new instance. ' : 'Stateless instances are reused; stateful instances are created for each resolve. ') +
|
|
789
|
+
(binding.sharedMutation ? 'Shared mutation is allowed. ' : '') +
|
|
790
|
+
(info?.dependencies.length ? 'It requires bindings for ' + coordinate(info.dependencies.map(code)) + '. ' : '')).trimEnd();
|
|
436
791
|
}
|
|
437
792
|
callable(method, owner) {
|
|
438
793
|
this.locals = new Map(owner && 'fields' in owner.node ? fieldsOf(owner.node).map(field => [field.name, field.type]) : []);
|
|
439
794
|
const name = owner && owner.node.kind !== 'function' ? `${owner.name}.${method.name}` : method.name;
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
795
|
+
const documentation = callableDocumentation(this.checked.project, method, owner);
|
|
796
|
+
const children = [];
|
|
797
|
+
if (method.endpoint)
|
|
798
|
+
children.push(paragraph(code(method.name) + ' handles ' + code(method.endpoint.method + ' ' + method.endpoint.path) + '.'));
|
|
799
|
+
if (method.name.startsWith('_'))
|
|
800
|
+
children.push(paragraph('It is private to its defining scope.'));
|
|
801
|
+
const notes = this.notes(documentation);
|
|
802
|
+
if (notes)
|
|
803
|
+
children.push(paragraph(notes));
|
|
804
|
+
children.push(...this.contract(method, documentation), ...this.layers(method));
|
|
805
|
+
if (method.body)
|
|
806
|
+
children.push(flow(this.statements(method.body)));
|
|
807
|
+
else if (method.externC)
|
|
808
|
+
children.push(paragraph('Native C implementation; only its declared contract is visible here.'));
|
|
809
|
+
return this.heading(name, method.span, owner && owner.node.kind !== 'function' ? 3 : 2, children);
|
|
810
|
+
}
|
|
811
|
+
exportLine(item) {
|
|
812
|
+
const folder = this.file.builtin ? libraryChild(this.checked.project.libraries, dirname(this.file.path), item.name) : join(dirname(this.file.path), item.name);
|
|
813
|
+
const file = item.folder ? join(folder, 'export.aug') : join(dirname(this.file.path), item.from + '.aug');
|
|
814
|
+
this.enqueue(file);
|
|
815
|
+
return `Export ${item.folder ? 'the folder ' : 'the declaration '}${code(item.name)} from [${code(basename(file))}](${url(relative(dirname(this.docs.get(this.file.path)), this.docs.get(file)))}${item.folder ? '' : '#' + encodeURIComponent(anchor(item.name))}).`;
|
|
816
|
+
}
|
|
817
|
+
providerLine(item) {
|
|
818
|
+
if (item.kind === 'bind')
|
|
819
|
+
return this.binding(item);
|
|
820
|
+
const def = this.definition(item.name);
|
|
821
|
+
return 'Include providers from ' + (def ? this.link(def) : code(item.name)) + '.';
|
|
444
822
|
}
|
|
445
823
|
declaration(item) {
|
|
446
824
|
switch (item.kind) {
|
|
447
|
-
case 'import': return '';
|
|
448
|
-
case 'export': {
|
|
449
|
-
const folder = this.file.builtin ? libraryChild(this.checked.project.libraries, dirname(this.file.path), item.name) : join(dirname(this.file.path), item.name);
|
|
450
|
-
const file = item.folder ? join(folder, 'export.aug') : join(dirname(this.file.path), item.from + '.aug');
|
|
451
|
-
this.enqueue(file);
|
|
452
|
-
return `- Export ${item.folder ? 'the folder ' : 'the declaration '}${code(item.name)} from [${code(basename(file))}](${url(relative(dirname(this.docs.get(this.file.path)), this.docs.get(file)))}${item.folder ? '' : '#' + encodeURIComponent(anchor(item.name))}).\n`;
|
|
453
|
-
}
|
|
454
825
|
case 'function': return this.callable(item);
|
|
455
826
|
case 'class': {
|
|
456
827
|
const def = this.definition(item.name);
|
|
457
828
|
this.locals = new Map();
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
829
|
+
const documentation = javadocBefore(this.file.source, item.annotations?.[0]?.span.start ?? item.span.start);
|
|
830
|
+
const children = [];
|
|
831
|
+
const generics = this.generics(item), notes = this.notes(documentation);
|
|
832
|
+
const inputs = this.inputs(item.fields, true, this.file.path, documentation?.parameters);
|
|
833
|
+
const implementsText = item.implements.length ? 'It implements ' + coordinate(item.implements.map(type => this.type(type))) + '.' : '';
|
|
834
|
+
const introduction = [notes, implementsText, item.name.startsWith('_') ? 'It is private to this file.' : '', generics].filter(Boolean).join(' ');
|
|
835
|
+
if (introduction)
|
|
836
|
+
children.push(paragraph(introduction));
|
|
837
|
+
if (inputs)
|
|
838
|
+
children.push(paragraph(inputs));
|
|
839
|
+
children.push(...this.layers(item));
|
|
840
|
+
const errors = this.checked.constructorContracts.get(item)?.errors;
|
|
841
|
+
if (errors?.length)
|
|
842
|
+
children.push(paragraph('Construction can fail with ' + errors.map(type => { this.use(type.def); return type.def ? this.link(type.def) : code(tyName(type)); }).join(', ') + '.'));
|
|
843
|
+
else if (item.validationErrors?.length)
|
|
844
|
+
children.push(paragraph('Construction can fail with ' + item.validationErrors.map(type => this.type(type)).join(', ') + '.'));
|
|
463
845
|
if (item.stateFields?.length)
|
|
464
|
-
|
|
846
|
+
children.push(...item.stateFields.map(field => { this.locals.set(field.name, field.type); return paragraph(`The ${field.mutable ? 'mutable' : 'read-only'}${field.name.startsWith('_') ? ', private' : ''} field ${code(field.name)} has type ${this.type(field.type)} and starts as ${this.expression(field.initializer)}.`); }));
|
|
465
847
|
if (item.constructorBody)
|
|
466
|
-
|
|
848
|
+
children.push(this.heading(item.name + '.initialize', item.span, 3, [flow(this.statements(item.constructorBody))]));
|
|
467
849
|
if (!item.record) {
|
|
468
850
|
const defaults = this.checked.defaults.get(def.id);
|
|
469
851
|
const inherited = [...(defaults?.values() ?? [])].filter(info => !item.methods.some(method => method.name === info.method.name));
|
|
470
852
|
if (inherited.length)
|
|
471
|
-
|
|
853
|
+
children.push(paragraph('It inherits the default implementations of ' + coordinate(inherited.map(info => { const parent = this.checked.project.definitions.get(info.from); this.use(parent, info.method); return parent ? this.link(parent, `${parent.name}.${info.method.name}`) : code(info.method.name); })) + '.'));
|
|
472
854
|
}
|
|
473
|
-
|
|
855
|
+
children.push(...item.methods.map(method => this.callable(method, def)));
|
|
856
|
+
return this.heading(item.name, item.span, 2, children, item.record ? 'immutable record' : 'class');
|
|
474
857
|
}
|
|
475
858
|
case 'interface':
|
|
476
859
|
case 'interceptor': {
|
|
477
860
|
const def = this.definition(item.name);
|
|
478
861
|
this.locals = new Map();
|
|
479
|
-
|
|
480
|
-
|
|
862
|
+
const generics = this.generics(item), notes = this.notes(javadocBefore(this.file.source, item.span.start));
|
|
863
|
+
const role = item.kind === 'interceptor' ? 'interceptor' : item.capability ? 'capability interface' : 'interface';
|
|
864
|
+
const introduction = [notes, item.name.startsWith('_') ? 'It is private to this file.' : '', generics].filter(Boolean).join(' ');
|
|
865
|
+
const children = introduction ? [paragraph(introduction)] : [];
|
|
481
866
|
if (item.kind === 'interface' && item.extends.length)
|
|
482
|
-
|
|
483
|
-
if (item.kind === 'interceptor')
|
|
484
|
-
|
|
485
|
-
|
|
867
|
+
children.push(paragraph('It inherits ' + coordinate(item.extends.map(type => this.type(type))) + '.'));
|
|
868
|
+
if (item.kind === 'interceptor') {
|
|
869
|
+
const inputs = this.inputs(item.fields, true);
|
|
870
|
+
if (inputs)
|
|
871
|
+
children.push(paragraph(inputs));
|
|
872
|
+
children.push(paragraph('Creates one interceptor per invocation. Its around operation may delegate once or finish early.'));
|
|
873
|
+
}
|
|
874
|
+
children.push(...item.methods.map(method => this.callable(method, def)));
|
|
875
|
+
return this.heading(item.name, item.span, 2, children, role);
|
|
486
876
|
}
|
|
487
|
-
case 'composition':
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
const def = this.definition(item.name);
|
|
491
|
-
return '- Include the providers from ' + (def ? this.link(def) : code(item.name)) + ' before execution.\n';
|
|
877
|
+
case 'composition': {
|
|
878
|
+
const notes = this.notes(javadocBefore(this.file.source, item.span.start));
|
|
879
|
+
return this.heading(item.name, item.span, 2, [...(notes ? [paragraph(notes)] : []), paragraph('These providers are registered before startup. ' + item.bindings.map(binding => this.binding(binding)).join(' '))]);
|
|
492
880
|
}
|
|
493
881
|
case 'test': return this.tests(item);
|
|
494
|
-
|
|
882
|
+
case 'bind':
|
|
883
|
+
case 'include': return paragraph(this.providerLine(item));
|
|
884
|
+
case 'export': return paragraph(this.exportLine(item));
|
|
885
|
+
case 'import': return paragraph('');
|
|
886
|
+
default: return flow(this.statement(item));
|
|
495
887
|
}
|
|
496
888
|
}
|
|
497
889
|
tests(suite) {
|
|
498
890
|
this.locals = new Map();
|
|
499
|
-
|
|
891
|
+
const children = [paragraph('Tests ' + this.type(suite.type) + '. Each case gets fresh setup and dependencies.')];
|
|
500
892
|
for (const group of suite.groups) {
|
|
501
893
|
this.locals = new Map();
|
|
502
|
-
|
|
894
|
+
const groupChildren = [];
|
|
503
895
|
if (group.setup.length)
|
|
504
|
-
|
|
896
|
+
groupChildren.push(paragraph('Setup for each case:'), flow(group.setup.flatMap(entry => entry.kind === 'bind' || entry.kind === 'include' ? [step(this.providerLine(entry))] : this.statement(entry))));
|
|
505
897
|
const setupLocals = new Map(this.locals);
|
|
506
898
|
for (const test of group.cases) {
|
|
507
899
|
this.locals = new Map(setupLocals);
|
|
508
|
-
|
|
900
|
+
const caseChildren = [];
|
|
509
901
|
if (test.parameters)
|
|
510
|
-
|
|
511
|
-
|
|
902
|
+
caseChildren.push(paragraph('Run once for each row of ' + test.rows.map(row => this.expression(row)).join('; ') + '. Bind row positions to ' + test.parameters.map(code).join(', ') + '.'));
|
|
903
|
+
caseChildren.push(flow(this.statements(test.body)));
|
|
904
|
+
groupChildren.push(section(code(test.name), 4, caseChildren, undefined, this.source(test.span)));
|
|
512
905
|
}
|
|
906
|
+
children.push(section(code(group.name), 3, groupChildren));
|
|
513
907
|
}
|
|
514
|
-
return
|
|
908
|
+
return this.heading(this.testTitle(suite), suite.span, 2, children);
|
|
515
909
|
}
|
|
516
910
|
testTitle(suite) {
|
|
517
911
|
return 'test ' + suite.type.name + (suite.name === suite.type.name ? '' : ' ' + suite.name);
|
|
518
912
|
}
|
|
519
913
|
dependencySurface() {
|
|
520
914
|
if (!this.used.size)
|
|
521
|
-
return
|
|
915
|
+
return;
|
|
522
916
|
const entries = [...this.used.values()].sort((a, b) => compare(relative(this.checked.project.root, this.docs.get(a.def.file)) + ':' + a.def.name, relative(this.checked.project.root, this.docs.get(b.def.file)) + ':' + b.def.name));
|
|
523
|
-
|
|
917
|
+
const groups = new Map();
|
|
524
918
|
for (const entry of entries) {
|
|
525
919
|
const def = entry.def, node = def.node;
|
|
526
|
-
const kind = node.kind === 'class' && node.record ? 'record' : node.kind === 'interface' && node.capability ? 'capability interface' : node.kind;
|
|
527
|
-
text += '### ' + this.link(def) + '\n\n' + kind[0].toUpperCase() + kind.slice(1);
|
|
528
920
|
const imported = this.file.items.find(item => item.kind === 'import' && (this.checked.project.imports.get(item) ?? []).some(imported => imported.id === def.id));
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
for (const name of [...entry.fields].sort(compare)) {
|
|
539
|
-
const field = fieldsOf(node).find(field => field.name === name);
|
|
540
|
-
if (field)
|
|
541
|
-
text += '- Read ' + code(name) + ' (' + this.type(field.type, def.file) + ')' + (field.mutable ? '; its owner can change it' : '') + '.\n';
|
|
542
|
-
}
|
|
543
|
-
if ('methods' in node)
|
|
544
|
-
for (const method of [...entry.operations].sort((a, b) => compare(a.name, b.name)))
|
|
545
|
-
text += this.dependencyOperation(method, def);
|
|
546
|
-
if (node.kind !== 'function' && !entry.constructed && !entry.fields.size && !entry.operations.size)
|
|
547
|
-
text += 'Used as a type or provider.\n';
|
|
548
|
-
text += '\n';
|
|
921
|
+
const origin = imported?.kind === 'import' ? code(imported.from.join('.')) + (imported.everything ? ' through import everything' : '') : '';
|
|
922
|
+
const names = [this.link(def)];
|
|
923
|
+
const members = [...entry.operations].sort((a, b) => compare(a.name, b.name)).map(method => this.link(def, def.name + '.' + method.name, method.name));
|
|
924
|
+
members.push(...[...entry.fields].sort(compare).map(name => code(name)));
|
|
925
|
+
if (members.length)
|
|
926
|
+
names[0] += ' (' + coordinate(members) + ')';
|
|
927
|
+
const group = groups.get(origin) ?? [];
|
|
928
|
+
group.push(...names);
|
|
929
|
+
groups.set(origin, group);
|
|
549
930
|
}
|
|
550
|
-
return
|
|
551
|
-
}
|
|
552
|
-
shortInputs(params, file) {
|
|
553
|
-
return params.filter(param => !param.injected).map(param => code(param.label ?? param.name) + ': ' + this.type(param.type, file) +
|
|
554
|
-
(param.source ? ' from HTTP ' + param.source.kind + (param.source.name ? ' ' + code(param.source.name) : '') : '')).join(', ') || 'no caller inputs';
|
|
555
|
-
}
|
|
556
|
-
dependencyOperation(method, owner) {
|
|
557
|
-
const effects = this.checked.effectContracts.get(method), layers = this.checked.interceptorPlans.get(method) ?? [];
|
|
558
|
-
const changes = effects?.changes ?? method.changes ?? [];
|
|
559
|
-
const uses = [...(effects?.uses.values() ?? method.uses ?? [])];
|
|
560
|
-
const errors = [...new Set([...method.throws.map(type => typeName(type)), ...layers.flatMap(layer => layer.errors.map(tyName))])];
|
|
561
|
-
const name = owner.node.kind === 'function' ? owner.name : owner.name + '.' + method.name;
|
|
562
|
-
let text = '- ' + this.link(owner, name) + (method.typeParams.length ? '<' + method.typeParams.map(code).join(', ') + '>' : '') +
|
|
563
|
-
' (' + this.shortInputs(method.params, method.span.file) + ') → ' + this.type(method.returns, method.span.file);
|
|
564
|
-
const injected = method.params.filter(param => param.injected);
|
|
565
|
-
if (injected.length)
|
|
566
|
-
text += '; inject ' + injected.map(param => code(param.name) + ': ' + this.type(param.type, method.span.file)).join(', ');
|
|
567
|
-
if (changes.length)
|
|
568
|
-
text += '; changes ' + changes.map(code).join(', ');
|
|
569
|
-
const otherUses = uses.filter(effect => effect.source !== owner.name || effect.operation !== method.name);
|
|
570
|
-
if (otherUses.length)
|
|
571
|
-
text += '; uses ' + otherUses.map(effect => {
|
|
572
|
-
const target = ('capability' in effect ? effect.capability?.def : undefined) ?? this.checked.project.scopes.get(method.span.file)?.get(effect.source);
|
|
573
|
-
const operation = target && 'methods' in target.node ? target.node.methods.find(method => method.name === effect.operation) : undefined;
|
|
574
|
-
this.use(target, operation);
|
|
575
|
-
return target ? this.link(target, operation ? `${target.name}.${operation.name}` : target.name, `${effect.source}.${effect.operation}`) : code(`${effect.source}.${effect.operation}`);
|
|
576
|
-
}).join(', ');
|
|
577
|
-
if (errors.length)
|
|
578
|
-
text += '; can fail with ' + errors.map(code).join(', ');
|
|
579
|
-
return text + '.\n';
|
|
580
|
-
}
|
|
581
|
-
fileOverview() {
|
|
582
|
-
const lines = [];
|
|
583
|
-
const declaration = (name) => `[${code(name)}](${url(basename(this.docs.get(this.file.path)))}#${encodeURIComponent(anchor(name))})`;
|
|
584
|
-
for (const item of this.file.items) {
|
|
585
|
-
switch (item.kind) {
|
|
586
|
-
case 'class':
|
|
587
|
-
lines.push(`- ${declaration(item.name)} is ${item.record ? 'an immutable record' : 'a class'}${item.implements.length ? ' implementing ' + item.implements.map(type => code(typeName(type))).join(', ') : ''}.`);
|
|
588
|
-
break;
|
|
589
|
-
case 'interface':
|
|
590
|
-
lines.push(`- ${declaration(item.name)} is ${item.capability ? 'a capability interface' : 'an interface'}.`);
|
|
591
|
-
break;
|
|
592
|
-
case 'interceptor':
|
|
593
|
-
lines.push(`- ${declaration(item.name)} is an interceptor.`);
|
|
594
|
-
break;
|
|
595
|
-
case 'function':
|
|
596
|
-
lines.push(`- ${declaration(item.name)} ${item.endpoint ? `handles ${code(item.endpoint.method)} ${code(item.endpoint.path)}` : 'is a function'}${item.returns.name === 'void' ? '' : ` returning ${code(typeName(item.returns))}`}.`);
|
|
597
|
-
break;
|
|
598
|
-
case 'composition':
|
|
599
|
-
lines.push(`- ${declaration(item.name)} declares ${item.bindings.length} ${item.bindings.length === 1 ? 'provider' : 'providers'}.`);
|
|
600
|
-
break;
|
|
601
|
-
case 'bind': break;
|
|
602
|
-
case 'include':
|
|
603
|
-
lines.push(`- Include providers from ${code(item.name)}.`);
|
|
604
|
-
break;
|
|
605
|
-
case 'export':
|
|
606
|
-
lines.push(`- Export ${code(item.name)} from this folder.`);
|
|
607
|
-
break;
|
|
608
|
-
case 'test':
|
|
609
|
-
lines.push(`- ${declaration(this.testTitle(item))} is a same-file test suite.`);
|
|
610
|
-
break;
|
|
611
|
-
default: break;
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
const providers = this.file.items.filter(item => item.kind === 'bind');
|
|
615
|
-
if (providers.length)
|
|
616
|
-
lines.push(`- Register ${providers.length} ${providers.length === 1 ? 'dependency provider' : 'dependency providers'} before startup.`);
|
|
617
|
-
const executable = this.file.items.filter(item => !['import', 'export', 'test', 'bind', 'include', 'class', 'interface', 'interceptor', 'function', 'composition'].includes(item.kind));
|
|
618
|
-
if (executable.some(item => item.kind === 'try'))
|
|
619
|
-
lines.push('- Run startup operations with checked error recovery.');
|
|
620
|
-
for (const item of executable)
|
|
621
|
-
if (item.kind === 'serve')
|
|
622
|
-
lines.push(`- Serve ${item.names.length} HTTP ${item.names.length === 1 ? 'route' : 'routes'}.`);
|
|
623
|
-
const otherSteps = executable.filter(item => item.kind !== 'try' && item.kind !== 'serve').length;
|
|
624
|
-
if (otherSteps)
|
|
625
|
-
lines.push(`- Run ${otherSteps} other startup ${otherSteps === 1 ? 'step' : 'steps'} in source order.`);
|
|
626
|
-
return lines.length ? '## In this file\n\n' + lines.join('\n') + '\n\n' : '';
|
|
931
|
+
return section('Dependencies', 2, [...[...groups].map(([origin, names]) => paragraph('It uses ' + coordinate(names) + (origin ? ' from ' + origin : '') + '.'))]);
|
|
627
932
|
}
|
|
628
933
|
render() {
|
|
629
934
|
const exports = this.file.items.filter(item => item.kind === 'export');
|
|
630
935
|
const declarations = this.file.items.filter(item => ['class', 'interface', 'interceptor', 'function', 'composition'].includes(item.kind));
|
|
631
936
|
const providers = this.file.items.filter(item => item.kind === 'bind' || item.kind === 'include');
|
|
632
937
|
const startup = this.file.items.filter(item => item.kind !== 'import' && item.kind !== 'export' && item.kind !== 'test' && item.kind !== 'bind' && item.kind !== 'include' && !declarations.includes(item));
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
(
|
|
636
|
-
(startup.length ? '## Startup, in source order\n\n' + startup.map(item => this.declaration(item)).join('') + '\n' : '') +
|
|
637
|
-
declarations.sort((a, b) => Number('name' in a && a.name.startsWith('_')) - Number('name' in b && b.name.startsWith('_'))).map(item => this.declaration(item)).join('') +
|
|
638
|
-
this.file.items.filter(item => item.kind === 'test').map(item => this.declaration(item)).join('');
|
|
639
|
-
const surface = this.dependencySurface() + this.builtinSurface();
|
|
640
|
-
let config = '';
|
|
938
|
+
const children = [];
|
|
939
|
+
if (declarations.some(item => item.kind === 'function' && item.endpoint))
|
|
940
|
+
children.push(paragraph('Plain handler results default to HTTP 200 unless another status is declared. HttpResponse values choose their own status. Unhandled request failures return HTTP 500 and cancel the request tasks.'));
|
|
641
941
|
if (basename(this.file.path) === 'main.aug') {
|
|
642
942
|
const project = this.checked.project;
|
|
643
943
|
if (project.files.get(this.file.path)?.items.some(item => item.kind === 'serve'))
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
(
|
|
654
|
-
|
|
944
|
+
children.push(section('HTTP configuration', 2, [paragraph(`Listen on ${code(project.config.web.host)}. Limit request bodies to ${project.config.web.body_limit} bytes and buffered responses to ${project.config.web.response_limit} bytes. ` +
|
|
945
|
+
(project.config.web.tls.certificate ? 'Use TLS. ' : '') +
|
|
946
|
+
(project.config.web.http3 ? 'Enable HTTP/3. ' : '') +
|
|
947
|
+
(project.config.openapi.enabled ? `Serve OpenAPI at ${code(project.config.openapi.path)} and API docs at ${code(project.config.openapi.docs)}. ` : ''))]));
|
|
948
|
+
}
|
|
949
|
+
// Build the checked explanation first; rendering decides all spacing.
|
|
950
|
+
if (exports.length)
|
|
951
|
+
children.push(section('Exports', 2, exports.map(item => paragraph(this.exportLine(item)))));
|
|
952
|
+
if (providers.length)
|
|
953
|
+
children.push(section('Providers', 2, providers.map(item => paragraph(this.providerLine(item)))));
|
|
954
|
+
if (startup.length)
|
|
955
|
+
children.push(section('Startup', 2, [flow(startup.flatMap(item => this.statement(item)))]));
|
|
956
|
+
children.push(...declarations.sort((a, b) => Number('name' in a && a.name.startsWith('_')) - Number('name' in b && b.name.startsWith('_'))).map(item => this.declaration(item)));
|
|
957
|
+
children.push(...this.file.items.filter(item => item.kind === 'test').map(item => this.declaration(item)));
|
|
958
|
+
if (!children.length)
|
|
959
|
+
children.push(paragraph('This file declares no operations.'));
|
|
960
|
+
// Links may add inherited operation owners. Walk until the surface is closed.
|
|
961
|
+
const surfaceSize = () => [...this.used.values()].reduce((size, entry) => size + 1 + entry.operations.size + entry.fields.size + Number(entry.constructed ?? false), 0);
|
|
962
|
+
let surface, size = -1;
|
|
963
|
+
do {
|
|
964
|
+
size = surfaceSize();
|
|
965
|
+
surface = this.dependencySurface();
|
|
966
|
+
} while (surfaceSize() !== size);
|
|
967
|
+
if (surface)
|
|
968
|
+
children.push(surface);
|
|
969
|
+
const builtins = this.builtinSurface();
|
|
970
|
+
if (builtins)
|
|
971
|
+
children.push(builtins);
|
|
972
|
+
return generated + '\n\n' + renderSpecTree(section(code(basename(this.file.path)), 1, children));
|
|
655
973
|
}
|
|
656
974
|
builtinSurface() {
|
|
657
975
|
if (!this.builtins.size && !this.properties.size)
|
|
658
|
-
return
|
|
659
|
-
return '
|
|
660
|
-
[...this.builtins].sort(([a], [b]) => compare(a, b)).map(([name, operation]) => `- ${code(name)} (${operation.parameters.map(param => code(param.label) + ': ' + code(param.type)).join(', ') || 'no inputs'}) → ${code(operation.returns)}: ${operation.documentation}` +
|
|
661
|
-
(operation.changes ? ' Changes the receiver.' : '') +
|
|
662
|
-
(operation.errors?.length ? ' Can fail with ' + operation.errors.map(code).join(', ') + '.' : '') + '\n').join('') +
|
|
663
|
-
'\n[Full built-in reference](https://greenpandastudios.github.io/augscript/language-constructs).\n\n';
|
|
976
|
+
return;
|
|
977
|
+
return paragraph('Built-in operations follow the [language reference](https://greenpandastudios.github.io/augscript/language-constructs).');
|
|
664
978
|
}
|
|
665
979
|
}
|
|
666
|
-
/**
|
|
980
|
+
/** Refresh pointers and artifacts atomically, or check drift without writing; protect handwritten documents. */
|
|
667
981
|
export function updateSpecs(checked, check = false, options = {}) {
|
|
668
982
|
const outputs = generateSpecs(checked, options), root = checked.project.root;
|
|
669
983
|
const manifest = join(root, '.aug-spec', 'manifest.json');
|
|
@@ -674,11 +988,12 @@ export function updateSpecs(checked, check = false, options = {}) {
|
|
|
674
988
|
throw new Error('Invalid generated specification manifest');
|
|
675
989
|
previous = saved.files.map(path => resolve(root, path));
|
|
676
990
|
}
|
|
677
|
-
const
|
|
991
|
+
const artifacts = outputs.filter(output => output.kind !== 'source-hint');
|
|
992
|
+
const names = new Set(artifacts.map(output => output.path));
|
|
678
993
|
const removed = previous.filter(path => !names.has(path) && existsSync(path));
|
|
679
994
|
const stale = outputs.filter(output => !existsSync(output.path) || readFileSync(output.path, 'utf8') !== output.text).map(output => relative(root, output.path));
|
|
680
995
|
stale.push(...removed.map(path => relative(root, path)));
|
|
681
|
-
const manifestText = JSON.stringify({ format: 1, files:
|
|
996
|
+
const manifestText = JSON.stringify({ format: 1, files: artifacts.map(output => relative(root, output.path).replaceAll('\\', '/')), digest: hash(artifacts.map(output => output.text).join('\0')) }, null, 2) + '\n';
|
|
682
997
|
if (options.manifest !== false && (!existsSync(manifest) || readFileSync(manifest, 'utf8') !== manifestText))
|
|
683
998
|
stale.push('.aug-spec/manifest.json');
|
|
684
999
|
if (check)
|
|
@@ -697,7 +1012,11 @@ export function updateSpecs(checked, check = false, options = {}) {
|
|
|
697
1012
|
if (lstatSync(output.path).isSymbolicLink())
|
|
698
1013
|
throw new Error(`Specification output is a symbolic link: ${output.path}`);
|
|
699
1014
|
const text = readFileSync(output.path, 'utf8');
|
|
700
|
-
if (
|
|
1015
|
+
if (output.kind === 'source-hint') {
|
|
1016
|
+
if (text !== checked.project.files.get(output.source)?.source)
|
|
1017
|
+
throw new Error('Source changed during compilation; retry before generating specifications');
|
|
1018
|
+
}
|
|
1019
|
+
else if (!text.startsWith(generated) && !text.startsWith(copied))
|
|
701
1020
|
throw new Error(`Refusing to overwrite handwritten file ${output.path}`);
|
|
702
1021
|
}
|
|
703
1022
|
}
|
|
@@ -706,12 +1025,14 @@ export function updateSpecs(checked, check = false, options = {}) {
|
|
|
706
1025
|
continue;
|
|
707
1026
|
mkdirSync(dirname(output.path), { recursive: true });
|
|
708
1027
|
const temporary = output.path + '.aug-spec-tmp';
|
|
1028
|
+
let created = false;
|
|
709
1029
|
try {
|
|
710
|
-
writeFileSync(temporary, output.text, { flag: 'wx' });
|
|
1030
|
+
writeFileSync(temporary, output.text, { flag: 'wx', mode: output.kind === 'source-hint' ? lstatSync(output.path).mode : undefined });
|
|
1031
|
+
created = true;
|
|
711
1032
|
renameSync(temporary, output.path);
|
|
712
1033
|
}
|
|
713
1034
|
finally {
|
|
714
|
-
if (existsSync(temporary))
|
|
1035
|
+
if (created && existsSync(temporary))
|
|
715
1036
|
rmSync(temporary);
|
|
716
1037
|
}
|
|
717
1038
|
}
|
|
@@ -724,12 +1045,14 @@ export function updateSpecs(checked, check = false, options = {}) {
|
|
|
724
1045
|
mkdirSync(dirname(manifest), { recursive: true });
|
|
725
1046
|
if (!existsSync(manifest) || readFileSync(manifest, 'utf8') !== manifestText) {
|
|
726
1047
|
const temporary = manifest + '.aug-spec-tmp';
|
|
1048
|
+
let created = false;
|
|
727
1049
|
try {
|
|
728
1050
|
writeFileSync(temporary, manifestText, { flag: 'wx' });
|
|
1051
|
+
created = true;
|
|
729
1052
|
renameSync(temporary, manifest);
|
|
730
1053
|
}
|
|
731
1054
|
finally {
|
|
732
|
-
if (existsSync(temporary))
|
|
1055
|
+
if (created && existsSync(temporary))
|
|
733
1056
|
rmSync(temporary);
|
|
734
1057
|
}
|
|
735
1058
|
}
|