Compare commits
37 Commits
1.0.0-beta
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b241a8a812 | ||
|
|
05ff5be5c5 | ||
|
|
7f71beac4c | ||
|
|
87e627045e | ||
|
|
c1231fdb5f | ||
|
|
98f81d2d59 | ||
|
|
3b231b7f21 | ||
|
|
1185a1fa18 | ||
|
|
ddb8afd904 | ||
|
|
436261730a | ||
|
|
d3e628553f | ||
|
|
acbe58f70e | ||
|
|
dd5302507d | ||
|
|
87f90ba264 | ||
|
|
f8f0862ee3 | ||
|
|
5a3ed0c41e | ||
|
|
4f52a54bea | ||
|
|
be6cdfb87c | ||
|
|
2726ec4915 | ||
|
|
9956b1d874 | ||
|
|
3dfd465e8d | ||
|
|
60d1d0f8a3 | ||
|
|
0337974923 | ||
|
|
0c612ad7fd | ||
|
|
33668a0793 | ||
|
|
4ba0a24b14 | ||
|
|
cda20bc895 | ||
|
|
bcfc8d7b68 | ||
|
|
25bad3cb8b | ||
|
|
0f90771fdd | ||
|
|
7e628995a1 | ||
|
|
bbcdf01ce3 | ||
|
|
7d081b2fad | ||
|
|
9839452476 | ||
|
|
5180f69c4c | ||
|
|
ee2b8e0930 | ||
|
|
d83e20b062 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -14,6 +14,7 @@ compiler/target/
|
||||
# Benchmark and local runtime artifacts
|
||||
benchmarks/**/build/
|
||||
compiler/glagol-std-layout-local-fs-alpha.txt
|
||||
**/.slovo/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
|
||||
62
.llm/BETA_10_DEVELOPER_EXPERIENCE_API_DISCOVERY.md
Normal file
62
.llm/BETA_10_DEVELOPER_EXPERIENCE_API_DISCOVERY.md
Normal file
@ -0,0 +1,62 @@
|
||||
# 1.0.0-beta.10 Developer Experience API Discovery
|
||||
|
||||
Status: release scope for `1.0.0-beta.10`.
|
||||
|
||||
`1.0.0-beta.10` is a tooling/docs slice on top of the beta.8 concrete alias
|
||||
foundation and beta.9 collection alias unification work. It improves API
|
||||
discovery for the existing source-authored standard library and adds
|
||||
editor-facing source metadata without adding new source-language execution
|
||||
semantics, compiler-known runtime names, or runtime helpers.
|
||||
|
||||
## Scope
|
||||
|
||||
- Upgrade `scripts/render-stdlib-api-doc.js` so the generated
|
||||
`docs/language/STDLIB_API.md` catalog lists exact exported helper
|
||||
signatures, not only helper names.
|
||||
- Parse each `lib/std/*.slo` module, collect module-local `(type ...)`
|
||||
aliases, and normalize those aliases recursively in public helper
|
||||
signatures.
|
||||
- Verify exported helper names have matching `(fn ...)` forms.
|
||||
- Omit non-exported helper functions and `(type ...)` aliases from the public
|
||||
catalog.
|
||||
- Regenerate `docs/language/STDLIB_API.md`.
|
||||
- Add `glagol symbols <file.slo|project|workspace>` for deterministic
|
||||
`slovo.symbols` S-expression metadata over modules, imports, exports,
|
||||
aliases, structs, enums, functions, tests, spans/ranges, and workspace
|
||||
package names.
|
||||
- Update README, language docs, compiler docs, and the post-beta roadmap to
|
||||
describe beta API discovery clearly.
|
||||
|
||||
## Public Contract
|
||||
|
||||
The generated catalog is a beta discovery aid for the current `lib/std`
|
||||
surface. Public signatures show concrete types such as `(vec i32)`,
|
||||
`(option string)`, and `(result u64 i32)` instead of module-local alias names
|
||||
such as `VecI32`, `OptionString`, or `ResultU64`.
|
||||
|
||||
The catalog remains generated from source and is not a hand-maintained API
|
||||
freeze. It can help reviewers see current helper signatures, but it does not
|
||||
make those helpers stable `1.0.0` standard-library APIs.
|
||||
|
||||
The `symbols` command is an editor-integration building block, not an LSP
|
||||
server. Its output is deterministic machine-readable S-expression text and
|
||||
uses the beta10 `slovo.symbols` schema label.
|
||||
|
||||
## Explicit Non-Scope
|
||||
|
||||
- no executable generics
|
||||
- no generic aliases or parameterized aliases
|
||||
- no maps or sets
|
||||
- no traits, inference, monomorphization, or iterators
|
||||
- no new compiler-known runtime names
|
||||
- no runtime helper or ABI/layout changes
|
||||
- no LSP server, watch mode, SARIF, or daemon protocol
|
||||
- no stable `1.0.0` standard-library freeze
|
||||
|
||||
## Checks
|
||||
|
||||
Focused checks for this slice:
|
||||
|
||||
- `node scripts/render-stdlib-api-doc.js`
|
||||
- `cargo test --test symbols_beta10`
|
||||
- `git diff --check -- scripts/render-stdlib-api-doc.js docs/language/STDLIB_API.md compiler/src/main.rs compiler/src/symbols.rs compiler/tests/symbols_beta10.rs README.md docs/language/SPEC-v1.md docs/language/ROADMAP.md docs/language/RELEASE_NOTES.md docs/compiler/ROADMAP.md docs/compiler/RELEASE_NOTES.md docs/POST_BETA_ROADMAP.md .llm/BETA_10_DEVELOPER_EXPERIENCE_API_DISCOVERY.md`
|
||||
57
.llm/BETA_11_LOCAL_PACKAGE_API_DOCUMENTATION.md
Normal file
57
.llm/BETA_11_LOCAL_PACKAGE_API_DOCUMENTATION.md
Normal file
@ -0,0 +1,57 @@
|
||||
# 1.0.0-beta.11 Local Package API Documentation
|
||||
|
||||
Status: release scope for `1.0.0-beta.11`.
|
||||
|
||||
`1.0.0-beta.11` extends the beta.10 API discovery lane. The release keeps the
|
||||
`1.0.0-beta` source-language and runtime baseline unchanged while making local
|
||||
package and module documentation show the public API surface users need to
|
||||
review.
|
||||
|
||||
## Scope
|
||||
|
||||
- Extend `glagol doc <file|project|workspace> -o <dir>` so generated Markdown
|
||||
includes deterministic exported/public API sections for local source files,
|
||||
projects, packages, and workspaces.
|
||||
- Render exact exported function signatures with parameter names, parameter
|
||||
types, and return types.
|
||||
- Render exported struct field names and field types.
|
||||
- Render exported enum variant names and payload types for payloadless and
|
||||
current single-payload variants.
|
||||
- Keep non-exported functions, structs, enums, tests, and `(type ...)` aliases
|
||||
out of the public API sections.
|
||||
- Normalize module-local concrete aliases before rendering public types, so
|
||||
private names such as `VecI32`, `OptionString`, or `ResultU64` do not leak
|
||||
into local package/module public docs.
|
||||
- Update README, language docs, compiler docs, and the post-beta roadmap to
|
||||
describe the beta11 documentation contract clearly.
|
||||
|
||||
## Public Contract
|
||||
|
||||
The generated local documentation is a beta API discovery aid. It exposes what
|
||||
the current local module/package export lists make public, with concrete public
|
||||
types after alias normalization.
|
||||
|
||||
The public API sections are deterministic and suitable for human review, but
|
||||
they are not a stable machine-readable Markdown schema. Headings, anchors, file
|
||||
names, and surrounding prose remain beta-scoped unless a later release freezes
|
||||
them explicitly.
|
||||
|
||||
## Explicit Non-Scope
|
||||
|
||||
- no stable Markdown schema
|
||||
- no stable stdlib/API compatibility freeze
|
||||
- no LSP server or watch mode
|
||||
- no SARIF or daemon protocol
|
||||
- no diagnostics schema policy
|
||||
- no executable generics
|
||||
- no maps or sets
|
||||
- no re-exports, glob imports, or hierarchical modules
|
||||
- no package registry semantics
|
||||
- no new compiler-known runtime names
|
||||
- no runtime helper or ABI/layout changes
|
||||
|
||||
## Checks
|
||||
|
||||
Focused checks for this slice:
|
||||
|
||||
- `git diff --check -- README.md docs/POST_BETA_ROADMAP.md docs/language/ROADMAP.md docs/language/RELEASE_NOTES.md docs/language/SPEC-v1.md docs/compiler/ROADMAP.md docs/compiler/RELEASE_NOTES.md .llm/BETA_11_LOCAL_PACKAGE_API_DOCUMENTATION.md`
|
||||
59
.llm/BETA_12_CONCRETE_VECTOR_QUERY_AND_PREFIX_PARITY.md
Normal file
59
.llm/BETA_12_CONCRETE_VECTOR_QUERY_AND_PREFIX_PARITY.md
Normal file
@ -0,0 +1,59 @@
|
||||
# 1.0.0-beta.12 Concrete Vector Query And Prefix Parity
|
||||
|
||||
Status: release scope for `1.0.0-beta.12`.
|
||||
|
||||
`1.0.0-beta.12` is a source-authored standard-library/helper parity release for
|
||||
current concrete vectors. It keeps the `1.0.0-beta` source language, typed core,
|
||||
runtime, ABI/layout, and compiler-known `std.vec.*` runtime names unchanged.
|
||||
|
||||
## Scope
|
||||
|
||||
- Add `count_of`, `starts_with`, `without_prefix`, `ends_with`, and
|
||||
`without_suffix` to `std.vec_i64`.
|
||||
- Add `count_of` to `std.vec_f64`.
|
||||
- Keep helpers ordinary Slovo source over the existing concrete vector runtime
|
||||
names, equality, `len`, `at`, and already staged recursive helpers.
|
||||
- Extend explicit source-helper project coverage for repeated `count_of`
|
||||
results and prefix/suffix empty, mismatch, exact, and longer-than-input
|
||||
cases where applicable.
|
||||
- Bump the Glagol package version to `1.0.0-beta.12`.
|
||||
- Update README, language docs, compiler docs, and the post-beta roadmap.
|
||||
|
||||
## Public Contract
|
||||
|
||||
The helper additions are concrete-family helpers:
|
||||
|
||||
- `std.vec_i64.count_of(values,target)` counts `i64` elements equal to
|
||||
`target`.
|
||||
- `std.vec_i64.starts_with(values,prefix)` and
|
||||
`std.vec_i64.ends_with(values,suffix)` treat empty prefixes/suffixes and
|
||||
exact full-vector matches as true, reject mismatches, and reject longer
|
||||
prefix/suffix inputs.
|
||||
- `std.vec_i64.without_prefix(values,prefix)` and
|
||||
`std.vec_i64.without_suffix(values,suffix)` return the unmatched original
|
||||
vector or the remaining vector after a match.
|
||||
- `std.vec_f64.count_of(values,target)` counts `f64` elements equal to
|
||||
`target`.
|
||||
|
||||
## Explicit Non-Scope
|
||||
|
||||
- no source-language syntax change
|
||||
- no typed-core or lowering change
|
||||
- no runtime implementation change
|
||||
- no new compiler-known stdlib or runtime names
|
||||
- no executable generics or generic stdlib dispatch
|
||||
- no maps or sets
|
||||
- no iterators
|
||||
- no mutable vector operations
|
||||
- no slice/view APIs
|
||||
- no ABI/layout stability promise
|
||||
- no performance claim
|
||||
- no stable stdlib/API freeze
|
||||
|
||||
## Checks
|
||||
|
||||
Focused checks for this slice:
|
||||
|
||||
- `cargo test --manifest-path compiler/Cargo.toml --test standard_vec_i64_source_helpers_alpha --test standard_vec_f64_source_helpers_alpha`
|
||||
- `cargo fmt --manifest-path compiler/Cargo.toml --check`
|
||||
- `git diff --check -- compiler/tests/standard_vec_i64_source_helpers_alpha.rs compiler/tests/standard_vec_f64_source_helpers_alpha.rs compiler/Cargo.toml compiler/Cargo.lock README.md docs/POST_BETA_ROADMAP.md docs/language/ROADMAP.md docs/language/RELEASE_NOTES.md docs/language/SPEC-v1.md docs/compiler/ROADMAP.md docs/compiler/RELEASE_NOTES.md .llm/BETA_12_CONCRETE_VECTOR_QUERY_AND_PREFIX_PARITY.md`
|
||||
66
.llm/BETA_13_DIAGNOSTIC_CATALOG_AND_SCHEMA_POLICY.md
Normal file
66
.llm/BETA_13_DIAGNOSTIC_CATALOG_AND_SCHEMA_POLICY.md
Normal file
@ -0,0 +1,66 @@
|
||||
# 1.0.0-beta.13 Diagnostic Catalog And Schema Policy
|
||||
|
||||
Status: release scope for `1.0.0-beta.13`.
|
||||
|
||||
`1.0.0-beta.13` is a docs/tooling-only policy slice for the existing
|
||||
diagnostic surface. It keeps the `1.0.0-beta` source language, typed core,
|
||||
runtime, standard library, compiler CLI, diagnostic output shape, ABI/layout,
|
||||
and compiler-known runtime names unchanged.
|
||||
|
||||
## Scope
|
||||
|
||||
- Add `docs/language/DIAGNOSTICS.md` as the beta policy for
|
||||
`slovo.diagnostic` version `1`.
|
||||
- Document the S-expression and JSON relationship.
|
||||
- Document required and optional fields, severity/source/range/related-span
|
||||
semantics, JSON-line discipline, source-less diagnostics, and
|
||||
artifact-manifest diagnostic metadata.
|
||||
- Define diagnostic compatibility and migration classes.
|
||||
- Inventory the current diagnostic codes covered by
|
||||
`compiler/tests/diagnostics_contract.rs` and the matching `.diag` snapshots.
|
||||
- Update README, language roadmap, language spec, release notes, migration
|
||||
policy, and post-beta roadmap to introduce beta13 and link the diagnostics
|
||||
policy.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- `docs/language/DIAGNOSTICS.md` names schema `slovo.diagnostic` and version
|
||||
`1`.
|
||||
- The document is clear that S-expression diagnostics and JSON diagnostics are
|
||||
encodings of the same data model.
|
||||
- JSON diagnostics are documented as one object per line on stderr, without an
|
||||
array wrapper or pretty-printing requirement.
|
||||
- Source-less diagnostics are documented without inventing fake source spans.
|
||||
- Artifact manifests are documented as carrying diagnostic schema version,
|
||||
diagnostic encoding, and diagnostic stream metadata, not a second diagnostic
|
||||
schema.
|
||||
- Human prose is documented as beta-flexible, while machine fields, codes,
|
||||
schema/version markers, ranges, JSON-line discipline, and golden fixture
|
||||
shape are compatibility-sensitive.
|
||||
- The code catalog is concise and derived from the current golden diagnostics
|
||||
contract.
|
||||
|
||||
## Explicit Non-Scope
|
||||
|
||||
- no source-language syntax change
|
||||
- no typed-core, lowering, runtime, stdlib, or ABI/layout change
|
||||
- no diagnostic-output shape change
|
||||
- no LSP server, watch mode, SARIF, daemon protocol, or debug adapter
|
||||
- no stable Markdown schema
|
||||
- no stable `1.0.0` diagnostics freeze
|
||||
- no source-map, DWARF, or LLVM debug metadata contract
|
||||
- no compiler-emitted diagnostic catalog artifact
|
||||
- no release publication, tag, push, or version publication work
|
||||
|
||||
## Expected Controller Verification
|
||||
|
||||
- Review the policy for consistency with `compiler/src/diag.rs`,
|
||||
`compiler/src/main.rs`, `compiler/tests/diagnostics_contract.rs`, and the
|
||||
current `.diag` snapshots.
|
||||
- Confirm the catalog count matches the current contract: 358 snapshots and
|
||||
114 unique diagnostic codes.
|
||||
- Run lightweight docs checks:
|
||||
- `git diff --check -- README.md docs/POST_BETA_ROADMAP.md docs/language/DIAGNOSTICS.md docs/language/MIGRATION_POLICY.md docs/language/SPEC-v1.md docs/language/ROADMAP.md docs/language/RELEASE_NOTES.md .llm/BETA_13_DIAGNOSTIC_CATALOG_AND_SCHEMA_POLICY.md`
|
||||
- run an `rg` check for stale beta12-only current-stage phrasing across the
|
||||
same touched docs
|
||||
- Do not commit, tag, push, or run release publication from this worker scope.
|
||||
69
.llm/BETA_14_BENCHMARK_SUITE_CATALOG_AND_METADATA_GATE.md
Normal file
69
.llm/BETA_14_BENCHMARK_SUITE_CATALOG_AND_METADATA_GATE.md
Normal file
@ -0,0 +1,69 @@
|
||||
# 1.0.0-beta.14 Benchmark Suite Catalog And Metadata Gate
|
||||
|
||||
Status: release scope for `1.0.0-beta.14`.
|
||||
|
||||
`1.0.0-beta.14` is a docs/tooling metadata slice for the existing benchmark
|
||||
suite. It keeps the `1.0.0-beta` source language, typed core, runtime,
|
||||
standard library, API surface, compiler diagnostics, diagnostic output shape,
|
||||
ABI/layout behavior, and compiler-known runtime names unchanged.
|
||||
|
||||
## Scope
|
||||
|
||||
- Add `benchmarks/README.md` as the top-level benchmark suite catalog.
|
||||
- Document `python3 benchmarks/runner.py --suite-list` as the non-JSON suite
|
||||
inventory command.
|
||||
- Document `python3 benchmarks/runner.py --suite-list --json` as the beta
|
||||
tooling metadata form for the same inventory.
|
||||
- Record the current suite inventory:
|
||||
`math-loop`, `branch-loop`, `parse-loop`, `array-index-loop`,
|
||||
`string-eq-loop`, `array-struct-field-loop`,
|
||||
`enum-struct-payload-loop`, `vec-i32-index-loop`,
|
||||
`vec-string-eq-loop`, and `json-quote-loop`.
|
||||
- Document that benchmark timings are local-machine evidence only.
|
||||
- Update README, language roadmap, language spec, release notes, post-beta
|
||||
roadmap, and this release contract to introduce beta14.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Public docs name current release/stage `1.0.0-beta.14`.
|
||||
- `benchmarks/README.md` explains both root suite-list commands:
|
||||
`python3 benchmarks/runner.py --suite-list` and
|
||||
`python3 benchmarks/runner.py --suite-list --json`.
|
||||
- The suite catalog lists the current benchmark suite inventory without adding
|
||||
or removing benchmark kernels.
|
||||
- The suite catalog verifies required scaffold files for each suite:
|
||||
`benchmark.json`, `run.py`, `slovo.toml`, and `src/main.slo`.
|
||||
- The docs say timing output is local-machine evidence only and do not publish
|
||||
timing numbers.
|
||||
- The JSON suite listing is described as beta tooling metadata, not a stable
|
||||
public schema.
|
||||
- Explicit exclusions include no new benchmark kernels, no timing publication,
|
||||
no performance thresholds, no stable JSON schema, no source-language change,
|
||||
no runtime change, no stdlib/API change, no diagnostic-output change, and no
|
||||
ABI/layout change.
|
||||
|
||||
## Explicit Non-Scope
|
||||
|
||||
- no new benchmark kernels or implementation language slots
|
||||
- no benchmark timing publication
|
||||
- no release performance threshold
|
||||
- no stable JSON schema or stable benchmark metadata compatibility promise
|
||||
- no source-language syntax, typed-core, lowering, runtime, stdlib, or API
|
||||
change
|
||||
- no compiler diagnostic or diagnostic-output shape change
|
||||
- no ABI/layout or optimizer guarantee
|
||||
- no worker-owned release publication work before controller review and gates
|
||||
|
||||
## Expected Controller Verification
|
||||
|
||||
- Run lightweight docs checks:
|
||||
- `git diff --check -- README.md benchmarks/README.md docs/POST_BETA_ROADMAP.md docs/language/SPEC-v1.md docs/language/ROADMAP.md docs/language/RELEASE_NOTES.md .llm/BETA_14_BENCHMARK_SUITE_CATALOG_AND_METADATA_GATE.md`
|
||||
- run an `rg` check for stale beta13-only current-stage phrasing across the
|
||||
same touched docs
|
||||
- Smoke the metadata commands:
|
||||
- `python3 benchmarks/runner.py --suite-list`
|
||||
- `python3 benchmarks/runner.py --suite-list --json`
|
||||
- Run focused compiler/tooling checks:
|
||||
- `cargo test --test benchmark_suite_catalog_beta14`
|
||||
- `cargo test --test benchmark_math_loop_scaffold`
|
||||
- Do not commit, tag, push, or run release publication from this worker scope.
|
||||
@ -0,0 +1,92 @@
|
||||
# 1.0.0-beta.15 Reserved Generic Collection Boundary Hardening And Collection Ledger
|
||||
|
||||
Status: release scope for `1.0.0-beta.15`.
|
||||
|
||||
`1.0.0-beta.15` is a docs/design and compiler-boundary hardening slice for
|
||||
the existing concrete collection and value-family surface. It keeps the
|
||||
`1.0.0-beta` source language, typed core, runtime, standard-library/API
|
||||
surface, diagnostic output shape, diagnostic codes, diagnostic schema,
|
||||
benchmark metadata schema, ABI/layout behavior, compiler-known runtime names,
|
||||
and performance claims unchanged while rewording reserved generic/map/set
|
||||
diagnostic prose away from beta.9-specific text.
|
||||
|
||||
## Scope
|
||||
|
||||
- Add `docs/language/COLLECTIONS.md` as the collection/value-family ledger.
|
||||
- Inventory the current concrete vector, option, result, and related
|
||||
option/result-returning facade surfaces by linking to
|
||||
`docs/language/STDLIB_API.md` instead of duplicating generated counts.
|
||||
- Record design pressure from duplicated concrete vector, option, and result
|
||||
helper families.
|
||||
- Define prerequisites before executable generics, generic aliases, maps,
|
||||
sets, iterators, mutable vectors, and slice/view APIs can be promoted.
|
||||
- Record current unsupported diagnostics as boundary evidence, not behavior
|
||||
changes.
|
||||
- Centralize reserved generic/collection diagnostics for lowerer, formatter,
|
||||
and checker paths in `compiler/src/reserved.rs`.
|
||||
- Reword affected reserved-boundary snapshots from `beta.9` to current-beta
|
||||
wording while preserving codes, schema, spans, expected/found values, hints,
|
||||
and output shape.
|
||||
- Add focused `reserved_generic_collection_beta15` coverage and run it from
|
||||
`scripts/release-gate.sh`.
|
||||
- Update README, post-beta roadmap, language/compiler roadmaps, language spec,
|
||||
release notes, compiler release notes, and this release contract to introduce
|
||||
beta15.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Public docs name current release/stage `1.0.0-beta.15`.
|
||||
- `docs/language/COLLECTIONS.md` links to `docs/language/STDLIB_API.md` for
|
||||
exact public helper signatures and does not duplicate generated helper
|
||||
counts.
|
||||
- The ledger inventories the current concrete collection/value-family surface:
|
||||
five concrete vector modules, concrete option families, concrete result
|
||||
families, and option/result-returning host/parsing facades.
|
||||
- The ledger records why repeated concrete vector/option/result facades create
|
||||
design pressure for later generic work.
|
||||
- The ledger defines promotion prerequisites for executable generics, generic
|
||||
aliases, maps, sets, iterators, mutable vectors, and slice/view APIs.
|
||||
- The docs state that current unsupported diagnostics are boundaries, that
|
||||
beta15 rewords only reserved-boundary diagnostic prose, and that beta15 does
|
||||
not change diagnostic output shape, codes, schema, spans, expected/found
|
||||
values, or hints.
|
||||
- Focused compiler tests prove `check`, `fmt --check`, and project-root
|
||||
`check` reject reserved generic/map/set surfaces with stage-neutral current
|
||||
beta wording.
|
||||
- Explicit exclusions include no source-language change, no runtime change, no
|
||||
stdlib/API surface change, no diagnostic output shape/code/schema change, no
|
||||
benchmark metadata schema change, no ABI/layout change, no performance
|
||||
claim, and no stable API freeze.
|
||||
|
||||
## Explicit Non-Scope
|
||||
|
||||
- no executable generics, traits, inference, monomorphization, or generic
|
||||
stdlib dispatch
|
||||
- no generic aliases or parameterized aliases
|
||||
- no map or set semantics
|
||||
- no iterator API
|
||||
- no mutable vector API
|
||||
- no slice/view API
|
||||
- no new compiler-known runtime names or helper symbols
|
||||
- no standard-library/API addition, removal, rename, or stable freeze
|
||||
- no diagnostic output shape, code, schema, span, expected/found, or hint change
|
||||
- no diagnostic policy change beyond reserved-boundary prose rewording
|
||||
- no benchmark metadata schema change
|
||||
- no source-language syntax, typed-core, lowering, runtime, ABI/layout, or
|
||||
optimizer change
|
||||
- no performance threshold or cross-machine performance claim
|
||||
- no worker-owned release publication work before controller review and gates
|
||||
|
||||
## Expected Controller Verification
|
||||
|
||||
- Run lightweight docs checks:
|
||||
- `git diff --check -- README.md docs/POST_BETA_ROADMAP.md docs/language/ROADMAP.md docs/language/SPEC-v1.md docs/language/RELEASE_NOTES.md docs/language/COLLECTIONS.md .llm/BETA_15_RESERVED_GENERIC_COLLECTION_BOUNDARY_HARDENING.md`
|
||||
- run an `rg` check for stale beta14-only current-stage phrasing across the
|
||||
same touched docs
|
||||
- Run focused compiler checks:
|
||||
- `cargo test --test reserved_generic_collection_beta15`
|
||||
- `cargo test --test diagnostics_contract`
|
||||
- `cargo test --test formatter`
|
||||
- `cargo test --test project_mode`
|
||||
- `rg -n "not supported in beta\\.9" compiler/src tests`
|
||||
- Do not commit, tag, push, or run release publication from this worker scope.
|
||||
@ -0,0 +1,85 @@
|
||||
# 1.0.0-beta.16 String Scanning And Token Boundary Foundation
|
||||
|
||||
Status: combined Slovo/Glagol release scope for `1.0.0-beta.16`.
|
||||
|
||||
`1.0.0-beta.16` adds the first explicit string scanning and token-boundary
|
||||
source facades to `std.string`: byte access, substring extraction, and
|
||||
prefix/suffix checks over the current runtime string representation. The
|
||||
release also promotes the matching compiler-known runtime names through the C
|
||||
runtime, LLVM lowering, source fallback evaluator, diagnostics inventory,
|
||||
local facade fixtures, and release-gate coverage.
|
||||
|
||||
## Scope
|
||||
|
||||
- Add `byte_at_result ((value string) (index i32)) -> (result i32 i32)` to
|
||||
`lib/std/string.slo`.
|
||||
- Add `slice_result ((value string) (start i32) (count i32)) -> (result string i32)`
|
||||
to `lib/std/string.slo`.
|
||||
- Add `starts_with ((value string) (prefix string)) -> bool` to
|
||||
`lib/std/string.slo`.
|
||||
- Add `ends_with ((value string) (suffix string)) -> bool` to
|
||||
`lib/std/string.slo`.
|
||||
- Mirror those source facades in
|
||||
`examples/projects/std-layout-local-string/src/string.slo`.
|
||||
- Extend the local and explicit `std.string` examples with success and ordinary
|
||||
failure checks for the four helpers.
|
||||
- Promote matching Glagol runtime entries for `std.string.byte_at_result`,
|
||||
`std.string.slice_result`, `std.string.starts_with`, and
|
||||
`std.string.ends_with`.
|
||||
- Add LLVM declarations and lowering for the four promoted runtime names.
|
||||
- Add C runtime implementations for byte access, byte slicing, prefix checks,
|
||||
and suffix checks.
|
||||
- Extend the source fallback test runner behavior for those names.
|
||||
- Add focused beta16 compiler coverage for lowering, hosted runtime execution,
|
||||
and source fallback execution.
|
||||
- Extend diagnostics snapshots for arity, type, context, shadowing, and richer
|
||||
unsupported string-scanning names.
|
||||
- Extend standard-library source-search and local-facade fixtures plus
|
||||
promotion-gate inventory alignment.
|
||||
- Update README, `lib/std/README.md`, post-beta roadmap, language roadmap,
|
||||
release notes, v1 spec, generated stdlib API, compiler docs, release gate,
|
||||
and standard-runtime catalog for beta16.
|
||||
|
||||
## Contract
|
||||
|
||||
- Helpers are byte-oriented over bytes before the trailing NUL in current
|
||||
runtime strings.
|
||||
- `byte_at_result` returns `ok byte` for a valid zero-based byte index and
|
||||
`err 1` for invalid indexes.
|
||||
- `slice_result` returns `ok text` for a valid byte range and `err 1` for
|
||||
invalid ranges.
|
||||
- `slice_result` allocation failure may follow the existing string allocation
|
||||
trap policy.
|
||||
- `starts_with` and `ends_with` perform byte prefix/suffix checks; empty
|
||||
prefix and suffix match.
|
||||
|
||||
## Explicit Non-Scope
|
||||
|
||||
- no Unicode scalar, grapheme, display-width, locale, normalization, or
|
||||
case-folding promise
|
||||
- no JSON parser
|
||||
- no object or array parser
|
||||
- no tokenizer/scanner object API
|
||||
- no language slice/view syntax or borrowed substring view
|
||||
- no mutable strings or string containers
|
||||
- no stable stdlib/API freeze
|
||||
- no stable ABI/layout or ownership guarantee for returned string allocations
|
||||
- no performance claim
|
||||
- no maps, sets, generic collections, or recursive JSON value support
|
||||
- no worker-owned commit, tag, push, or publication before controller gates
|
||||
|
||||
## Expected Verification
|
||||
|
||||
- `cargo fmt --check`
|
||||
- `cargo test --test standard_string_scanning_beta16`
|
||||
- `cargo test --test diagnostics_contract`
|
||||
- `cargo test --test standard_string_source_fallback_helpers_alpha`
|
||||
- `cargo test --test standard_core_facade_source_search_alpha`
|
||||
- `cargo test --test string_runtime`
|
||||
- `cargo test --test promotion_gate -- --ignored`
|
||||
- `cargo test`
|
||||
- `git diff --check`
|
||||
- stale current-stage scan for `1.0.0-beta.15`
|
||||
- public-text scan for local usernames, local filesystem paths, and private host
|
||||
publication references
|
||||
- final `./scripts/release-gate.sh` after commit and generated catalog sync
|
||||
39
.llm/BETA_17_JSON_PRIMITIVE_SCALAR_PARSING_FOUNDATION.md
Normal file
39
.llm/BETA_17_JSON_PRIMITIVE_SCALAR_PARSING_FOUNDATION.md
Normal file
@ -0,0 +1,39 @@
|
||||
# 1.0.0-beta.17 JSON Primitive Scalar Parsing Foundation
|
||||
|
||||
## Scope
|
||||
|
||||
Slovo-facing `1.0.0-beta.17` adds narrow `std.json` facade names for primitive
|
||||
scalar token parsing:
|
||||
|
||||
- `parse_bool_value_result ((token string)) -> (result bool i32)`
|
||||
- `parse_i32_value_result ((token string)) -> (result i32 i32)`
|
||||
- `parse_u32_value_result ((token string)) -> (result u32 i32)`
|
||||
- `parse_i64_value_result ((token string)) -> (result i64 i32)`
|
||||
- `parse_u64_value_result ((token string)) -> (result u64 i32)`
|
||||
- `parse_f64_value_result ((token string)) -> (result f64 i32)`
|
||||
- `parse_null_value_result ((token string)) -> (result bool i32)`
|
||||
|
||||
The construction surface from `1.0.0-beta.7` remains intact: the existing 24
|
||||
JSON construction exports are still present.
|
||||
|
||||
## Contract
|
||||
|
||||
These helpers parse one already-isolated primitive scalar token. Success
|
||||
returns `ok payload`; ordinary parse failure returns `err 1`.
|
||||
`parse_null_value_result` returns `ok true` only for exact `null` and `err 1`
|
||||
otherwise.
|
||||
|
||||
Numeric and boolean conversion is intentionally concrete and result-returning.
|
||||
The promoted numeric and boolean helpers consume a whole JSON primitive token:
|
||||
no leading/trailing whitespace, no leading `+`, no leading-zero integer form
|
||||
except `0`, and no non-finite f64 values. `parse_f64_value_result` accepts the
|
||||
current JSON-number grammar subset implemented by the runtime, including
|
||||
exponent notation. This slice does not freeze a stable API or ABI.
|
||||
|
||||
## Non-Scope
|
||||
|
||||
This is not full JSON parsing. It does not add `parse_string`, `parse_object`,
|
||||
`parse_array`, `parse_value`, tokenizers, recursive `JsonValue`, maps/sets,
|
||||
generic parse APIs, whitespace-tolerant document parsing, schema validation,
|
||||
streaming, Unicode escape handling, stable API freeze, stable ABI/layout, or
|
||||
performance claims.
|
||||
41
.llm/BETA_18_JSON_STRING_TOKEN_PARSING_FOUNDATION.md
Normal file
41
.llm/BETA_18_JSON_STRING_TOKEN_PARSING_FOUNDATION.md
Normal file
@ -0,0 +1,41 @@
|
||||
# 1.0.0-beta.18 JSON String Token Parsing Foundation
|
||||
|
||||
## Scope
|
||||
|
||||
Slovo-facing `1.0.0-beta.18` adds one narrow `std.json` facade name for JSON
|
||||
string token parsing:
|
||||
|
||||
- `parse_string_value_result ((token string)) -> (result string i32)`
|
||||
|
||||
The helper is a thin source wrapper over the promoted compiler/runtime name
|
||||
`std.json.parse_string_value_result`. The existing JSON construction helpers
|
||||
from `1.0.0-beta.7` and primitive scalar parse helpers from `1.0.0-beta.17`
|
||||
remain intact.
|
||||
|
||||
## Contract
|
||||
|
||||
`parse_string_value_result` consumes one already-isolated ASCII JSON string
|
||||
token. The token must start and end with quotes and must not include leading or
|
||||
trailing whitespace outside those quotes. Success returns `ok decoded_text`;
|
||||
ordinary parse failure returns `err 1`.
|
||||
|
||||
The token parser decodes the simple JSON escapes `\"`, `\\`, `\/`, `\b`,
|
||||
`\f`, `\n`, `\r`, and `\t`. It rejects raw control bytes, bad escapes,
|
||||
unterminated strings, trailing bytes after the closing quote, raw non-ASCII,
|
||||
and all `\uXXXX` escapes for this slice.
|
||||
|
||||
## Non-Scope
|
||||
|
||||
This is not full JSON parsing. It does not add object parsing, array parsing,
|
||||
recursive `JsonValue`, tokenizer APIs, generic parse APIs,
|
||||
whitespace-tolerant document parsing, streaming decoders or encoders, schema
|
||||
validation, Unicode escape decoding or normalization, embedded NUL policy,
|
||||
stable API freeze, stable ABI/layout, or performance claims.
|
||||
|
||||
## Gate-Supporting Compiler Hardening
|
||||
|
||||
The beta18 release also includes a bounded `glagol test` stack hardening fix:
|
||||
test execution runs on a worker thread with a 16 MiB stack. This is not a new
|
||||
language feature. It keeps deep source-authored stdlib fixtures gateable through
|
||||
normal `glagol test` behavior instead of host process stack overflow, and is
|
||||
covered by the promotion gate's stdlib fixture test execution.
|
||||
87
.llm/BETA_19_TEST_DISCOVERY_AND_CONFORMANCE.md
Normal file
87
.llm/BETA_19_TEST_DISCOVERY_AND_CONFORMANCE.md
Normal file
@ -0,0 +1,87 @@
|
||||
# 1.0.0-beta.19 Test Discovery And User-Project Conformance Foundation
|
||||
|
||||
## Scope
|
||||
|
||||
`1.0.0-beta.19` is a compiler/tooling and conformance slice. It does
|
||||
not change the Slovo source language or standard library surface.
|
||||
|
||||
Add deterministic list-only test discovery for:
|
||||
|
||||
- `glagol test --list <file|project|workspace>`
|
||||
- `glagol --run-tests --list <file>` for the legacy single-file path
|
||||
|
||||
## Contract
|
||||
|
||||
List mode must reuse the same checked front-end path as normal test execution:
|
||||
parse, lower, type-check, resolve project/workspace inputs, discover tests, and
|
||||
apply `--filter <substring>`.
|
||||
|
||||
The command then lists discovered/selected tests without evaluating test bodies.
|
||||
It must not execute runtime calls from test bodies, mutate files through test
|
||||
logic, open sockets through test logic, or otherwise trigger user test
|
||||
side-effects.
|
||||
|
||||
Ordering must remain deterministic and match current test execution discovery:
|
||||
|
||||
- single-file tests keep source order
|
||||
- project tests keep existing module/package discovery order
|
||||
- workspace tests keep existing workspace/package discovery order
|
||||
|
||||
Normal `glagol test` behavior and output remain unchanged unless `--list` is
|
||||
present. Invalid files, projects, and workspaces still fail through the
|
||||
existing diagnostic path.
|
||||
|
||||
## Output Shape
|
||||
|
||||
The initial output format is beta tooling. It should be stable enough for local
|
||||
release-gate tests, but it is not a frozen public schema.
|
||||
|
||||
The output should make selected, skipped, total discovered, and filter state
|
||||
visible. A concise text shape is enough; a stable JSON/event stream is out of
|
||||
scope for this slice.
|
||||
|
||||
## Non-Scope
|
||||
|
||||
This scope does not add:
|
||||
|
||||
- source-language syntax
|
||||
- runtime helper names
|
||||
- JSON expansion
|
||||
- parallel test execution
|
||||
- retries
|
||||
- tags or groups
|
||||
- coverage reports
|
||||
- event streams
|
||||
- stable artifact-manifest schema freeze
|
||||
- stable Markdown schema freeze
|
||||
- LSP or watch behavior
|
||||
- SARIF or daemon protocols
|
||||
- package registries
|
||||
- semver solving
|
||||
- performance claims
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- `glagol test --list <file.slo>` lists checked/discovered tests without
|
||||
executing bodies.
|
||||
- `glagol test --list <project>` and workspace inputs preserve current
|
||||
project/workspace ordering.
|
||||
- `glagol --run-tests --list <file.slo>` works for the legacy single-file path.
|
||||
- `--filter <substring>` marks/selects the same tests as normal filtered
|
||||
execution while avoiding body evaluation.
|
||||
- Normal `glagol test` output stays byte-stable for existing covered cases.
|
||||
- Invalid inputs still emit existing diagnostics.
|
||||
- Docs describe beta19 as a released tooling/conformance slice.
|
||||
- Release-gate coverage includes the focused beta19 test-discovery suite.
|
||||
|
||||
## Suggested Gates
|
||||
|
||||
```bash
|
||||
cargo fmt --check
|
||||
cargo test --test test_discovery_beta19
|
||||
cargo test --test project_mode
|
||||
cargo test --test cli_v1_1
|
||||
cargo test --test diagnostics_schema_beta13
|
||||
cargo test
|
||||
./scripts/release-gate.sh
|
||||
```
|
||||
41
.llm/BETA_1_TOOLING_HARDENING.md
Normal file
41
.llm/BETA_1_TOOLING_HARDENING.md
Normal file
@ -0,0 +1,41 @@
|
||||
# Beta.1 Tooling Hardening Scope
|
||||
|
||||
This file tracks the first post-`1.0.0-beta` tooling bundle, released as
|
||||
`1.0.0-beta.1`. The slice keeps the beta language surface unchanged and
|
||||
hardens the local compiler workflow, install layout, and release gate.
|
||||
|
||||
## Implemented In This Slice
|
||||
|
||||
- `glagol run <file.slo|project>` builds through the existing native hosted
|
||||
path, executes the result, forwards stdout/stderr, and exits with the program
|
||||
status.
|
||||
- `glagol run` writes to `.slovo/build/<name>` by default and supports `-o
|
||||
<binary>` for an explicit executable path.
|
||||
- `glagol run ... -- <args>` forwards program arguments to the produced
|
||||
executable.
|
||||
- `glagol clean <file.slo|project>` removes generated `.slovo/build` artifacts.
|
||||
- `glagol new --template binary|library|workspace` supports the existing
|
||||
binary scaffold plus checkable/testable library and local workspace
|
||||
scaffolds.
|
||||
- `scripts/install.sh` installs `bin/glagol`, `share/slovo/std/*.slo`, and
|
||||
`share/slovo/runtime/runtime.c` under a configurable prefix.
|
||||
- Installed `glagol` discovers both standard-library source modules and the
|
||||
runtime C input relative to the executable, with `SLOVO_STD_PATH`,
|
||||
`SLOVO_RUNTIME_C`, `GLAGOL_RUNTIME_C`, and `GLAGOL_CLANG` override paths.
|
||||
- The release gate prints a concise success line after docs, formatting, tests,
|
||||
promotion, binary, and LLVM smoke checks pass.
|
||||
|
||||
## Explicitly Out Of Scope
|
||||
|
||||
- no source-language syntax change
|
||||
- no networking or runtime resource model
|
||||
- no package registry behavior
|
||||
- no stable ABI/layout promise
|
||||
- no operating-system package-manager integration
|
||||
- no stable install layout promise beyond this beta toolchain layout
|
||||
|
||||
## Release Gate
|
||||
|
||||
- rerender publication PDFs when documentation release text changes
|
||||
- run the full release gate from a clean checkout state before tagging
|
||||
- tag only after the connected tooling bundle and generated documents agree
|
||||
89
.llm/BETA_20_STRING_SEARCH_AND_ASCII_TRIM_FOUNDATION.md
Normal file
89
.llm/BETA_20_STRING_SEARCH_AND_ASCII_TRIM_FOUNDATION.md
Normal file
@ -0,0 +1,89 @@
|
||||
# 1.0.0-beta.20 String Search And ASCII Trim Foundation
|
||||
|
||||
## Scope
|
||||
|
||||
`1.0.0-beta.20` is a standard-library and compiler-gate slice. It does not
|
||||
change source-language syntax, runtime C, compiler-known runtime names, or
|
||||
ABI/layout policy.
|
||||
|
||||
The release adds source-authored `std.string` helpers:
|
||||
|
||||
- `contains ((value string) (needle string)) -> bool`
|
||||
- `index_of_option ((value string) (needle string)) -> (option i32)`
|
||||
- `last_index_of_option ((value string) (needle string)) -> (option i32)`
|
||||
- `trim_ascii_start ((value string)) -> string`
|
||||
- `trim_ascii_end ((value string)) -> string`
|
||||
- `trim_ascii ((value string)) -> string`
|
||||
|
||||
## Contract
|
||||
|
||||
Search is byte-oriented over the current runtime string representation.
|
||||
`index_of_option` returns the first zero-based byte offset for a matching
|
||||
needle, `last_index_of_option` returns the last zero-based byte offset, and
|
||||
`contains` is true when `index_of_option` returns `some`.
|
||||
|
||||
Empty needles are valid:
|
||||
|
||||
- `index_of_option value ""` returns `some 0`
|
||||
- `last_index_of_option value ""` returns `some (len value)`
|
||||
- `contains value ""` returns `true`
|
||||
|
||||
Missing needles return `none`.
|
||||
|
||||
ASCII trim removes only these byte values from the requested edges:
|
||||
|
||||
- `9` horizontal tab
|
||||
- `10` line feed
|
||||
- `11` vertical tab
|
||||
- `12` form feed
|
||||
- `13` carriage return
|
||||
- `32` space
|
||||
|
||||
The helpers compose over already-promoted string primitives:
|
||||
|
||||
- `std.string.len`
|
||||
- `std.string.byte_at_result`
|
||||
- `std.string.slice_result`
|
||||
- `std.string.starts_with`
|
||||
- `std.string.ends_with`
|
||||
|
||||
## Non-Scope
|
||||
|
||||
This scope does not add:
|
||||
|
||||
- compiler-known `std.string.*` runtime names for the new helpers
|
||||
- runtime C helper implementations
|
||||
- source-language syntax
|
||||
- Unicode scalar, grapheme, display-width, or normalization semantics
|
||||
- case folding or locale-sensitive search
|
||||
- regular expressions
|
||||
- tokenizer/parser APIs
|
||||
- mutable strings
|
||||
- language slice/view syntax
|
||||
- stable string ABI/layout
|
||||
- stable allocation ownership rules
|
||||
- stable standard-library compatibility
|
||||
- performance claims
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- `lib/std/string.slo` exports all six helpers.
|
||||
- Explicit `std.string` import examples exercise contains, first/last search,
|
||||
missing needles, empty needles, leading trim, trailing trim, full trim,
|
||||
all-whitespace trim, and no-trim cases.
|
||||
- Local `std-layout-local-string` examples mirror the public helper surface.
|
||||
- Focused compiler coverage verifies the helpers are source-authored and that
|
||||
direct compiler-known runtime calls for the new names remain unsupported.
|
||||
- `scripts/release-gate.sh` runs the focused beta20 test.
|
||||
- Generated standard-library API documentation includes the new signatures.
|
||||
|
||||
## Gates
|
||||
|
||||
```bash
|
||||
cargo fmt --check
|
||||
cargo test --test standard_string_search_trim_beta20
|
||||
cargo test --test standard_string_source_fallback_helpers_alpha
|
||||
cargo test --test standard_string_scanning_beta16
|
||||
cargo test
|
||||
./scripts/release-gate.sh
|
||||
```
|
||||
72
.llm/BETA_21_JSON_DOCUMENT_SCALAR_PARSING_FOUNDATION.md
Normal file
72
.llm/BETA_21_JSON_DOCUMENT_SCALAR_PARSING_FOUNDATION.md
Normal file
@ -0,0 +1,72 @@
|
||||
# 1.0.0-beta.21 JSON Document Scalar Parsing Foundation
|
||||
|
||||
## Scope
|
||||
|
||||
`1.0.0-beta.21` is a standard-library and compiler-gate slice. It adds
|
||||
source-authored `std.json` helpers for scalar JSON documents while keeping
|
||||
source-language syntax, runtime C, compiler-known runtime names, and ABI/layout
|
||||
policy unchanged.
|
||||
|
||||
The release adds:
|
||||
|
||||
- `parse_string_document_result ((document string)) -> (result string i32)`
|
||||
- `parse_bool_document_result ((document string)) -> (result bool i32)`
|
||||
- `parse_i32_document_result ((document string)) -> (result i32 i32)`
|
||||
- `parse_u32_document_result ((document string)) -> (result u32 i32)`
|
||||
- `parse_i64_document_result ((document string)) -> (result i64 i32)`
|
||||
- `parse_u64_document_result ((document string)) -> (result u64 i32)`
|
||||
- `parse_f64_document_result ((document string)) -> (result f64 i32)`
|
||||
- `parse_null_document_result ((document string)) -> (result bool i32)`
|
||||
|
||||
## Contract
|
||||
|
||||
Each helper trims ASCII whitespace around the whole input document with
|
||||
`std.string.trim_ascii`, then delegates to the already released exact
|
||||
value-token parser for that scalar family.
|
||||
|
||||
Leading and trailing ASCII whitespace around one scalar document is accepted.
|
||||
Trailing non-whitespace remains an ordinary parse failure and returns `err 1`
|
||||
through the underlying exact parser.
|
||||
|
||||
## Non-Scope
|
||||
|
||||
This scope does not add:
|
||||
|
||||
- compiler-known `std.json.*_document_result` runtime names
|
||||
- private `__glagol_json_*document*` runtime symbols
|
||||
- runtime C helper implementations
|
||||
- source-language syntax
|
||||
- object or array parsing
|
||||
- recursive `JsonValue`
|
||||
- tokenizer/parser objects
|
||||
- maps, sets, executable generics, or generic collections
|
||||
- streaming parsers or encoders
|
||||
- Unicode escape decoding beyond the existing string-token helper
|
||||
- embedded NUL policy
|
||||
- stable JSON APIs
|
||||
- stable ABI/layout
|
||||
- stable standard-library compatibility
|
||||
- performance claims
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- `lib/std/json.slo` exports all eight document scalar helpers.
|
||||
- Explicit `std.json` import examples exercise trimmed whitespace success,
|
||||
no-whitespace success, and trailing non-whitespace failure.
|
||||
- Local `std-layout-local-json` examples mirror the public helper surface with
|
||||
an explicit local `string` dependency for `trim_ascii`.
|
||||
- Focused compiler coverage verifies the helpers are source-authored and that
|
||||
direct compiler-known runtime calls for the new names remain unsupported.
|
||||
- `scripts/release-gate.sh` runs the focused beta21 test.
|
||||
- Generated standard-library API documentation includes the new signatures.
|
||||
|
||||
## Gates
|
||||
|
||||
```bash
|
||||
cargo fmt --check
|
||||
cargo test --test standard_json_document_scalar_parsing_beta21
|
||||
cargo test --test standard_json_source_facade_alpha
|
||||
cargo test --test promotion_gate
|
||||
cargo test
|
||||
./scripts/release-gate.sh
|
||||
```
|
||||
71
.llm/BETA_22_RUN_MANIFEST_AND_EXECUTION_REPORT_HARDENING.md
Normal file
71
.llm/BETA_22_RUN_MANIFEST_AND_EXECUTION_REPORT_HARDENING.md
Normal file
@ -0,0 +1,71 @@
|
||||
# 1.0.0-beta.22 Run Manifest And Execution Report Hardening
|
||||
|
||||
## Scope
|
||||
|
||||
`1.0.0-beta.22` is a compiler/tooling evidence-hardening slice for
|
||||
`glagol run --manifest`. It keeps the Slovo source language, typed core,
|
||||
runtime capabilities, standard-library surface, compiler-known runtime names,
|
||||
ABI/layout policy, and package behavior unchanged.
|
||||
|
||||
The release adds an additive run-report block to run-mode artifact manifests
|
||||
so manifest evidence can record:
|
||||
|
||||
- process exit status for the invoked program
|
||||
- captured stdout from the run
|
||||
- captured stderr from the run
|
||||
- forwarded program arguments passed through `glagol run`
|
||||
|
||||
## Contract
|
||||
|
||||
When `glagol run --manifest <path>` completes far enough to write an artifact
|
||||
manifest, the manifest should include run execution evidence in addition to the
|
||||
existing schema marker, command, mode, success, diagnostics metadata, primary
|
||||
output, and artifacts fields.
|
||||
|
||||
The run-report data describes the native executable invocation performed by
|
||||
`glagol run`, not a new source-language or runtime feature. Captured stdout
|
||||
and stderr are evidence fields for tooling and release-gate review; they do
|
||||
not redefine ordinary terminal behavior. Forwarded args are recorded so
|
||||
fixtures can distinguish compiler arguments from user-program arguments.
|
||||
|
||||
The block is additive beta tooling metadata. It is not a stable public schema
|
||||
freeze and does not bump `slovo.artifact-manifest` version `1`.
|
||||
|
||||
## Non-Scope
|
||||
|
||||
This scope does not add:
|
||||
|
||||
- source-language syntax
|
||||
- standard-library helpers
|
||||
- compiler-known `std.*` runtime names
|
||||
- runtime C capabilities
|
||||
- package, workspace, import, or registry behavior
|
||||
- stable artifact-manifest schema freeze
|
||||
- stable Markdown schema freeze
|
||||
- LSP, watch, SARIF, or daemon protocols
|
||||
- performance claims
|
||||
- stable ABI/layout
|
||||
- beta maturity beyond the existing `1.0.0-beta` line
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- `glagol run --manifest <path>` writes an artifact manifest with an additive
|
||||
run-report block after executing a supported program.
|
||||
- The run report records exit status, captured stdout, captured stderr, and
|
||||
forwarded args.
|
||||
- Existing manifest fields and schema/version markers remain compatible with
|
||||
the beta artifact-manifest contract.
|
||||
- Non-run modes do not need run-report metadata.
|
||||
- Documentation describes beta22 as tooling/CLI evidence hardening only.
|
||||
- Release notes and roadmaps state that beta22 does not add language or
|
||||
stdlib features and does not freeze the manifest schema.
|
||||
|
||||
## Suggested Gates
|
||||
|
||||
```bash
|
||||
cargo fmt --check
|
||||
cargo test --test run_manifest_beta22
|
||||
cargo test --test cli_v1_1
|
||||
./scripts/release-gate.sh
|
||||
git diff --check
|
||||
```
|
||||
@ -0,0 +1,73 @@
|
||||
# 1.0.0-beta.23 Standard Library Stability Tier Ledger And Catalog Alignment
|
||||
|
||||
## Scope
|
||||
|
||||
`1.0.0-beta.23` is a documentation/catalog clarity slice for the standard
|
||||
library. It adds a public stability-tier ledger and aligns the surrounding
|
||||
README, roadmap, release-note, and specification text with the generated API
|
||||
catalog boundary.
|
||||
|
||||
This release does not change the source language, typed core, runtime
|
||||
capabilities, standard-library helper surface, compiler-known runtime names,
|
||||
ABI/layout policy, or package behavior. It does change generated catalog
|
||||
output and release-gate checks so tier metadata is visible and enforced.
|
||||
|
||||
## Contract
|
||||
|
||||
The public standard-library docs use exactly these tier labels:
|
||||
|
||||
- `beta-supported`
|
||||
- `experimental`
|
||||
- `internal`
|
||||
|
||||
The generated `docs/language/STDLIB_API.md` catalog remains the exact exported
|
||||
signature inventory generated from `lib/std/*.slo`. The new
|
||||
`docs/language/STDLIB_TIERS.md` ledger records maturity and stability
|
||||
expectations for those helpers and surrounding standard-library domains.
|
||||
|
||||
Experimental domains in the beta23 ledger include JSON, loopback networking,
|
||||
random/time, and filesystem resource-handle helpers. Concrete vector modules
|
||||
remain beta-supported concrete lanes; they are not a generic collections
|
||||
freeze and do not imply executable generics, maps, sets, iterators, mutable
|
||||
vectors, slice/view APIs, runtime collection changes, or stable ABI/layout.
|
||||
|
||||
## Non-Scope
|
||||
|
||||
This scope does not add:
|
||||
|
||||
- source-language syntax
|
||||
- standard-library helpers
|
||||
- compiler-known `std.*` runtime names
|
||||
- runtime C capabilities
|
||||
- package, workspace, import, or registry behavior
|
||||
- stable standard-library/API compatibility freeze
|
||||
- stable manifest schema freeze
|
||||
- stable Markdown schema freeze
|
||||
- stable ABI/layout
|
||||
- performance claims
|
||||
- beta maturity beyond the existing `1.0.0-beta` line
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- `docs/language/STDLIB_TIERS.md` defines the public tier labels and their
|
||||
current meaning.
|
||||
- The tier ledger marks JSON, loopback networking, random/time, and filesystem
|
||||
resource-handle helpers as experimental domains.
|
||||
- The tier ledger records concrete vector modules as beta-supported concrete
|
||||
lanes without claiming generic collection stability.
|
||||
- README, `lib/std/README.md`, language release notes, language roadmap,
|
||||
post-beta roadmap, and the v1 spec link or describe the tier ledger.
|
||||
- Documentation states beta23 is docs/catalog tooling clarity only and does
|
||||
not add language, stdlib, runtime, stable schema, or stable API behavior.
|
||||
- The generated API catalog emits tier metadata, and the release gate checks
|
||||
that experimental tiers remain represented.
|
||||
|
||||
## Suggested Gates
|
||||
|
||||
```bash
|
||||
node --check scripts/render-stdlib-api-doc.js
|
||||
node --check scripts/check-stdlib-api-tiers.js
|
||||
./scripts/render-stdlib-api-doc.sh
|
||||
./scripts/check-stdlib-api-tiers.js
|
||||
git diff --check
|
||||
```
|
||||
@ -0,0 +1,79 @@
|
||||
# 1.0.0-beta.24 Package Manifest Identity And Dependency Discipline
|
||||
|
||||
## Scope
|
||||
|
||||
`1.0.0-beta.24` is a package/workspace discipline hardening slice for local
|
||||
manifest diagnostics. It tightens how package manifest identity and local
|
||||
dependency tables are reported when users write ambiguous or invalid manifest
|
||||
keys.
|
||||
|
||||
This release changes diagnostics only. It does not change the Slovo source
|
||||
language, typed core, runtime behavior, standard-library helper surface,
|
||||
compiler-known runtime names, package graph semantics, ABI/layout policy, or
|
||||
artifact/Markdown schema stability guarantees.
|
||||
|
||||
## Contract
|
||||
|
||||
The beta package model remains a closed local workspace model:
|
||||
|
||||
- package manifests declare `[package]` identity metadata and optional
|
||||
`[dependencies]` local path records
|
||||
- dependency keys name local packages and must match the target package name
|
||||
- dependency paths remain local path records under the existing workspace and
|
||||
package boundary checks
|
||||
|
||||
The beta24 diagnostic hardening makes these manifest errors explicit:
|
||||
|
||||
- duplicate keys in package manifests report `PackageManifestInvalid`
|
||||
- invalid dependency keys report `InvalidPackageDependencyName`
|
||||
- duplicate dependency keys report `DuplicatePackageDependencyName`
|
||||
|
||||
These diagnostics are part of the beta package/workspace discipline surface.
|
||||
They do not promote a package manager, resolver, lockfile, registry, publish
|
||||
flow, or stable package ABI/layout.
|
||||
|
||||
## Non-Scope
|
||||
|
||||
This scope does not add:
|
||||
|
||||
- remote registry behavior
|
||||
- lockfiles
|
||||
- semantic-version solving
|
||||
- package publishing
|
||||
- optional, dev, target, or feature-gated dependencies
|
||||
- build scripts or package archives
|
||||
- stable package ABI/layout
|
||||
- stable package manager behavior
|
||||
- source-language syntax or semantics
|
||||
- runtime behavior or runtime C capabilities
|
||||
- standard-library helpers or stdlib behavior
|
||||
- compiler-known `std.*` runtime names
|
||||
- stable artifact-manifest or Markdown schema guarantees
|
||||
- performance claims
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- `docs/language/PACKAGES.md` documents duplicate package-manifest keys,
|
||||
invalid dependency keys, and duplicate dependency keys as explicit beta
|
||||
diagnostics.
|
||||
- Language and compiler release notes describe beta24 as package manifest
|
||||
identity/dependency diagnostic hardening only.
|
||||
- Language and compiler roadmaps record beta24 as a local package/workspace
|
||||
diagnostics slice with all registry, lockfile, semver, publishing,
|
||||
optional/dev/target dependency, ABI/layout, language, runtime, and stdlib
|
||||
work deferred.
|
||||
- README and the post-beta roadmap identify `1.0.0-beta.24` as the current
|
||||
package/workspace discipline hardening slice.
|
||||
- Glagol is versioned as `1.0.0-beta.24`.
|
||||
- `compiler/tests/package_workspace_discipline_beta24.rs` covers the focused
|
||||
package manifest/dependency diagnostics and a positive local dependency
|
||||
workspace.
|
||||
- `scripts/release-gate.sh` runs the focused beta24 test.
|
||||
|
||||
## Suggested Gates
|
||||
|
||||
```bash
|
||||
git diff --check
|
||||
cargo fmt --check
|
||||
cargo test --test package_workspace_discipline_beta24
|
||||
```
|
||||
60
.llm/BETA_2_RUNTIME_RESOURCE_FOUNDATION.md
Normal file
60
.llm/BETA_2_RUNTIME_RESOURCE_FOUNDATION.md
Normal file
@ -0,0 +1,60 @@
|
||||
# Beta.2 Runtime Resource Foundation Scope
|
||||
|
||||
This file tracks the first post-`1.0.0-beta.1` runtime/resource foundation
|
||||
slice released as `1.0.0-beta.2`.
|
||||
|
||||
## Implemented In This Slice
|
||||
|
||||
- `std.fs.open_text_read_result : (string) -> (result i32 i32)` opens a text
|
||||
file for read-only resource-handle flow.
|
||||
- `std.fs.read_open_text_result : (i32) -> (result string i32)` reads remaining
|
||||
text from an open read handle.
|
||||
- `std.fs.close_result : (i32) -> (result i32 i32)` closes an open resource
|
||||
handle.
|
||||
- `std.fs.exists : (string) -> bool`, `std.fs.is_file : (string) -> bool`,
|
||||
and `std.fs.is_dir : (string) -> bool` expose narrow filesystem status
|
||||
checks.
|
||||
- `std.fs.remove_file_result : (string) -> (result i32 i32)` removes one file,
|
||||
and `std.fs.create_dir_result : (string) -> (result i32 i32)` creates one
|
||||
directory. Both return `ok 0` on success or `err 1` on ordinary host failure.
|
||||
- `lib/std/fs.slo` exposes matching explicit source facade helpers:
|
||||
`open_text_read_result`, `read_open_text_result`, `close_result`,
|
||||
`read_text_via_handle_result`, `close_ok`, `exists`, `is_file`, `is_dir`,
|
||||
`remove_file_result`, `create_dir_result`, `remove_file_ok`, and
|
||||
`create_dir_ok`.
|
||||
- Glagol lowers the new calls to private runtime symbols and keeps the existing
|
||||
concrete `result` families.
|
||||
- The test runner and hosted C runtime both enforce the same basic policy:
|
||||
invalid, missing, closed, or exhausted ordinary host failures return `err 1`
|
||||
instead of panicking.
|
||||
|
||||
## Resource Policy
|
||||
|
||||
- Resource handles are positive `i32` tokens owned by the current process.
|
||||
- Handles are opaque Slovo values, not host file descriptors and not stable ABI
|
||||
values.
|
||||
- A handle is valid only until `std.fs.close_result` succeeds.
|
||||
- Reading a closed or invalid handle returns `err 1`.
|
||||
- Closing a closed or invalid handle returns `err 1`.
|
||||
- Resource cleanup is explicit in this beta slice; no finalizer, destructor,
|
||||
ownership transfer, or affine typing rule is claimed.
|
||||
|
||||
## Explicitly Out Of Scope
|
||||
|
||||
- no writable file handles
|
||||
- no binary IO
|
||||
- no directory handles
|
||||
- no directory enumeration or recursive filesystem operations
|
||||
- no process handles
|
||||
- no sockets or networking
|
||||
- no async/event-loop resource model
|
||||
- no platform-specific error codes
|
||||
- no rich host-error ADT
|
||||
- no stable handle ABI/layout guarantee
|
||||
|
||||
## Release Gate
|
||||
|
||||
- focused resource/host tests must pass
|
||||
- focused `std.fs` source facade tests must pass
|
||||
- promotion-gate std facade alignment must pass
|
||||
- full release gate passed before tagging `1.0.0-beta.2`
|
||||
37
.llm/BETA_3_STDLIB_STABILIZATION.md
Normal file
37
.llm/BETA_3_STDLIB_STABILIZATION.md
Normal file
@ -0,0 +1,37 @@
|
||||
# Beta 3 Standard Library Stabilization
|
||||
|
||||
Release label: `1.0.0-beta.3`
|
||||
|
||||
Release date: 2026-05-22
|
||||
|
||||
Status: released beta standard-library stabilization slice.
|
||||
|
||||
## Scope
|
||||
|
||||
This post-`1.0.0-beta.2` slice stabilizes the existing standard-library surface
|
||||
before adding new large API families.
|
||||
|
||||
## Current Work
|
||||
|
||||
- Generate `docs/language/STDLIB_API.md` from `lib/std/*.slo`.
|
||||
- Gate the generated catalog in `scripts/release-gate.sh`.
|
||||
- Add `examples/projects/stdlib-composition` as a realistic multi-module
|
||||
command-line project.
|
||||
- Keep imports explicit and beta-scoped; do not claim stable std APIs, stable
|
||||
ABI, automatic prelude imports, richer host error codes, or general resource
|
||||
ownership semantics.
|
||||
|
||||
## Acceptance Gates
|
||||
|
||||
- `./scripts/render-stdlib-api-doc.sh`
|
||||
- `git diff --check`
|
||||
- `cargo test --test standard_stdlib_composition_beta`
|
||||
- `./scripts/release-gate.sh`
|
||||
|
||||
## Next Decisions
|
||||
|
||||
- Finish naming/tier review for duplicated concrete helper families.
|
||||
- Identify helpers that should pause until generics rather than expanding more
|
||||
type-specific copies.
|
||||
- Release this connected slice as the next beta tag only after the catalog,
|
||||
composition example, docs, and final review all pass together.
|
||||
34
.llm/BETA_4_LANGUAGE_USABILITY.md
Normal file
34
.llm/BETA_4_LANGUAGE_USABILITY.md
Normal file
@ -0,0 +1,34 @@
|
||||
# Beta 4 Language Usability
|
||||
|
||||
Release label: `1.0.0-beta.4`
|
||||
|
||||
Release date: 2026-05-22
|
||||
|
||||
Status: released beta language-usability diagnostics slice.
|
||||
|
||||
## Scope
|
||||
|
||||
This post-`1.0.0-beta.3` slice reduces friction in ordinary project use
|
||||
without changing the typed core or claiming new syntax stability.
|
||||
|
||||
## Current Work
|
||||
|
||||
- Improve project/workspace build and run entry diagnostics.
|
||||
- Keep the accepted entry contract unchanged: `(fn main () -> i32 ...)`.
|
||||
- Use precise diagnostic codes for missing and invalid entry `main` functions.
|
||||
- Make non-exhaustive `match` diagnostics clearer and deterministic.
|
||||
|
||||
## Acceptance Gates
|
||||
|
||||
- `cargo test --test project_mode entry_main`
|
||||
- `cargo test --test diagnostics_contract`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
|
||||
## Deferrals
|
||||
|
||||
- No implicit `main` return conversions.
|
||||
- No argument-taking `main`.
|
||||
- No async, package registry, or stable ABI/layout promises.
|
||||
- Broader type aliases, match diagnostics, and destructuring remain candidate
|
||||
follow-up items in the same language usability roadmap lane.
|
||||
48
.llm/BETA_5_PACKAGE_WORKSPACE_DISCIPLINE.md
Normal file
48
.llm/BETA_5_PACKAGE_WORKSPACE_DISCIPLINE.md
Normal file
@ -0,0 +1,48 @@
|
||||
# Beta 5 Package And Workspace Discipline
|
||||
|
||||
Release label: `1.0.0-beta.5`
|
||||
|
||||
Release date: 2026-05-22
|
||||
|
||||
Status: released beta package/workspace discipline slice.
|
||||
|
||||
## Scope
|
||||
|
||||
This post-`1.0.0-beta.4` slice tightens local package/workspace behavior
|
||||
without adding a remote registry, package solver, lockfile, or stable package
|
||||
ABI promise.
|
||||
|
||||
## Current Work
|
||||
|
||||
- Add `[workspace] default_package = "name"` as an explicit build/run entry
|
||||
selector for workspaces with multiple packages that contain their entry
|
||||
module.
|
||||
- Keep `check`, `test`, `fmt`, and `doc` project-wide over the full closed
|
||||
workspace graph.
|
||||
- Diagnose missing `default_package` references during workspace loading.
|
||||
- Diagnose duplicate workspace members after path normalization before package
|
||||
loading.
|
||||
- Add workspace package and dependency summaries to `glagol doc`.
|
||||
- Teach generated workspace templates and canonical workspace examples to
|
||||
declare `default_package = "app"`.
|
||||
- Add `docs/language/PACKAGES.md` as the public beta local package/workspace
|
||||
guide, including the no-registry/no-lockfile policy.
|
||||
- Keep dependency resolution local-path-only and deterministic.
|
||||
|
||||
## Acceptance Gates
|
||||
|
||||
- `cargo test --test project_mode workspace_default_package`
|
||||
- `cargo test --test project_mode workspace_package_boundaries`
|
||||
- `cargo test --test dx_v1_7 doc_generates_workspace_package_summary`
|
||||
- `cargo test --test dx_v1_7 new_workspace_template`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
|
||||
## Deferrals
|
||||
|
||||
- No remote registry.
|
||||
- No lockfile.
|
||||
- No semantic-version solver.
|
||||
- No optional, dev, target, or feature dependencies.
|
||||
- No package publishing or archive format.
|
||||
- No stable package ABI/layout guarantee.
|
||||
66
.llm/BETA_6_NETWORKING_FOUNDATION.md
Normal file
66
.llm/BETA_6_NETWORKING_FOUNDATION.md
Normal file
@ -0,0 +1,66 @@
|
||||
# 1.0.0-beta.6 Networking Foundation Target
|
||||
|
||||
Status: released as `1.0.0-beta.6` on 2026-05-22.
|
||||
|
||||
`1.0.0-beta.6` targets a deliberately narrow networking foundation after the
|
||||
resource-handle and host-error policy introduced in `1.0.0-beta.2`. The goal
|
||||
is blocking loopback TCP only, enough for local client/server fixtures and
|
||||
small request/response examples without committing to a full networking stack.
|
||||
|
||||
## Slovo Source Surface
|
||||
|
||||
The staged source facade is `lib/std/net.slo`, importable explicitly as
|
||||
`std.net`.
|
||||
|
||||
Exported helpers:
|
||||
|
||||
- `tcp_connect_loopback_result : (i32) -> (result i32 i32)`
|
||||
- `tcp_listen_loopback_result : (i32) -> (result i32 i32)`
|
||||
- `tcp_bound_port_result : (i32) -> (result i32 i32)`
|
||||
- `tcp_accept_result : (i32) -> (result i32 i32)`
|
||||
- `tcp_read_all_result : (i32) -> (result string i32)`
|
||||
- `tcp_write_text_result : (i32, string) -> (result i32 i32)`
|
||||
- `tcp_close_result : (i32) -> (result i32 i32)`
|
||||
- `tcp_write_text_ok : (i32, string) -> bool`
|
||||
- `tcp_close_ok : (i32) -> bool`
|
||||
|
||||
The `i32` values returned by successful connect/listen/accept operations are
|
||||
opaque process-local handles. They are not host file descriptors, stable ABI
|
||||
values, transferable capabilities, or ownership-checked affine resources.
|
||||
|
||||
## Runtime Calls
|
||||
|
||||
The facade wraps these compiler-known runtime calls:
|
||||
|
||||
- `std.net.tcp_connect_loopback_result(port i32) -> (result i32 i32)`
|
||||
- `std.net.tcp_listen_loopback_result(port i32) -> (result i32 i32)`
|
||||
- `std.net.tcp_bound_port_result(handle i32) -> (result i32 i32)`
|
||||
- `std.net.tcp_accept_result(listener i32) -> (result i32 i32)`
|
||||
- `std.net.tcp_read_all_result(handle i32) -> (result string i32)`
|
||||
- `std.net.tcp_write_text_result(handle i32, text string) -> (result i32 i32)`
|
||||
- `std.net.tcp_close_result(handle i32) -> (result i32 i32)`
|
||||
|
||||
Ordinary host failures return `err 1`. Successful status-returning operations
|
||||
return `ok 0`. Successful handle-returning operations return `ok handle`.
|
||||
Successful `tcp_bound_port_result` returns the bound loopback TCP port.
|
||||
|
||||
## Fixtures
|
||||
|
||||
- `examples/projects/std-import-net/` exercises explicit `std.net` source
|
||||
import.
|
||||
- `examples/projects/std-layout-local-net/` mirrors the facade as a local
|
||||
module fixture and keeps the source-search contract explicit.
|
||||
|
||||
The source-side fixtures use invalid ports and handles for deterministic
|
||||
result-shape checks. Positive loopback client/server behavior is covered by
|
||||
the matching compiler/runtime tests when the local sandbox allows loopback
|
||||
sockets.
|
||||
|
||||
## Deferrals
|
||||
|
||||
This scope does not add DNS, TLS, UDP, Unix-domain sockets, non-loopback
|
||||
binding, async IO, event loops, readiness polling, timeouts, buffering policy,
|
||||
HTTP frameworks, socket options beyond the implementation minimum,
|
||||
platform-specific error codes, rich host-error ADTs, stable runtime helper
|
||||
symbols, stable ABI/layout/ownership guarantees, automatic cleanup, or a
|
||||
stable standard-library API freeze.
|
||||
61
.llm/BETA_7_SERIALIZATION_DATA_INTERCHANGE.md
Normal file
61
.llm/BETA_7_SERIALIZATION_DATA_INTERCHANGE.md
Normal file
@ -0,0 +1,61 @@
|
||||
# 1.0.0-beta.7 Serialization And Data Interchange Target
|
||||
|
||||
Status: released as `1.0.0-beta.7` on 2026-05-22.
|
||||
|
||||
`1.0.0-beta.7` targets a deliberately narrow serialization/data-interchange
|
||||
foundation after the beta.6 networking slice. The goal is compact JSON text
|
||||
construction for CLI, file, and loopback-network programs without pretending
|
||||
that Slovo already has the collections and string APIs needed for a complete
|
||||
JSON library.
|
||||
|
||||
## Slovo Source Surface
|
||||
|
||||
The staged source facade is `lib/std/json.slo`, importable explicitly as
|
||||
`std.json`.
|
||||
|
||||
Exported helpers:
|
||||
|
||||
- `quote_string : (string) -> string`
|
||||
- `null_value : () -> string`
|
||||
- `bool_value : (bool) -> string`
|
||||
- `i32_value : (i32) -> string`
|
||||
- `u32_value : (u32) -> string`
|
||||
- `i64_value : (i64) -> string`
|
||||
- `u64_value : (u64) -> string`
|
||||
- `f64_value : (f64) -> string`
|
||||
- `field_string`, `field_bool`, `field_i32`, `field_u32`, `field_i64`,
|
||||
`field_u64`, `field_f64`, and `field_null`
|
||||
- `array0`, `array1`, `array2`, `array3`
|
||||
- `object0`, `object1`, `object2`, `object3`
|
||||
|
||||
The array and object helpers accept already-encoded JSON text fragments. They
|
||||
are compact construction helpers, not recursive data structures.
|
||||
|
||||
## Runtime Call
|
||||
|
||||
The facade wraps one compiler-known runtime call:
|
||||
|
||||
- `std.json.quote_string(value string) -> string`
|
||||
|
||||
The hosted runtime symbol is `__glagol_json_quote_string`. It returns a
|
||||
complete compact JSON string literal including surrounding quotes. It escapes
|
||||
quote, backslash, newline, tab, carriage return, backspace, form feed, and
|
||||
other control bytes. Allocation failure uses the existing string allocation
|
||||
trap.
|
||||
|
||||
## Fixtures And Benchmarks
|
||||
|
||||
- `examples/projects/std-import-json/` exercises explicit `std.json` source
|
||||
import.
|
||||
- `examples/projects/std-layout-local-json/` mirrors the facade as a local
|
||||
module fixture and keeps the source-search contract explicit.
|
||||
- `benchmarks/json-quote-loop/` adds a local-machine timing scaffold for JSON
|
||||
string quoting across Slovo, C, Rust, Python, Clojure, and Common Lisp/SBCL.
|
||||
|
||||
## Deferrals
|
||||
|
||||
This scope does not add JSON parsing, recursive JSON values, maps/sets, generic
|
||||
collections, source-level byte/character scanners, slicing, streaming encoders,
|
||||
schema validation, Unicode normalization, embedded NUL support in the current
|
||||
null-terminated runtime string ABI, stable runtime helper symbols, stable
|
||||
ABI/layout/ownership guarantees, or a stable standard-library API freeze.
|
||||
60
.llm/BETA_8_CONCRETE_TYPE_ALIASES.md
Normal file
60
.llm/BETA_8_CONCRETE_TYPE_ALIASES.md
Normal file
@ -0,0 +1,60 @@
|
||||
# 1.0.0-beta.8 Concrete Type Alias Foundation
|
||||
|
||||
Status: release scope for `1.0.0-beta.8`.
|
||||
|
||||
`1.0.0-beta.8` targets a deliberately small language-usability slice:
|
||||
transparent top-level aliases for existing concrete Slovo types.
|
||||
|
||||
## Source Surface
|
||||
|
||||
The declaration form is:
|
||||
|
||||
```slo
|
||||
(type Alias TargetType)
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
```slo
|
||||
(type JsonText string)
|
||||
(type Scores (vec i32))
|
||||
(type MaybeName (option string))
|
||||
(type ReadResult (result string i32))
|
||||
```
|
||||
|
||||
Aliases are module-local names. A later declaration in the same module may use
|
||||
the alias wherever the resolved concrete target type is already supported.
|
||||
|
||||
## Resolution Contract
|
||||
|
||||
- Resolve aliases before typed-core lowering, checked import signatures,
|
||||
backend layout, ABI decisions, and runtime behavior.
|
||||
- Treat aliases as transparent names, not new nominal types.
|
||||
- Preserve the target type's existing value-flow, constructor, operator,
|
||||
`match`, field-access, import, and diagnostic behavior.
|
||||
- Normalize exported function or struct signatures that mention local aliases
|
||||
to their concrete target types for cross-module use.
|
||||
- Generated documentation may show source alias spellings; runtime, checked
|
||||
lowering, and cross-module typing must use the resolved concrete target.
|
||||
- Reject failed alias resolution; do not silently fall back to a primitive type.
|
||||
|
||||
## Source Fixtures
|
||||
|
||||
- `lib/std/json.slo` uses local `JsonText` and `JsonField` aliases for already
|
||||
encoded JSON fragments.
|
||||
- `examples/projects/std-import-json/` uses the same alias shape in the
|
||||
explicit standard-import fixture without importing aliases.
|
||||
- `examples/projects/std-layout-local-json/` mirrors the facade and example as
|
||||
a local source fixture.
|
||||
|
||||
These fixtures keep the existing JSON helper surface and test names. They do
|
||||
not add compiler-known runtime calls or new public standard-library helper
|
||||
names.
|
||||
|
||||
## Deferrals
|
||||
|
||||
This target does not add generic aliases, parameterized aliases, alias type
|
||||
parameters, alias re-exports, cross-module alias imports, import aliases, glob
|
||||
imports, maps/sets, alias-driven overloads, implicit casts, runtime helpers,
|
||||
hosted runtime symbols, stable ABI/layout promises, or a standard-library API
|
||||
freeze.
|
||||
@ -0,0 +1,62 @@
|
||||
# 1.0.0-beta.9 Collection Alias Unification And Generic Reservation
|
||||
|
||||
Status: release scope for `1.0.0-beta.9`.
|
||||
|
||||
`1.0.0-beta.9` is a Slovo stdlib/docs slice on top of the beta.8 concrete
|
||||
alias foundation. It applies transparent aliases to the repeated concrete
|
||||
collection and value-family facades without changing the public helper surface
|
||||
or executable semantics.
|
||||
|
||||
## Source Surface
|
||||
|
||||
Current source-authored facades may declare module-local aliases such as:
|
||||
|
||||
```slo
|
||||
(type VecBool (vec bool))
|
||||
(type OptionString (option string))
|
||||
(type ResultU64 (result u64 i32))
|
||||
```
|
||||
|
||||
The aliases are used only inside their defining module. Exported helper names
|
||||
and helper meanings remain concrete, and importers observe normalized concrete
|
||||
types.
|
||||
|
||||
## Applied Scope
|
||||
|
||||
- `lib/std/vec_i32.slo`
|
||||
- `lib/std/vec_i64.slo`
|
||||
- `lib/std/vec_f64.slo`
|
||||
- `lib/std/vec_bool.slo`
|
||||
- `lib/std/vec_string.slo`
|
||||
- `lib/std/option.slo`
|
||||
- `lib/std/result.slo`
|
||||
|
||||
The vector facades use one local alias for the concrete vector family. The
|
||||
option and result facades use local aliases for the current concrete option and
|
||||
result families.
|
||||
|
||||
## Contract
|
||||
|
||||
- Current vectors remain concrete families: `(vec i32)`, `(vec i64)`,
|
||||
`(vec f64)`, `(vec bool)`, and `(vec string)`.
|
||||
- Current options and results remain concrete families over the explicitly
|
||||
promoted payload shapes.
|
||||
- Aliases are transparent, module-local, and erased before typed-core lowering,
|
||||
backend layout, ABI decisions, runtime behavior, and cross-module signatures.
|
||||
- Public helper names, exports, constructors, runtime calls, and behavior are
|
||||
preserved.
|
||||
- Public API documentation must not turn these local aliases into imported
|
||||
public type names; the semantic contract is the normalized concrete target
|
||||
type.
|
||||
|
||||
## Deferrals
|
||||
|
||||
This release does not add executable generics, generic aliases, parameterized
|
||||
aliases, maps, sets, traits, inference, monomorphization, iterators, new
|
||||
compiler-known runtime names, stable ABI/layout promises, or a stable
|
||||
standard-library API freeze.
|
||||
|
||||
The generic/map/set direction is reserved through diagnostics and design
|
||||
language only. Future work must define syntax, typed-core representation,
|
||||
lowering, conformance gates, compatibility policy, and stdlib migration rules
|
||||
before any executable generic collection surface is promoted.
|
||||
@ -13,11 +13,31 @@ Slovo reaches stable `1.0.0` only after:
|
||||
|
||||
## Recommended Sequence
|
||||
|
||||
1. Harden monorepo release tooling, clean docs, and public install/build flow.
|
||||
2. Stabilize `lib/std` module boundaries and document beta-vs-stable APIs.
|
||||
3. Broaden numeric completeness with explicit `f32` and additional integer
|
||||
policy where needed.
|
||||
4. Improve collection and string breadth without exposing unstable ABI details.
|
||||
The detailed post-beta feature plan lives in
|
||||
`docs/POST_BETA_ROADMAP.md`. Keep this file as the short stable-readiness
|
||||
summary and use the public roadmap document when choosing the next connected
|
||||
implementation scope.
|
||||
|
||||
1. Harden monorepo release tooling, install flow, and common build/run loops
|
||||
(`1.0.0-beta.1`).
|
||||
2. Define runtime resource and host-error policy before broad IO and
|
||||
networking (released in `1.0.0-beta.2` with read-only text file
|
||||
handles plus narrow filesystem status and mutation calls).
|
||||
3. Stabilize `lib/std` module boundaries and document beta-vs-stable APIs.
|
||||
4. Improve language usability around entry points, `match`, concrete aliases,
|
||||
and concrete numeric completeness.
|
||||
5. Expand package/workspace discipline before remote registry work.
|
||||
6. Add editor-facing diagnostics and generated documentation improvements.
|
||||
7. Freeze migration/deprecation policy and cut stable only after beta feedback.
|
||||
6. Add networking only after resource/error policy is coherent.
|
||||
7. Add serialization/data-interchange helpers before richer network libraries
|
||||
(released in `1.0.0-beta.7` with compact JSON text construction and JSON
|
||||
string quoting).
|
||||
8. Promote concrete type aliases before generics so long concrete vector,
|
||||
option/result, array, JSON, and resource-handle signatures can be named
|
||||
without changing runtime representation (released in `1.0.0-beta.8`).
|
||||
9. Design generics and collection unification from real stdlib duplication
|
||||
pressure by first applying concrete aliases to existing collection/value
|
||||
facades and reserving generic/map/set directions without executable
|
||||
semantics (released in `1.0.0-beta.9`).
|
||||
10. Add editor-facing diagnostics, watch mode, and generated documentation
|
||||
improvements.
|
||||
11. Freeze migration/deprecation policy and cut stable only after beta feedback.
|
||||
|
||||
47
.llm/reviews/BETA_10_RELEASE_REVIEW.md
Normal file
47
.llm/reviews/BETA_10_RELEASE_REVIEW.md
Normal file
@ -0,0 +1,47 @@
|
||||
# 1.0.0-beta.10 Release Review
|
||||
|
||||
Status: ready for publication after the controller release gate.
|
||||
|
||||
## Verdict
|
||||
|
||||
No blocking issues found after integrating the stdlib API catalog worker and
|
||||
the compiler symbol-metadata worker.
|
||||
|
||||
## Scope Checked
|
||||
|
||||
- `docs/language/STDLIB_API.md` now lists exact exported `lib/std` helper
|
||||
signatures instead of helper names only.
|
||||
- `scripts/render-stdlib-api-doc.js` verifies exported helpers have matching
|
||||
`(fn ...)` forms, omits non-exported helpers and aliases, and normalizes
|
||||
module-local concrete aliases in public signatures.
|
||||
- `glagol symbols <file.slo|project|workspace>` emits deterministic
|
||||
`slovo.symbols` metadata for modules, imports, exports, aliases, structs,
|
||||
enums, functions, tests, spans/ranges, and workspace package labels.
|
||||
- README, roadmaps, release notes, specification text, whitepapers, and PDFs
|
||||
describe beta10 as tooling/API-discovery work only.
|
||||
- Docs do not claim executable generics, maps, sets, new runtime helpers,
|
||||
stable ABI/layout, LSP/watch protocols, or a stable stdlib API freeze.
|
||||
|
||||
## Verification
|
||||
|
||||
- `node scripts/render-stdlib-api-doc.js`
|
||||
- `cargo fmt --check`
|
||||
- `cargo check`
|
||||
- `cargo test --test symbols_beta10`
|
||||
- `cargo test --test dx_v1_7`
|
||||
- `cargo test --test cli_v1_1`
|
||||
- `cargo test --test promotion_gate`
|
||||
- `MD_TO_PDF_PACKAGE=<local md-to-pdf package path> ./scripts/render-doc-pdfs.sh`
|
||||
- `git diff --check`
|
||||
- `./scripts/release-gate.sh`
|
||||
|
||||
Final full `./scripts/release-gate.sh` result: passed docs, generated stdlib
|
||||
API catalog consistency, private-publication text checks, formatter checks, the
|
||||
full cargo test suite, ignored promotion checks, binary smoke, and LLVM smoke.
|
||||
|
||||
## Residual Risk
|
||||
|
||||
The `symbols` command is a stable-shaped beta metadata export, not a complete
|
||||
editor protocol. Future editor work still needs a separate LSP/watch contract,
|
||||
diagnostic stability policy, local package API docs, and compatibility tests
|
||||
before claiming full editor integration.
|
||||
73
.llm/reviews/BETA_11_RELEASE_REVIEW.md
Normal file
73
.llm/reviews/BETA_11_RELEASE_REVIEW.md
Normal file
@ -0,0 +1,73 @@
|
||||
# 1.0.0-beta.11 Release Review
|
||||
|
||||
Status: ready for publication after the controller release gate.
|
||||
|
||||
## Verdict
|
||||
|
||||
No blocking issues remain after controller follow-up. The original workspace
|
||||
package API leak was reproduced, fixed, and covered by a regression test before
|
||||
publication.
|
||||
|
||||
## Resolved Finding
|
||||
|
||||
- Workspace package API docs originally included compiler-loaded standard
|
||||
library modules when a workspace package imported `std.option`.
|
||||
- Controller fix: workspace package API rendering now filters package modules
|
||||
to the package source root before collecting public API sections, matching
|
||||
the existing project-mode local-root behavior. Ordinary module summaries can
|
||||
still show loaded standard-library modules, but package API sections no
|
||||
longer present those modules as package-owned API.
|
||||
- Regression added:
|
||||
`doc_workspace_package_api_excludes_loaded_std_modules`.
|
||||
|
||||
## Scope Checked
|
||||
|
||||
- `compiler/src/docgen.rs` adds per-module public API sections and package API
|
||||
sections, renders exported function signatures, struct fields, enum variants,
|
||||
and normalizes module-local aliases before public rendering.
|
||||
- `compiler/tests/doc_api_beta11.rs` covers file, project, workspace scaffold,
|
||||
workspace packages importing `std.*`, alias normalization, non-exported
|
||||
function omission, and byte-identical repeat generation for a file input.
|
||||
- README, compiler/language roadmaps, release notes, SPEC-v1, `STDLIB_API.md`,
|
||||
and the beta11 `.llm` contract include the main deferrals: no stable Markdown
|
||||
schema, no stable stdlib/API compatibility freeze, no LSP/watch, no
|
||||
SARIF/daemon protocol, no diagnostics schema policy, no executable generics,
|
||||
no maps/sets, no re-exports/globs/hierarchical modules, and no registry
|
||||
semantics.
|
||||
- Version bumps in `compiler/Cargo.toml` and `compiler/Cargo.lock` move
|
||||
`glagol` from `1.0.0-beta.10` to `1.0.0-beta.11`.
|
||||
|
||||
## Test Coverage Notes
|
||||
|
||||
- Workspace packages that import `std.*` are now covered by a focused
|
||||
regression that verifies package API excludes loaded standard-library
|
||||
modules and helpers.
|
||||
- Determinism is asserted byte-for-byte only for file docs. Project and
|
||||
workspace determinism are indirectly exercised by sorted rendering but not
|
||||
protected by repeat-generation tests.
|
||||
- Non-export filtering is asserted for functions and aliases. There is no
|
||||
explicit negative test for non-exported structs, non-exported enums, or tests
|
||||
appearing in public API sections.
|
||||
- Alias normalization is covered for file/module docs, but not for project or
|
||||
workspace package API fixtures.
|
||||
|
||||
## Verification
|
||||
|
||||
- `cargo fmt --check`: passed.
|
||||
- `cargo check`: passed.
|
||||
- `cargo test --test doc_api_beta11`: passed, 6 tests.
|
||||
- `cargo test --test dx_v1_7`: passed, 13 tests.
|
||||
- `cargo test --test symbols_beta10`: passed, 4 tests.
|
||||
- `cargo test --test promotion_gate`: passed, 1 unignored test.
|
||||
- `git diff --check`: passed.
|
||||
- Stale beta10 current-release/version scan over README, docs, `.llm`, and
|
||||
compiler package metadata: no matches.
|
||||
- Private/local publication text scan over README, docs, scripts, compiler
|
||||
source/tests, lib, examples, benchmarks, tests, and `.llm`: no matches.
|
||||
- `node scripts/render-stdlib-api-doc.js`: passed and refreshed the catalog
|
||||
release wording to `1.0.0-beta.11`.
|
||||
- Manual workspace std-import doc generation after the fix: package API no
|
||||
longer includes loaded standard-library modules.
|
||||
- `./scripts/release-gate.sh`: passed docs, generated stdlib API catalog
|
||||
consistency, private-publication text checks, formatter checks, the full cargo
|
||||
test suite, ignored promotion checks, binary smoke, and LLVM smoke.
|
||||
54
.llm/reviews/BETA_12_RELEASE_REVIEW.md
Normal file
54
.llm/reviews/BETA_12_RELEASE_REVIEW.md
Normal file
@ -0,0 +1,54 @@
|
||||
# 1.0.0-beta.12 Release Review
|
||||
|
||||
Status: ready for publication after controller release gate.
|
||||
|
||||
## Verdict
|
||||
|
||||
No blocking issues found in the current beta12 worktree. The slice is correctly
|
||||
framed as concrete vector source-helper parity and can proceed to the final
|
||||
publication gate.
|
||||
|
||||
## Scope Checked
|
||||
|
||||
- `lib/std/vec_i64.slo` exports and implements `count_of`, `starts_with`,
|
||||
`without_prefix`, `ends_with`, and `without_suffix` as ordinary source
|
||||
helpers over the existing `std.vec.i64` runtime wrappers and staged helper
|
||||
surface.
|
||||
- `lib/std/vec_f64.slo` exports and implements `count_of` as an ordinary
|
||||
source helper; the existing f64 prefix/suffix helpers remain aligned.
|
||||
- Local fixture copies, explicit `std.vec_i64` and `std.vec_f64` examples, and
|
||||
mirrored language-doc examples import and exercise the new helpers.
|
||||
- Focused helper tests, source-search tests, and promotion-gate inventories now
|
||||
require the new exports and expected `20 test(s) passed` fixture output.
|
||||
- README, roadmaps, release notes, SPEC-v1, regenerated `STDLIB_API.md`, the
|
||||
beta12 `.llm` scope note, and the compiler package version bump align on the
|
||||
`1.0.0-beta.12` concrete vector query and prefix parity scope.
|
||||
|
||||
## Findings
|
||||
|
||||
No blockers.
|
||||
|
||||
- Helper semantics match the existing concrete lanes: exact equality counts,
|
||||
empty prefix/suffix success, longer input rejection, mismatch rejection, exact
|
||||
removal to empty, and unmatched removal returning the original vector.
|
||||
- Release-facing text keeps the scope narrow: no source-language, runtime,
|
||||
typed-core, compiler-known stdlib/runtime-name, generic dispatch, map/set,
|
||||
iterator, mutable-vector, slice/view, ABI/layout, performance, or stable
|
||||
stdlib/API-freeze claim.
|
||||
- `STDLIB_API.md` is consistent with the current `lib/std` sources: 578
|
||||
exported helper signatures total, with 36 each for `std.vec_i64` and
|
||||
`std.vec_f64`.
|
||||
- Publication hygiene review found no beta12 private local path leakage in the
|
||||
touched release-facing text.
|
||||
|
||||
## Verification
|
||||
|
||||
- `cargo test --manifest-path compiler/Cargo.toml --test standard_vec_i64_source_helpers_alpha --test standard_vec_f64_source_helpers_alpha --test standard_vec_i64_source_search_alpha --test standard_vec_f64_source_search_alpha`: passed.
|
||||
- `cargo fmt --manifest-path compiler/Cargo.toml --check`: passed.
|
||||
- `git diff --check`: passed.
|
||||
- `STDLIB_API.md` was regenerated in an isolated copy and compared against the
|
||||
worktree copy: no diff.
|
||||
|
||||
- `./scripts/release-gate.sh`: passed after the beta12 release commit, including
|
||||
docs/API freshness, `cargo fmt --check`, full `cargo test`, ignored promotion
|
||||
gates, binary smoke, and LLVM smoke.
|
||||
64
.llm/reviews/BETA_13_RELEASE_REVIEW.md
Normal file
64
.llm/reviews/BETA_13_RELEASE_REVIEW.md
Normal file
@ -0,0 +1,64 @@
|
||||
# 1.0.0-beta.13 Release Review
|
||||
|
||||
Status: ready for publication after controller release gate.
|
||||
|
||||
## Verdict
|
||||
|
||||
No blockers after controller catalog-count reconciliation.
|
||||
|
||||
## Findings
|
||||
|
||||
No blocking findings remain.
|
||||
|
||||
Controller reconciliation note: the initial review used `rg -o -h ...` for the
|
||||
catalog count. On the local ripgrep version, `-h` prints help instead of
|
||||
meaning "no filename", so that command mixed help text with diagnostic codes.
|
||||
Using `--no-filename` gives the correct inventory: 358 `.diag` snapshots and
|
||||
114 unique `(code ...)` values. The Current Golden Catalog table matches that
|
||||
114-code snapshot set.
|
||||
|
||||
## Scope Checked
|
||||
|
||||
- Public docs in scope: `README.md`, `docs/POST_BETA_ROADMAP.md`,
|
||||
`docs/language/DIAGNOSTICS.md`, `docs/language/MIGRATION_POLICY.md`,
|
||||
`docs/language/SPEC-v1.md`, `docs/language/ROADMAP.md`,
|
||||
`docs/language/RELEASE_NOTES.md`, `docs/compiler/ROADMAP.md`,
|
||||
`docs/compiler/RELEASE_NOTES.md`, `docs/language/STDLIB_API.md`, and
|
||||
`.llm/BETA_13_DIAGNOSTIC_CATALOG_AND_SCHEMA_POLICY.md`.
|
||||
- Compiler/tooling in scope: `compiler/src/diag.rs`, `compiler/src/main.rs`,
|
||||
`compiler/tests/diagnostics_schema_beta13.rs`,
|
||||
`compiler/tests/diagnostics_contract.rs`, `scripts/release-gate.sh`,
|
||||
`compiler/Cargo.toml`, and `compiler/Cargo.lock`.
|
||||
- The compiler diff keeps diagnostic output shape limited to centralizing
|
||||
`slovo.diagnostic` schema name/version constants and reusing the version in
|
||||
artifact manifest diagnostics metadata.
|
||||
- `docs/language/DIAGNOSTICS.md` describes the current S-expression and JSON
|
||||
diagnostic shape, newline-delimited JSON stderr behavior, source-less JSON
|
||||
`file:null` and `span:null`, artifact-manifest diagnostic metadata,
|
||||
compatibility/migration classes, and explicit deferrals.
|
||||
- Release-facing stage wording consistently points at `1.0.0-beta.13` as the
|
||||
current stage and post-beta13 work as the next scope. Focused private-path
|
||||
and stale current-stage beta12 checks found no matches.
|
||||
|
||||
## Verification Commands And Results
|
||||
|
||||
- `cargo fmt --check`: passed.
|
||||
- `cargo test --test diagnostics_schema_beta13`: passed, 3 tests.
|
||||
- `cargo test --test diagnostics_contract current_negative_cases_match_machine_diagnostic_snapshots`:
|
||||
passed, 1 test.
|
||||
- `git diff --check`: passed.
|
||||
- `rg -o 'snapshot: "\\.\\./tests/[^"]+\\.diag"' compiler/tests/diagnostics_contract.rs | wc -l`:
|
||||
`358`.
|
||||
- `rg --files tests | rg '\\.diag$' | wc -l`: `358`.
|
||||
- `rg --no-filename -o '\\(code [A-Za-z][A-Za-z0-9]*\\)' tests/*.diag | sed -E 's/^\\(code ([^)]+)\\)$/\\1/' | sort -u | wc -l`:
|
||||
`114`.
|
||||
- Comparing the unique snapshot code set with the Current Golden Catalog code
|
||||
set produced no differences, confirming the catalog table itself matches the
|
||||
114-code snapshot set.
|
||||
- Focused `rg` check for private/local paths and stale current-stage beta12
|
||||
release wording in the scoped public docs produced no matches.
|
||||
|
||||
- `./scripts/release-gate.sh`: passed after the beta13 release commit,
|
||||
including docs/API freshness, the focused beta13 diagnostics schema test,
|
||||
`cargo fmt --check`, full `cargo test`, ignored promotion gates, binary
|
||||
smoke, and LLVM smoke.
|
||||
76
.llm/reviews/BETA_14_RELEASE_REVIEW.md
Normal file
76
.llm/reviews/BETA_14_RELEASE_REVIEW.md
Normal file
@ -0,0 +1,76 @@
|
||||
# 1.0.0-beta.14 Release Review
|
||||
|
||||
Status: ready for publication after controller release gate.
|
||||
|
||||
## Verdict
|
||||
|
||||
No blocking findings found for the benchmark suite catalog and metadata gate.
|
||||
|
||||
## Findings
|
||||
|
||||
No blocking findings.
|
||||
|
||||
Non-blocking note: `compiler/tests/benchmark_suite_catalog_beta14.rs` verifies
|
||||
byte-stable JSON, the current 10 benchmark names/directories, top-level counts,
|
||||
required scaffold-file status, missing-file lists, checksum metadata presence,
|
||||
timing mode strings, and implementation slot names. The current runner output
|
||||
also includes `run_args`, `source_stem`, and per-implementation source paths.
|
||||
If those fields become contractual in a later release, the focused gate should
|
||||
parse the JSON and assert them per benchmark.
|
||||
|
||||
## Scope Checked
|
||||
|
||||
- Public release docs in scope: `README.md`, `benchmarks/README.md`,
|
||||
`docs/POST_BETA_ROADMAP.md`, `docs/language/SPEC-v1.md`,
|
||||
`docs/language/ROADMAP.md`, `docs/language/RELEASE_NOTES.md`,
|
||||
`docs/compiler/ROADMAP.md`, `docs/compiler/RELEASE_NOTES.md`, and
|
||||
`.llm/BETA_14_BENCHMARK_SUITE_CATALOG_AND_METADATA_GATE.md`.
|
||||
- Tooling and tests in scope: `benchmarks/runner.py`,
|
||||
`compiler/tests/benchmark_suite_catalog_beta14.rs`,
|
||||
`compiler/tests/benchmark_math_loop_scaffold.rs`,
|
||||
`scripts/release-gate.sh`, `compiler/Cargo.toml`, and
|
||||
`compiler/Cargo.lock`.
|
||||
- Release-facing current-stage wording points at `1.0.0-beta.14`. Remaining
|
||||
beta13 references are historical beta13 diagnostics/release notes, the
|
||||
existing `diagnostics_schema_beta13` gate, or older review/contract files.
|
||||
- Benchmark docs and runner output consistently keep timing local-machine-only,
|
||||
do not publish timing numbers, do not define performance thresholds, and do
|
||||
not claim a stable benchmark JSON schema.
|
||||
- The top-level catalog documents the current 10 suites and all suite-list
|
||||
commands. Relative links to `benchmarks/README.md` resolve from the files
|
||||
that introduce them.
|
||||
- The runner preserves per-suite `run.py --list --json` behavior and adds root
|
||||
`benchmarks/runner.py --suite-list` and `--suite-list --json` metadata.
|
||||
- `scripts/release-gate.sh` now runs `cargo test --test
|
||||
benchmark_suite_catalog_beta14` before the full compiler test suite.
|
||||
- Cargo package version and lockfile version both read `1.0.0-beta.14`.
|
||||
- Focused private/local publication text scan found no machine-local paths,
|
||||
private checkout/user names, private remotes, or local IP text in the reviewed
|
||||
release surface.
|
||||
|
||||
## Verification Commands And Results
|
||||
|
||||
- `python3 benchmarks/runner.py --suite-list --json`: passed. Output reported
|
||||
`benchmark_count: 10`, `implementation_slots: 60`, status `ok`, cold/hot
|
||||
timing modes, required scaffold-file status with no missing files, checksum
|
||||
metadata, runtime args, implementation source paths, and the local-only
|
||||
timing disclaimer.
|
||||
- `python3 benchmarks/runner.py --suite-list`: passed.
|
||||
- `python3 benchmarks/math-loop/run.py --list --json`: passed, confirming the
|
||||
existing per-benchmark list mode still works through the shared runner.
|
||||
- `python3 benchmarks/math-loop/run.py --suite-list --json`: passed, confirming
|
||||
suite listing also resolves correctly through a per-benchmark wrapper.
|
||||
- `cargo test --test benchmark_suite_catalog_beta14`: passed, 1 test.
|
||||
- `cargo test --test benchmark_math_loop_scaffold`: passed, 1 test.
|
||||
- `git diff --check`: passed.
|
||||
- `git diff --check -- README.md benchmarks/README.md docs/POST_BETA_ROADMAP.md docs/language/SPEC-v1.md docs/language/ROADMAP.md docs/language/RELEASE_NOTES.md .llm/BETA_14_BENCHMARK_SUITE_CATALOG_AND_METADATA_GATE.md`:
|
||||
passed.
|
||||
- `bash -n scripts/release-gate.sh`: passed.
|
||||
- `cargo fmt --check`: passed.
|
||||
- Focused `rg` stale-current-stage scan for beta13 current release/stage wording
|
||||
in the touched release docs produced no matches.
|
||||
- Focused `rg` private/local publication text scan over README, docs,
|
||||
benchmarks, `.llm`, compiler tests, and scripts produced no matches.
|
||||
|
||||
The full `./scripts/release-gate.sh` was not run during this review; the
|
||||
focused beta14 gate coverage and requested lightweight checks passed.
|
||||
53
.llm/reviews/BETA_15_RELEASE_REVIEW.md
Normal file
53
.llm/reviews/BETA_15_RELEASE_REVIEW.md
Normal file
@ -0,0 +1,53 @@
|
||||
# 1.0.0-beta.15 Release Review
|
||||
|
||||
## Findings
|
||||
|
||||
No blocking findings.
|
||||
|
||||
- Ready for controller release gate. The beta15 candidate consistently scopes
|
||||
this release as reserved generic collection boundary hardening plus a
|
||||
collection ledger, without promoting executable generics, maps, sets,
|
||||
generic stdlib dispatch, runtime names, ABI/layout, or stable API behavior.
|
||||
- Evidence: the release contract preserves diagnostic shape/codes/schema while
|
||||
allowing reserved-boundary prose rewording
|
||||
(`.llm/BETA_15_RESERVED_GENERIC_COLLECTION_BOUNDARY_HARDENING.md:5`,
|
||||
`.llm/BETA_15_RESERVED_GENERIC_COLLECTION_BOUNDARY_HARDENING.md:27`,
|
||||
`.llm/BETA_15_RESERVED_GENERIC_COLLECTION_BOUNDARY_HARDENING.md:49`),
|
||||
public docs describe the same boundary
|
||||
(`README.md:86`, `docs/language/COLLECTIONS.md:12`,
|
||||
`docs/language/COLLECTIONS.md:113`), and compiler docs mirror the
|
||||
centralized-diagnostic scope (`docs/compiler/RELEASE_NOTES.md:28`,
|
||||
`docs/compiler/ROADMAP.md:64`).
|
||||
- Evidence: reserved diagnostic construction is centralized in
|
||||
`compiler/src/reserved.rs:9`, `compiler/src/reserved.rs:62`,
|
||||
`compiler/src/reserved.rs:77`, `compiler/src/reserved.rs:92`, and
|
||||
`compiler/src/reserved.rs:107`; lowerer, formatter, and checker paths import
|
||||
those helpers at `compiler/src/lower.rs:12`,
|
||||
`compiler/src/formatter.rs:6`, and `compiler/src/check.rs:10`.
|
||||
- Evidence: release gate wiring includes the focused beta15 test at
|
||||
`scripts/release-gate.sh:68`, and the version bump is present in
|
||||
`compiler/Cargo.toml:3` and `compiler/Cargo.lock:7`.
|
||||
|
||||
## Verification
|
||||
|
||||
- `cargo fmt --check`: passed.
|
||||
- `cargo test --test reserved_generic_collection_beta15`: passed, 1 test.
|
||||
- `cargo test --test diagnostics_contract`: passed, 1 test.
|
||||
- `cargo test --test formatter`: passed, 16 tests.
|
||||
- `cargo test --test project_mode`: passed, 36 tests.
|
||||
- `rg -n "not supported in beta\\.9" compiler/src tests`: passed, no matches.
|
||||
- `git diff --check`: passed.
|
||||
|
||||
Additional review scans:
|
||||
|
||||
- Current-stage stale beta14 scan across README/docs/.llm/Cargo metadata:
|
||||
passed, no current-stage matches.
|
||||
- Private/local publication text scan across README, docs, scripts, compiler
|
||||
sources/tests, lib, examples, benchmarks, tests, and `.llm`: passed, no
|
||||
matches.
|
||||
|
||||
## Residual Risk
|
||||
|
||||
Full release gate was not run by this reviewer because the requested scope was
|
||||
focused review-only. Controller should still run `./scripts/release-gate.sh`
|
||||
after accepting the candidate and before tagging/publishing.
|
||||
55
.llm/reviews/BETA_16_RELEASE_REVIEW.md
Normal file
55
.llm/reviews/BETA_16_RELEASE_REVIEW.md
Normal file
@ -0,0 +1,55 @@
|
||||
# 1.0.0-beta.16 Release Review
|
||||
|
||||
Scope: `1.0.0-beta.16 String Scanning And Token Boundary Foundation`
|
||||
|
||||
Final verdict: ready for controller commit and final publication gate. I found no remaining blocking findings in the current worktree.
|
||||
|
||||
## Findings
|
||||
|
||||
No blocking findings.
|
||||
|
||||
Previously reported blockers are resolved:
|
||||
|
||||
- The unignored `promotion_gate_artifacts_are_aligned` stack overflow is fixed. The beta16 string runtime handlers are now extracted in `compiler/src/test_runner.rs` and dispatched from the runtime-symbol branch without re-entering the old recursive path in a way that overflows the vec_i32 import fixture.
|
||||
- `.llm/BETA_16_STRING_SCANNING_AND_TOKEN_BOUNDARY_FOUNDATION.md` is now a combined Slovo/Glagol release scope rather than Slovo-side-only.
|
||||
- `lib/std/README.md` now documents `byte_at_result`, `slice_result`, `starts_with`, `ends_with`, and the beta16 non-scope boundaries.
|
||||
|
||||
## Scope Confirmation
|
||||
|
||||
Confirmed: the beta16 candidate adds byte-oriented `std.string.byte_at_result`, `std.string.slice_result`, `std.string.starts_with`, and `std.string.ends_with`, with matching source facades, runtime entries, LLVM lowering, C runtime behavior, test-runner behavior, diagnostics inventory, examples, docs, release-gate coverage, and version bump.
|
||||
|
||||
Confirmed no beta16 claim for JSON parser, Unicode scalar/grapheme/display-width semantics, language-level slice/view syntax or borrowed substring views, generic collections, maps/sets, stable ABI/layout, stable stdlib/API freeze, tokenizers, object/array parsing, mutable strings, or performance claims. Matches for those terms are deferrals or explicit non-scope statements.
|
||||
|
||||
`docs/language/STDLIB_API.md` reflects the generated beta16 surface as 582 exported helper signatures total, with `std.string` at 30 exports including the four beta16 helpers.
|
||||
|
||||
## Verification
|
||||
|
||||
Passed:
|
||||
|
||||
- `cargo fmt --check`
|
||||
- `cargo test --test standard_string_scanning_beta16`
|
||||
- `cargo test --test diagnostics_contract`
|
||||
- `cargo test --test standard_string_source_fallback_helpers_alpha`
|
||||
- `cargo test --test standard_core_facade_source_search_alpha`
|
||||
- `cargo test --test string_runtime`
|
||||
- `cargo test --test promotion_gate -- --ignored`
|
||||
- `cargo test --test promotion_gate promotion_gate_artifacts_are_aligned`
|
||||
- `cargo test`
|
||||
- `cargo test --test diagnostics_schema_beta13`
|
||||
- `cargo test --test benchmark_suite_catalog_beta14`
|
||||
- `cargo test --test reserved_generic_collection_beta15`
|
||||
- `cargo test --test binary_smoke -- --ignored`
|
||||
- `cargo test --test llvm_smoke -- --ignored`
|
||||
- `bash -n scripts/install.sh`
|
||||
- `bash -n scripts/render-stdlib-api-doc.sh`
|
||||
- `scripts/render-stdlib-api-doc.sh`
|
||||
- `git diff --check`
|
||||
- stale current-stage beta15 scan
|
||||
- local/private publication text scan across source/docs/tests/.llm
|
||||
- required PDF artifact existence check
|
||||
- `pdftotext` plus local/private publication text scan across required PDFs
|
||||
|
||||
Not run:
|
||||
|
||||
- Full `./scripts/release-gate.sh` before commit. The script intentionally requires the generated `docs/language/STDLIB_API.md` diff to already be committed, so running it on this dirty release candidate would produce a known false failure. Its cargo, shell syntax, generated-catalog, private-text, PDF, binary-smoke, and LLVM-smoke components were run manually above. Run the full script after committing beta16 and before tagging.
|
||||
|
||||
97
.llm/reviews/BETA_17_RELEASE_REVIEW.md
Normal file
97
.llm/reviews/BETA_17_RELEASE_REVIEW.md
Normal file
@ -0,0 +1,97 @@
|
||||
# 1.0.0-beta.17 Release Review
|
||||
|
||||
Scope: `1.0.0-beta.17 JSON Primitive Scalar Parsing Foundation`
|
||||
|
||||
## Verdict
|
||||
|
||||
Ready after controller follow-up. The source facade, runtime lowering,
|
||||
test-runner behavior, examples, generated API catalog, focused gates, full
|
||||
`cargo test`, and release diagnostics are coherent for the beta17 scope.
|
||||
|
||||
## Findings
|
||||
|
||||
### P2 - Resolved: unsupported standard-library diagnostics omitted the beta17 promoted JSON names
|
||||
|
||||
`compiler/src/std_runtime.rs:259` through `compiler/src/std_runtime.rs:299`
|
||||
register the six promoted beta17 helpers:
|
||||
|
||||
- `std.json.parse_bool_value_result`
|
||||
- `std.json.parse_i32_value_result`
|
||||
- `std.json.parse_u32_value_result`
|
||||
- `std.json.parse_i64_value_result`
|
||||
- `std.json.parse_u64_value_result`
|
||||
- `std.json.parse_f64_value_result`
|
||||
|
||||
`compiler/src/std_runtime.rs:875` hard-coded the
|
||||
`UnsupportedStandardLibraryCall.expected` text with only
|
||||
`std.json.quote_string` in the JSON family. A user who called an unsupported
|
||||
`std.*` name would receive stale guidance that omitted the newly supported
|
||||
beta17 helpers.
|
||||
|
||||
Controller resolution: the six beta17 `std.json.parse_*_value_result` names
|
||||
are now present in the expected supported-call list, and
|
||||
`compiler/tests/standard_json_scalar_parsing_beta17.rs` includes
|
||||
`unsupported_json_diagnostics_list_beta17_promoted_scalar_parsers` to fail if
|
||||
that user-facing guidance drifts again.
|
||||
|
||||
No P0/P1/P2 runtime, source-facade, grammar, lowering, diagnostics, or
|
||||
release-gate wiring findings remain open.
|
||||
|
||||
## Verification
|
||||
|
||||
Inspected:
|
||||
|
||||
- `lib/std/json.slo` and both JSON example fixtures.
|
||||
- `compiler/src/std_runtime.rs`, `compiler/src/llvm.rs`, and
|
||||
`compiler/src/test_runner.rs`.
|
||||
- `runtime/runtime.c`.
|
||||
- `compiler/tests/standard_json_scalar_parsing_beta17.rs`,
|
||||
`compiler/tests/standard_json_source_facade_alpha.rs`, and
|
||||
`compiler/tests/promotion_gate.rs`.
|
||||
- `scripts/release-gate.sh`, version bump, release notes, roadmaps, spec,
|
||||
standard-runtime docs, and generated `docs/language/STDLIB_API.md`.
|
||||
|
||||
Ran:
|
||||
|
||||
- `git -C slovo diff --check` - passed.
|
||||
- `cargo fmt --check` - passed.
|
||||
- `cargo test --test standard_json_scalar_parsing_beta17` - passed, 5/5.
|
||||
- `cargo test --test standard_json_source_facade_alpha` - passed, 2/2.
|
||||
- `cargo test --test standard_json` - passed, 4/4.
|
||||
- `cargo test --test diagnostics_contract` - passed, 1/1.
|
||||
- `cargo test --test promotion_gate` - passed, 1/1 with 2 ignored.
|
||||
- `cargo test --test promotion_gate -- --ignored` - passed, 2/2.
|
||||
- `cargo test` - passed.
|
||||
- Controller follow-up `cargo test --test standard_json_scalar_parsing_beta17`
|
||||
- passed after the diagnostic inventory fix.
|
||||
|
||||
Not run:
|
||||
|
||||
- Full `./scripts/release-gate.sh`; run it after final controller cleanup. Its
|
||||
core `cargo test`, fmt, promotion, and beta17-focused components were
|
||||
covered above, but the full script also runs documentation/private-text
|
||||
checks and ignored binary/LLVM smoke gates.
|
||||
|
||||
## Residual Risks And Non-Scope Confirmation
|
||||
|
||||
Confirmed in-scope behavior:
|
||||
|
||||
- `std.json.parse_bool_value_result`, numeric concrete
|
||||
`parse_*_value_result` helpers, and source-only `parse_null_value_result`
|
||||
are present in `lib/std/json.slo` and mirrored by the local JSON fixture.
|
||||
- Promoted bool/numeric helpers lower to private runtime symbols and have
|
||||
matching test-runner and hosted-runtime behavior.
|
||||
- Primitive JSON token checks reject whitespace, leading `+`, leading-zero
|
||||
forms such as `01`/`01.0`, negative unsigned integers, overflow, and
|
||||
non-finite f64 results. Exponent f64 is accepted. `-0` is accepted
|
||||
consistently as a JSON-valid number token, not treated as a leading-zero
|
||||
form.
|
||||
- `parse_null_value_result` remains source-only; no compiler-known null parser
|
||||
was introduced.
|
||||
|
||||
Confirmed non-scope:
|
||||
|
||||
- No JSON string parser, object parser, array parser, recursive `JsonValue`,
|
||||
tokenizer, schema validator, stream parser, Unicode escape parser, map/set
|
||||
surface, stable ABI/layout claim, stable API freeze, or performance claim is
|
||||
introduced by this candidate.
|
||||
67
.llm/reviews/BETA_18_RELEASE_REVIEW.md
Normal file
67
.llm/reviews/BETA_18_RELEASE_REVIEW.md
Normal file
@ -0,0 +1,67 @@
|
||||
# Beta 18 Release Review: JSON String Token Parsing Foundation
|
||||
|
||||
Reviewer: beta18 release reviewer
|
||||
Date: 2026-05-23
|
||||
Repo: Slovo monorepo
|
||||
|
||||
## Findings
|
||||
|
||||
### Blocking: unrelated test-runner stack wrapper drift is present in the beta18 worktree
|
||||
|
||||
- `compiler/src/main.rs:27`
|
||||
- `compiler/src/main.rs:30`
|
||||
- `compiler/src/main.rs:57`
|
||||
|
||||
The dirty worktree includes a broad `Mode::RunTests` execution change that imports
|
||||
`std::thread`, defines a 16 MiB test-runner stack size, and runs every `glagol test`
|
||||
invocation inside a spawned thread. That is not part of the documented beta18 JSON
|
||||
string-token parsing scope and is not covered by the beta18 release notes, runtime
|
||||
contract, `.llm/BETA_18_JSON_STRING_TOKEN_PARSING_FOUNDATION.md`, or a targeted test
|
||||
that justifies changing CLI execution behavior.
|
||||
|
||||
This is release-blocking for beta18 as scoped. Either remove this drift from the
|
||||
beta18 release, or promote it into an explicit separate scope with release notes,
|
||||
docs, tests for panic/reporting behavior, and a clear reason for changing the
|
||||
test-runner resource model.
|
||||
|
||||
## Non-Blocking Observations
|
||||
|
||||
- The JSON string-token parser surface is coherent across `lib/std/json.slo`,
|
||||
`examples/projects/std-layout-local-json/src/json.slo`, compiler runtime
|
||||
registration, LLVM lowering, test-runner emulation, hosted C runtime, generated
|
||||
standard-library API docs, and source-facade examples.
|
||||
- The promoted signature is consistently `(string) -> (result string i32)`.
|
||||
- The documented contract is narrow and explicit: one isolated ASCII JSON string
|
||||
token, exact quotes, simple JSON escapes, `err 1` for ordinary parse failure,
|
||||
and deferred Unicode/full JSON parsing.
|
||||
- `docs/compiler/RELEASE_NOTES.md:81` still says beta17 did not implement JSON
|
||||
string parsing. That appears historical and scoped to the beta17 section, not a
|
||||
stale beta18 claim.
|
||||
|
||||
## Verification Run
|
||||
|
||||
Passed:
|
||||
|
||||
- `cargo fmt --check`
|
||||
- `cargo test --test standard_json_string_parsing_beta18`
|
||||
- `cargo test --test standard_json_scalar_parsing_beta17`
|
||||
- `cargo test --test standard_json_source_facade_alpha`
|
||||
- `cargo test --test promotion_gate`
|
||||
- `cargo test --test diagnostics_contract`
|
||||
- `cargo test`
|
||||
- `git diff --check`
|
||||
- `git grep -nE '(/home/[[:alnum:]_.-]+|sanjin[0-9]+|[0-9]{1,3}\.64\.0\.1|git\.hermeticum\.io)' -- README.md CONTRIBUTING.md docs scripts compiler lib examples benchmarks tests .llm`
|
||||
|
||||
Not run:
|
||||
|
||||
- `./scripts/release-gate.sh`; the controller should run the full release gate
|
||||
after resolving the blocking drift and committing generated docs/source changes,
|
||||
because the gate intentionally checks generated `docs/language/STDLIB_API.md`,
|
||||
ignored promotion gates, binary smoke, and LLVM smoke.
|
||||
|
||||
## Verdict
|
||||
|
||||
Not release-ready yet because of the unrelated `compiler/src/main.rs` test-runner
|
||||
stack wrapper drift. The beta18 JSON string-token parsing implementation itself did
|
||||
not show blocking defects in this review, and the focused plus full Rust test stack
|
||||
passed.
|
||||
24
.llm/reviews/BETA_18_RELEASE_REVIEW_DISPOSITION.md
Normal file
24
.llm/reviews/BETA_18_RELEASE_REVIEW_DISPOSITION.md
Normal file
@ -0,0 +1,24 @@
|
||||
# Beta 18 Release Review Disposition
|
||||
|
||||
Reviewer file: `.llm/reviews/BETA_18_RELEASE_REVIEW.md`
|
||||
|
||||
## Blocking Finding Disposition
|
||||
|
||||
The reviewer correctly flagged `compiler/src/main.rs` test-runner stack/thread
|
||||
hardening as release drift while it was undocumented. The controller kept the
|
||||
change because `cargo test --test promotion_gate` reproduced a host stack
|
||||
overflow in `std-layout-local-vec_i32` without it, making the release gate
|
||||
unreliable.
|
||||
|
||||
Resolution:
|
||||
|
||||
- `docs/compiler/RELEASE_NOTES.md` now lists the bounded worker-stack behavior
|
||||
in the `1.0.0-beta.18` summary.
|
||||
- `docs/compiler/ROADMAP.md` now records the same behavior as beta18
|
||||
test-runner hardening.
|
||||
- `.llm/BETA_18_JSON_STRING_TOKEN_PARSING_FOUNDATION.md` now marks it as
|
||||
gate-supporting compiler hardening, not a new language feature.
|
||||
- `cargo test --test promotion_gate` passes after the fix and failed with host
|
||||
stack overflow before the fix.
|
||||
|
||||
Verdict after disposition: release may proceed if the full gate stack passes.
|
||||
55
.llm/reviews/BETA_19_RELEASE_REVIEW.md
Normal file
55
.llm/reviews/BETA_19_RELEASE_REVIEW.md
Normal file
@ -0,0 +1,55 @@
|
||||
# 1.0.0-beta.19 Release Review
|
||||
|
||||
Scope: Test Discovery And User-Project Conformance Foundation
|
||||
|
||||
## Findings
|
||||
|
||||
No blocking findings.
|
||||
|
||||
The implementation matches the beta19 contract at the release-blocking level:
|
||||
`glagol test --list <file|project|workspace>` and legacy
|
||||
`glagol --run-tests --list <file>` route through checked discovery, avoid test
|
||||
body evaluation, preserve the existing discovery ordering, honor
|
||||
`--filter <substring>`, keep ordinary test output unchanged, and are wired into
|
||||
the release gate through `cargo test --test test_discovery_beta19`.
|
||||
|
||||
## Non-Blocking Notes
|
||||
|
||||
- Resolved during controller integration: unfiltered list output now prints the
|
||||
same summary suffix as filtered list output, including `total_discovered`,
|
||||
`selected`, `passed`, `failed`, `skipped`, and `filter none`.
|
||||
- Resolved during controller integration: the grammar typo in
|
||||
`docs/POST_BETA_ROADMAP.md` was corrected.
|
||||
|
||||
## Verification Notes
|
||||
|
||||
Inspected:
|
||||
|
||||
- Working tree status and beta19 diff across CLI parsing/dispatch, project test
|
||||
mode, test-runner listing, focused tests, docs, version files, and
|
||||
`scripts/release-gate.sh`.
|
||||
- Contract drift against
|
||||
`.llm/BETA_19_TEST_DISCOVERY_AND_CONFORMANCE.md`,
|
||||
`docs/language/SPEC-v1.md`, release notes, roadmaps, and README beta scope.
|
||||
- Cached diff status; no cached beta19 changes were present.
|
||||
|
||||
Read-only checks run:
|
||||
|
||||
- `git diff --check` - passed.
|
||||
- `git diff --cached --check` - passed.
|
||||
- Stale-version scan for beta18/beta19 references - no blocking stale current
|
||||
release references found.
|
||||
- Conflict-marker and trailing-whitespace scans for the untracked beta19
|
||||
contract/test files - passed.
|
||||
|
||||
Not run:
|
||||
|
||||
- `cargo fmt --check`, focused cargo tests, full `cargo test`, and
|
||||
`./scripts/release-gate.sh`; those commands write build artifacts, and this
|
||||
review was constrained to read-only commands except for the review file.
|
||||
|
||||
## Verdict
|
||||
|
||||
Release-ready from this review. No blocking beta19 issues remain in the current
|
||||
working tree diff. The controller should still run the focused beta19 test suite
|
||||
and full release gate before tagging.
|
||||
95
.llm/reviews/BETA_20_RELEASE_REVIEW.md
Normal file
95
.llm/reviews/BETA_20_RELEASE_REVIEW.md
Normal file
@ -0,0 +1,95 @@
|
||||
# 1.0.0-beta.20 Release Review
|
||||
|
||||
Scope: String Search And ASCII Trim Foundation
|
||||
|
||||
## Findings
|
||||
|
||||
No blocking findings.
|
||||
|
||||
The beta20 diff matches the documented release contract at the
|
||||
release-blocking level. `lib/std/string.slo:1` exports all six new helpers, and
|
||||
the implementations remain ordinary source helpers over the existing string
|
||||
facade primitives: first search at `lib/std/string.slo:30`, last search at
|
||||
`lib/std/string.slo:47`, `contains` at `lib/std/string.slo:64`, and ASCII trim
|
||||
at `lib/std/string.slo:71`. The local mirror in
|
||||
`examples/projects/std-layout-local-string/src/string.slo:1` exposes the same
|
||||
surface without changing the public contract shape.
|
||||
|
||||
The example coverage is explicit and scoped correctly. The standard import
|
||||
example covers present, missing, empty-needle, first/last index, leading trim,
|
||||
trailing trim, full trim, all-whitespace trim, and no-trim cases at
|
||||
`examples/projects/std-import-string/src/main.slo:204`; the documentation copy
|
||||
matches at `docs/language/examples/projects/std-import-string/src/main.slo:204`;
|
||||
the local fixture mirrors the same behavior at
|
||||
`examples/projects/std-layout-local-string/src/main.slo:204`.
|
||||
|
||||
The compiler gate coverage is aligned with the non-scope. The focused beta20
|
||||
test builds an explicit `std.string` import fixture at
|
||||
`compiler/tests/standard_string_search_trim_beta20.rs:47`, asserts the helpers
|
||||
are source-authored at `compiler/tests/standard_string_search_trim_beta20.rs:178`,
|
||||
and rejects direct compiler-known runtime calls for the new names. The existing
|
||||
local-string fallback test now expects and inventories the new helpers at
|
||||
`compiler/tests/standard_string_source_fallback_helpers_alpha.rs:8` and
|
||||
`compiler/tests/standard_string_source_fallback_helpers_alpha.rs:24`.
|
||||
The repo-root standard-source import gate and promotion gate are also aligned at
|
||||
`compiler/tests/standard_core_facade_source_search_alpha.rs:8`,
|
||||
`compiler/tests/standard_core_facade_source_search_alpha.rs:33`,
|
||||
`compiler/tests/promotion_gate.rs:1318`, and
|
||||
`compiler/tests/promotion_gate.rs:7368`.
|
||||
|
||||
## Contract Drift
|
||||
|
||||
No blocking contract drift found.
|
||||
|
||||
The README, language release notes, language roadmap, v1 spec, compiler release
|
||||
notes, compiler roadmap, post-beta roadmap, stdlib README, and beta20 `.llm`
|
||||
contract all describe the same narrow surface: byte-oriented search, empty
|
||||
needle behavior, ASCII-only trimming over bytes `9`, `10`, `11`, `12`, `13`,
|
||||
and `32`, no new compiler-known runtime names, no runtime C work, no new source
|
||||
syntax, and no Unicode, regex, tokenizer, mutable-string, stable ABI/layout, or
|
||||
stable stdlib/API claims. Representative anchors: `README.md:144`,
|
||||
`docs/language/RELEASE_NOTES.md:39`, `docs/language/ROADMAP.md:80`,
|
||||
`docs/language/SPEC-v1.md:232`, `docs/compiler/RELEASE_NOTES.md:15`,
|
||||
`docs/compiler/ROADMAP.md:100`, `docs/POST_BETA_ROADMAP.md:111`,
|
||||
`lib/std/README.md:233`, and
|
||||
`.llm/BETA_20_STRING_SEARCH_AND_ASCII_TRIM_FOUNDATION.md:1`.
|
||||
|
||||
The version and generated catalog updates are coherent: `compiler/Cargo.toml:3`
|
||||
and `compiler/Cargo.lock` are bumped to `1.0.0-beta.20`,
|
||||
`docs/language/STDLIB_API.md:18` reports 596 exported signatures, and
|
||||
`docs/language/STDLIB_API.md:486` lists 36 `std.string` signatures including
|
||||
the six beta20 helpers. `scripts/release-gate.sh:73` wires the focused beta20
|
||||
test into the release gate.
|
||||
|
||||
## Verification Notes
|
||||
|
||||
Inspected:
|
||||
|
||||
- Working tree diff and untracked beta20 contract/test files for std source,
|
||||
explicit std/local examples, docs, compiler tests, generated catalog, version
|
||||
bump, and release-gate integration.
|
||||
- Contract drift against README, language roadmap/spec/release notes, compiler
|
||||
roadmap/release notes, post-beta roadmap, stdlib README, and the beta20 `.llm`
|
||||
contract.
|
||||
- Sibling `glagol` repository status; no sibling worktree changes were present.
|
||||
|
||||
Read-only checks run:
|
||||
|
||||
- `git diff --check` - passed.
|
||||
- `git diff --cached --check` - passed.
|
||||
- `bash -n scripts/release-gate.sh` - passed.
|
||||
- `cargo fmt --check --manifest-path compiler/Cargo.toml` - passed.
|
||||
- Local/private publication text scan over source/docs/tests/.llm with build
|
||||
artifacts excluded - passed.
|
||||
|
||||
Not run:
|
||||
|
||||
- Focused cargo tests, full `cargo test`, and `./scripts/release-gate.sh`.
|
||||
Those commands write build/generated artifacts, and this review was
|
||||
constrained to read-only commands except for the review file.
|
||||
|
||||
## Verdict
|
||||
|
||||
Release-ready from this review. No blocking beta20 issues remain in the current
|
||||
working tree diff. The controller should still run the focused beta20 test stack
|
||||
and full release gate before tagging.
|
||||
25
.llm/reviews/BETA_21_IMPLEMENTATION_NOTES.md
Normal file
25
.llm/reviews/BETA_21_IMPLEMENTATION_NOTES.md
Normal file
@ -0,0 +1,25 @@
|
||||
# 1.0.0-beta.21 Glagol Implementation Notes
|
||||
|
||||
Scope: JSON Document Scalar Parsing Foundation.
|
||||
|
||||
This Glagol-side slice prepares compiler/test/release-gate coverage for the
|
||||
source-authored `std.json` document scalar helpers:
|
||||
|
||||
- `parse_string_document_result`
|
||||
- `parse_bool_document_result`
|
||||
- `parse_i32_document_result`
|
||||
- `parse_u32_document_result`
|
||||
- `parse_i64_document_result`
|
||||
- `parse_u64_document_result`
|
||||
- `parse_f64_document_result`
|
||||
- `parse_null_document_result`
|
||||
|
||||
The focused beta21 test uses an explicit `std.json` import, checks/formats/tests
|
||||
the imported helpers, requires the helpers to exist in `lib/std/json.slo`, and
|
||||
asserts that no `std.json.parse_*_document_result` compiler-known calls or
|
||||
private `__glagol_json_*document*` runtime symbols are introduced.
|
||||
|
||||
The JSON source-facade and promotion-gate inventories are updated to match the
|
||||
Slovo-side source/export/example work, which adds three JSON document scalar
|
||||
fixture tests and raises local and explicit `std.json` fixture output from 9 to
|
||||
12 tests.
|
||||
52
.llm/reviews/BETA_21_RELEASE_REVIEW.md
Normal file
52
.llm/reviews/BETA_21_RELEASE_REVIEW.md
Normal file
@ -0,0 +1,52 @@
|
||||
# 1.0.0-beta.21 Release Review
|
||||
|
||||
Scope: JSON Document Scalar Parsing Foundation.
|
||||
|
||||
## Findings
|
||||
|
||||
No blocking findings.
|
||||
|
||||
The uncommitted beta21 candidate matches the release contract:
|
||||
|
||||
- `lib/std/json.slo` exports all eight source-authored
|
||||
`parse_*_document_result` helpers.
|
||||
- Each document helper trims the whole input with `trim_ascii`, then delegates
|
||||
to the existing exact scalar value-token parser for the matching family.
|
||||
- Direct compiler-known `std.json.parse_*_document_result` calls are not
|
||||
introduced, and no private `__glagol_json_*document*` runtime symbol is
|
||||
introduced.
|
||||
- The explicit `std.json` example imports repo-root `std.json` and covers
|
||||
trimmed success, plain success, trailing non-whitespace failure, scalar
|
||||
token parsing, fields, arrays, objects, and 12 tests.
|
||||
- The local JSON example imports local `json`, the local `json` fixture imports
|
||||
local `string (trim_ascii)`, and it mirrors the same 12-test coverage.
|
||||
- Compiler coverage includes the focused beta21 test, JSON facade inventory,
|
||||
promotion-gate alignment, release-gate wiring, package version bump, and the
|
||||
generated standard-library API catalog signatures.
|
||||
- Release docs describe `1.0.0-beta.21` as released, and the source/docs scan
|
||||
did not find local/private publication text.
|
||||
|
||||
## Verification
|
||||
|
||||
Commands run:
|
||||
|
||||
```bash
|
||||
git diff --check
|
||||
git diff --cached --check
|
||||
bash -n scripts/release-gate.sh
|
||||
cargo fmt --check --manifest-path compiler/Cargo.toml
|
||||
cargo test --test standard_json_document_scalar_parsing_beta21
|
||||
cargo test --test standard_json_source_facade_alpha
|
||||
cargo test --test promotion_gate -- promotion_gate_artifacts_are_aligned
|
||||
```
|
||||
|
||||
Results:
|
||||
|
||||
- `standard_json_document_scalar_parsing_beta21`: 2 passed.
|
||||
- `standard_json_source_facade_alpha`: 2 passed.
|
||||
- `promotion_gate_artifacts_are_aligned`: 1 passed.
|
||||
- Formatting, shell syntax, and diff whitespace checks passed.
|
||||
- Targeted stale-release/private-text scans over source/docs paths passed.
|
||||
|
||||
Not run by design for this review: full `cargo test`, ignored smoke tests, and
|
||||
the full release gate.
|
||||
45
.llm/reviews/BETA_22_RELEASE_REVIEW.md
Normal file
45
.llm/reviews/BETA_22_RELEASE_REVIEW.md
Normal file
@ -0,0 +1,45 @@
|
||||
# Beta22 Release Review
|
||||
|
||||
Verdict: PASS with notes.
|
||||
|
||||
## Findings
|
||||
|
||||
No blocking or non-blocking findings in the reviewed scope.
|
||||
|
||||
## Scope Reviewed
|
||||
|
||||
- `compiler/src/main.rs` run manifest rendering and exit behavior.
|
||||
- `compiler/tests/run_manifest_beta22.rs` focused coverage.
|
||||
- `.llm/BETA_22_RUN_MANIFEST_AND_EXECUTION_REPORT_HARDENING.md` beta22 contract and suggested gates.
|
||||
- `README.md`, `docs/compiler/RELEASE_NOTES.md`, `docs/compiler/ROADMAP.md`, `docs/language/RELEASE_NOTES.md`, `docs/language/ROADMAP.md`, and `docs/language/SPEC-v1.md` release claims.
|
||||
- `scripts/release-gate.sh` inclusion.
|
||||
|
||||
## Acceptance Checklist
|
||||
|
||||
- PASS: `glagol run --manifest <path>` writes additive run execution evidence after a supported program executes. The run path captures child stdout/stderr/status before manifest rendering and exits with the child status in `compiler/src/main.rs:892` through `compiler/src/main.rs:918`; rendering emits `(run-report ...)` in `compiler/src/main.rs:2162` through `compiler/src/main.rs:2188`.
|
||||
- PASS: Run-report records exit status, captured stdout, captured stderr, and forwarded args. Tests cover success stdout and args in `compiler/tests/run_manifest_beta22.rs:10` through `compiler/tests/run_manifest_beta22.rs:60`, and nonzero exit plus stderr preservation in `compiler/tests/run_manifest_beta22.rs:62` through `compiler/tests/run_manifest_beta22.rs:128`.
|
||||
- PASS: Existing manifest fields and schema/version markers remain compatible. The new path passes `Some(run_report)` only through the run-specific wrapper in `compiler/src/main.rs:2019` through `compiler/src/main.rs:2043`; the renderer still emits `slovo.artifact-manifest` version `1` in `compiler/src/main.rs:2067` through `compiler/src/main.rs:2069`.
|
||||
- PASS: Source failures do not receive fake run evidence. The focused test asserts no run-report for a type failure in `compiler/tests/run_manifest_beta22.rs:130` through `compiler/tests/run_manifest_beta22.rs:161`.
|
||||
- PASS: Non-run modes are outside the run-report requirement. Existing foreign-import/project manifest wrappers pass `None` for the run-report slot in `compiler/src/main.rs:1992` through `compiler/src/main.rs:2017`; beta22 docs state this explicitly in `.llm/BETA_22_RUN_MANIFEST_AND_EXECUTION_REPORT_HARDENING.md:58` and `docs/compiler/RELEASE_NOTES.md:35`.
|
||||
- PASS: Documentation does not overclaim language, stdlib, runtime, package, ABI, or stable manifest-schema changes. Representative deferrals are present in `README.md:172` through `README.md:178`, `docs/compiler/ROADMAP.md:118` through `docs/compiler/ROADMAP.md:124`, `docs/language/RELEASE_NOTES.md:52` through `docs/language/RELEASE_NOTES.md:62`, `docs/language/ROADMAP.md:99` through `docs/language/ROADMAP.md:106`, and `docs/language/SPEC-v1.md:260` through `docs/language/SPEC-v1.md:267`.
|
||||
- PASS: The beta22 contract suggested gate names match the implemented focused test and release gate. `.llm/BETA_22_RUN_MANIFEST_AND_EXECUTION_REPORT_HARDENING.md:63` through `.llm/BETA_22_RUN_MANIFEST_AND_EXECUTION_REPORT_HARDENING.md:71` lists `cargo test --test run_manifest_beta22`; `scripts/release-gate.sh:75` includes the same focused test.
|
||||
|
||||
## Verification
|
||||
|
||||
Ran:
|
||||
|
||||
```bash
|
||||
cargo test --test run_manifest_beta22
|
||||
```
|
||||
|
||||
Result: passed, 3 tests passed.
|
||||
|
||||
Recommended before release tag:
|
||||
|
||||
```bash
|
||||
cargo fmt --check
|
||||
./scripts/release-gate.sh
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Note: the focused hosted-run tests depend on Clang discovery, matching existing project patterns. In this local run they executed and passed rather than skipping.
|
||||
54
.llm/reviews/BETA_23_RELEASE_REVIEW.md
Normal file
54
.llm/reviews/BETA_23_RELEASE_REVIEW.md
Normal file
@ -0,0 +1,54 @@
|
||||
# Beta23 Release Review
|
||||
|
||||
Verdict: PASS with notes.
|
||||
|
||||
## Findings
|
||||
|
||||
No blocking or non-blocking findings in the reviewed scope.
|
||||
|
||||
## Scope Reviewed
|
||||
|
||||
- `scripts/render-stdlib-api-doc.js` tier rendering.
|
||||
- `scripts/check-stdlib-api-tiers.js` tier gate.
|
||||
- `scripts/release-gate.sh` integration.
|
||||
- Generated `docs/language/STDLIB_API.md`.
|
||||
- `docs/language/STDLIB_TIERS.md`, README, release notes, roadmaps, and spec updates.
|
||||
- Compiler package version bump and compiler release documentation.
|
||||
|
||||
## Acceptance Checklist
|
||||
|
||||
- PASS: `docs/language/STDLIB_TIERS.md` defines the public tier labels and explains the current beta meaning. See `docs/language/STDLIB_TIERS.md:21` through `docs/language/STDLIB_TIERS.md:25`.
|
||||
- PASS: The tier ledger marks JSON, loopback networking, random/time, and filesystem resource-handle helpers as experimental domains. See `docs/language/STDLIB_TIERS.md:46` through `docs/language/STDLIB_TIERS.md:49`.
|
||||
- PASS: The tier ledger records concrete vector modules as beta-supported concrete lanes without claiming generic collection stability. See `docs/language/STDLIB_TIERS.md:44`.
|
||||
- PASS: The generated API catalog emits tier metadata and summary counts. See `docs/language/STDLIB_API.md:6` through `docs/language/STDLIB_API.md:24`.
|
||||
- PASS: Generated catalog classification matches the beta23 contract: filesystem handle helpers are experimental in `docs/language/STDLIB_API.md:115` through `docs/language/STDLIB_API.md:119`; `std.json`, `std.net`, `std.random`, and `std.time` are experimental in `docs/language/STDLIB_API.md:195` through `docs/language/STDLIB_API.md:240`, `docs/language/STDLIB_API.md:294` through `docs/language/STDLIB_API.md:308`, `docs/language/STDLIB_API.md:425` through `docs/language/STDLIB_API.md:432`, and `docs/language/STDLIB_API.md:540` through `docs/language/STDLIB_API.md:547`.
|
||||
- PASS: Concrete vector modules retain explicit no-generic-collection-freeze notes in generated output. Representative coverage is `docs/language/STDLIB_API.md:549` through `docs/language/STDLIB_API.md:554`, with the same note repeated for the other concrete `std.vec_*` modules.
|
||||
- PASS: The renderer implements the tier map and emits per-module/per-helper tiers. See `scripts/render-stdlib-api-doc.js:12` through `scripts/render-stdlib-api-doc.js:46`, `scripts/render-stdlib-api-doc.js:245` through `scripts/render-stdlib-api-doc.js:263`, and `scripts/render-stdlib-api-doc.js:297` through `scripts/render-stdlib-api-doc.js:330`.
|
||||
- PASS: The tier gate checks stale wording, summary tier definitions, experimental module/helper coverage, and vector boundary notes. See `scripts/check-stdlib-api-tiers.js:47` through `scripts/check-stdlib-api-tiers.js:80`.
|
||||
- PASS: `scripts/release-gate.sh` runs syntax checks for both tier scripts and executes the tier checker after catalog rendering. See `scripts/release-gate.sh:12` through `scripts/release-gate.sh:16`.
|
||||
- PASS: README, `lib/std/README.md`, language release notes, language roadmap, post-beta roadmap, and v1 spec link or describe the tier ledger and non-scope. Representative references: `README.md:69` through `README.md:79`, `lib/std/README.md:174` through `lib/std/README.md:184`, `docs/language/RELEASE_NOTES.md:53` through `docs/language/RELEASE_NOTES.md:69`, `docs/language/ROADMAP.md:109` through `docs/language/ROADMAP.md:119`, `docs/POST_BETA_ROADMAP.md:121` through `docs/POST_BETA_ROADMAP.md:132`, and `docs/language/SPEC-v1.md:268` through `docs/language/SPEC-v1.md:279`.
|
||||
- PASS: Documentation states beta23 is documentation/catalog tooling clarity only and does not add language, stdlib, runtime, stable schema, ABI/layout, or stable API behavior. See `docs/language/STDLIB_TIERS.md:10` through `docs/language/STDLIB_TIERS.md:15` and `docs/language/STDLIB_TIERS.md:52` through `docs/language/STDLIB_TIERS.md:68`.
|
||||
- PASS: Compiler package version and compiler release docs are aligned to `1.0.0-beta.23`. See `compiler/Cargo.toml:3`, `compiler/Cargo.lock:7`, `docs/compiler/RELEASE_NOTES.md:15` through `docs/compiler/RELEASE_NOTES.md:43`, and `docs/compiler/ROADMAP.md:126` through `docs/compiler/ROADMAP.md:132`.
|
||||
|
||||
## Verification
|
||||
|
||||
Ran:
|
||||
|
||||
```bash
|
||||
node --check scripts/render-stdlib-api-doc.js
|
||||
node --check scripts/check-stdlib-api-tiers.js
|
||||
./scripts/check-stdlib-api-tiers.js
|
||||
git diff --check
|
||||
rg -n '^- `experimental`' docs/language/STDLIB_API.md
|
||||
```
|
||||
|
||||
Result: all focused checks passed. The `rg` inspection confirmed 58 experimental helper signatures across the expected filesystem-handle, JSON, loopback networking, random, and time surfaces.
|
||||
|
||||
Recommended before release tag:
|
||||
|
||||
```bash
|
||||
./scripts/render-stdlib-api-doc.sh
|
||||
./scripts/release-gate.sh
|
||||
```
|
||||
|
||||
Note: run the full release gate after the generated catalog changes are staged or committed; the gate intentionally fails if `docs/language/STDLIB_API.md` has unstaged generated-doc drift.
|
||||
66
.llm/reviews/BETA_24_RELEASE_REVIEW.md
Normal file
66
.llm/reviews/BETA_24_RELEASE_REVIEW.md
Normal file
@ -0,0 +1,66 @@
|
||||
# Beta24 Release Review
|
||||
|
||||
Verdict: PASS.
|
||||
|
||||
## Findings
|
||||
|
||||
No blocking or non-blocking findings in the reviewed scope.
|
||||
|
||||
## Scope Reviewed
|
||||
|
||||
- `compiler/src/project.rs` package manifest diagnostics.
|
||||
- `compiler/tests/package_workspace_discipline_beta24.rs`.
|
||||
- `scripts/release-gate.sh` beta24 focused gate entry.
|
||||
- `compiler/Cargo.toml` and `compiler/Cargo.lock` version bump.
|
||||
- `.llm/BETA_24_PACKAGE_MANIFEST_IDENTITY_AND_DEPENDENCY_DISCIPLINE.md`.
|
||||
- README, language package/diagnostic/release/roadmap docs, compiler release/roadmap docs, and post-beta roadmap updates.
|
||||
|
||||
## Acceptance Checklist
|
||||
|
||||
- PASS: Duplicate package manifest keys now use package-scoped diagnostics through `set_manifest_key(..., "PackageManifestInvalid", "package")` for package identity keys. See `compiler/src/project.rs:1505` through `compiler/src/project.rs:1545` and `compiler/src/project.rs:1752` through `compiler/src/project.rs:1771`.
|
||||
- PASS: Invalid dependency keys are rejected before graph validation with `InvalidPackageDependencyName`. See `compiler/src/project.rs:1556` through `compiler/src/project.rs:1572`.
|
||||
- PASS: Duplicate dependency keys are rejected before graph validation with `DuplicatePackageDependencyName`, and invalid/duplicate dependency records are not added to the dependency graph. See `compiler/src/project.rs:1573` through `compiler/src/project.rs:1593`.
|
||||
- PASS: Existing local-path-only dependency parsing and later dependency key/name matching remain in place. See `compiler/src/project.rs:1594` through `compiler/src/project.rs:1601`, `compiler/src/project.rs:1743` through `compiler/src/project.rs:1749`, and `compiler/src/project.rs:2970` through `compiler/src/project.rs:3005`.
|
||||
- PASS: Focused tests cover duplicate package keys, invalid dependency keys, duplicate dependency keys, and a clean local dependency workspace. See `compiler/tests/package_workspace_discipline_beta24.rs:11` through `compiler/tests/package_workspace_discipline_beta24.rs:109`.
|
||||
- PASS: `scripts/release-gate.sh` runs the beta24 focused test before full `cargo test`. See `scripts/release-gate.sh:68` through `scripts/release-gate.sh:82`.
|
||||
- PASS: The compiler package version is aligned to `1.0.0-beta.24` in both Cargo files. See `compiler/Cargo.toml:3` and `compiler/Cargo.lock:7`.
|
||||
- PASS: The beta24 contract defines diagnostics-only scope, explicit non-scope, acceptance criteria, and focused gates. See `.llm/BETA_24_PACKAGE_MANIFEST_IDENTITY_AND_DEPENDENCY_DISCIPLINE.md:5` through `.llm/BETA_24_PACKAGE_MANIFEST_IDENTITY_AND_DEPENDENCY_DISCIPLINE.md:79`.
|
||||
- PASS: README identifies `1.0.0-beta.24` as current and describes the package manifest/dependency diagnostic hardening scope without claiming package-manager, runtime, language, or stdlib changes. See `README.md:9` and `README.md:27` through `README.md:50`.
|
||||
- PASS: `docs/language/PACKAGES.md` documents duplicate package manifest keys, invalid dependency keys, and duplicate dependency keys as beta package/workspace diagnostics. See `docs/language/PACKAGES.md:59` through `docs/language/PACKAGES.md:73` and `docs/language/PACKAGES.md:111` through `docs/language/PACKAGES.md:130`.
|
||||
- PASS: `docs/language/DIAGNOSTICS.md` includes the new package/workspace diagnostic codes in the project/workspace catalog. See `docs/language/DIAGNOSTICS.md:240` through `docs/language/DIAGNOSTICS.md:279`.
|
||||
- PASS: Language and compiler release notes describe beta24 as package manifest identity/dependency diagnostic hardening only. See `docs/language/RELEASE_NOTES.md:44` through `docs/language/RELEASE_NOTES.md:68` and `docs/compiler/RELEASE_NOTES.md:15` through `docs/compiler/RELEASE_NOTES.md:42`.
|
||||
- PASS: Language/compiler roadmaps and post-beta roadmap mark beta24 as the current local package/workspace diagnostics slice with registry, lockfile, semver, publishing, optional/dev/target dependencies, ABI/layout, language, runtime, and stdlib work deferred. See `docs/language/ROADMAP.md:11` through `docs/language/ROADMAP.md:33`, `docs/compiler/ROADMAP.md:24` through `docs/compiler/ROADMAP.md:38`, `docs/compiler/ROADMAP.md:722` through `docs/compiler/ROADMAP.md:728`, and `docs/POST_BETA_ROADMAP.md:185` through `docs/POST_BETA_ROADMAP.md:191`.
|
||||
|
||||
## Verification Commands
|
||||
|
||||
Ran:
|
||||
|
||||
```bash
|
||||
git diff --check
|
||||
cargo fmt --check
|
||||
cargo test --test package_workspace_discipline_beta24
|
||||
cargo test --test project_mode workspace_package_boundaries_are_diagnostics
|
||||
cargo test --test project_mode
|
||||
rg -n "beta\\.23|beta23|beta\\.24|beta24|1\\.0\\.0-beta\\.2[0-9]|current|Release state|Last updated" README.md docs/language/PACKAGES.md docs/language/DIAGNOSTICS.md docs/language/RELEASE_NOTES.md docs/language/ROADMAP.md docs/compiler/RELEASE_NOTES.md docs/compiler/ROADMAP.md docs/POST_BETA_ROADMAP.md .llm/BETA_24_PACKAGE_MANIFEST_IDENTITY_AND_DEPENDENCY_DISCIPLINE.md compiler/Cargo.toml compiler/Cargo.lock scripts/release-gate.sh
|
||||
```
|
||||
|
||||
Result: all executed checks passed. The focused beta24 test reported 4 passed tests. The full `project_mode` suite reported 36 passed tests.
|
||||
|
||||
## Final Gate Disposition
|
||||
|
||||
After this review, the controller ran the full release gate:
|
||||
|
||||
```bash
|
||||
./scripts/release-gate.sh
|
||||
```
|
||||
|
||||
Result: PASS. The gate completed docs/catalog checks, focused beta13-beta24
|
||||
tests, full `cargo test`, ignored promotion smoke, ignored binary smoke, and
|
||||
ignored LLVM smoke.
|
||||
|
||||
## Residual Risks
|
||||
|
||||
- No release-blocking residual risks remain. The release gate regenerated and
|
||||
checked the standard-library API catalog; publication PDFs were not
|
||||
regenerated because this slice does not touch paper sources, and the gate
|
||||
verified that the required PDF artifacts are present.
|
||||
37
.llm/reviews/BETA_7_RELEASE_REVIEW.md
Normal file
37
.llm/reviews/BETA_7_RELEASE_REVIEW.md
Normal file
@ -0,0 +1,37 @@
|
||||
# 1.0.0-beta.7 Release Review
|
||||
|
||||
Date: 2026-05-22
|
||||
|
||||
Scope: serialization and data-interchange foundation.
|
||||
|
||||
## Verdict
|
||||
|
||||
`1.0.0-beta.7` is a coherent beta follow-up slice. The release adds a narrow
|
||||
runtime-backed JSON string quoting primitive, a source-authored `std/json.slo`
|
||||
facade, explicit std/local example projects, and a `json-quote-loop` benchmark
|
||||
scaffold without claiming full JSON parsing, recursive JSON values, maps,
|
||||
schema validation, or streaming encoders.
|
||||
|
||||
## Review Notes
|
||||
|
||||
- The new compiler-known call is intentionally small:
|
||||
`std.json.quote_string(value string) -> string` lowers to
|
||||
`__glagol_json_quote_string` and is listed in unsupported-call diagnostics.
|
||||
- The source facade composes existing string and numeric helpers and keeps
|
||||
object/array helpers limited to small fixed arities over already-encoded JSON
|
||||
fragments.
|
||||
- The local and std import fixtures preserve the current explicit-import
|
||||
standard-library discipline.
|
||||
- The benchmark scaffold is suitable as a local regression/comparison harness,
|
||||
but the whitepapers still treat the exp-123 nine-row table as the current
|
||||
published numeric baseline until fresh full-suite timing is rerun.
|
||||
- The hosted runtime escaping path covers quotes, backslashes, standard JSON
|
||||
control escapes, and `\u00XX` for remaining control bytes.
|
||||
|
||||
## Remaining Deferred Work
|
||||
|
||||
- JSON parsing and recursive JSON value modeling.
|
||||
- Maps/sets or generic collection-backed object construction.
|
||||
- Streaming encoders and schema-oriented validation.
|
||||
- Unicode normalization or code point policy beyond byte-preserving string
|
||||
literal emission.
|
||||
52
.llm/reviews/BETA_8_RELEASE_REVIEW.md
Normal file
52
.llm/reviews/BETA_8_RELEASE_REVIEW.md
Normal file
@ -0,0 +1,52 @@
|
||||
# 1.0.0-beta.8 Release Review
|
||||
|
||||
Date: 2026-05-22
|
||||
|
||||
Scope: concrete type alias foundation.
|
||||
|
||||
## Verdict
|
||||
|
||||
The Slovo documentation and source-fixture side is coherent for a beta.8
|
||||
concrete-alias slice. The contract is intentionally transparent: top-level
|
||||
`(type Alias TargetType)` declarations name existing supported concrete target
|
||||
types, normalize before backend/ABI decisions, and do not create runtime types,
|
||||
hosted symbols, generic alias machinery, maps/sets, or cross-module alias
|
||||
visibility.
|
||||
|
||||
## Review Notes
|
||||
|
||||
- The language spec defines syntax, resolution, formatter shape, diagnostics,
|
||||
and explicit deferrals for concrete aliases.
|
||||
- The public roadmap and stable-readiness notes place beta.8 between JSON data
|
||||
interchange and generics/collection unification.
|
||||
- `std/json.slo` uses `JsonText` and `JsonField` as local aliases only. The
|
||||
helper export list remains unchanged.
|
||||
- The explicit std-import and local JSON fixtures exercise local alias use
|
||||
without importing, exporting, or re-exporting aliases.
|
||||
- Glagol parser, checker, formatter, project import resolution, lowering
|
||||
inspectors, diagnostics fixtures, and promotion inventory now carry the
|
||||
matching implementation side.
|
||||
- Reviewer findings were integrated: JSON fixtures use canonical alias spacing,
|
||||
unsupported alias targets are rejected, alias/value name conflicts are
|
||||
diagnosed, and alias export/import attempts produce explicit visibility
|
||||
diagnostics.
|
||||
|
||||
## Verification
|
||||
|
||||
- `cargo fmt --check`
|
||||
- `cargo test --test diagnostics_contract current_negative_cases_match_machine_diagnostic_snapshots`
|
||||
- `cargo test --test concrete_type_aliases_beta`
|
||||
- `cargo test --test project_mode type_aliases_are_local_across_project_visibility`
|
||||
- `cargo test --test lowering_inspector`
|
||||
- `cargo test --test promotion_gate promotion_gate_artifacts_are_aligned`
|
||||
- `cargo test --test standard_json_source_facade_alpha`
|
||||
- `cargo test`
|
||||
- `git diff --check`
|
||||
|
||||
## Remaining Deferred Work
|
||||
|
||||
- Generated documentation may still display source alias spelling. Runtime
|
||||
behavior, checked lowering, backend layout, and cross-module typing use the
|
||||
resolved concrete target.
|
||||
- Any future generic alias, cross-module alias import/export, or re-export
|
||||
design.
|
||||
50
.llm/reviews/BETA_9_RELEASE_REVIEW.md
Normal file
50
.llm/reviews/BETA_9_RELEASE_REVIEW.md
Normal file
@ -0,0 +1,50 @@
|
||||
# 1.0.0-beta.9 Release Review
|
||||
|
||||
Status: ready for publication after the controller release gate.
|
||||
|
||||
## Verdict
|
||||
|
||||
No blocking issues found after integrating the Slovo stdlib/docs worker and
|
||||
the Glagol compiler/test worker.
|
||||
|
||||
## Scope Checked
|
||||
|
||||
- `lib/std/vec_i32.slo`, `lib/std/vec_i64.slo`, `lib/std/vec_f64.slo`,
|
||||
`lib/std/vec_bool.slo`, `lib/std/vec_string.slo`, `lib/std/option.slo`, and
|
||||
`lib/std/result.slo` now use module-local concrete aliases without exporting
|
||||
alias names.
|
||||
- Compiler diagnostics reserve generic functions, parameterized aliases,
|
||||
uppercase single-letter generic type parameters in unsupported positions,
|
||||
map/set type forms, and future generic stdlib calls.
|
||||
- Formatter diagnostics mirror the compiler rejection path for reserved
|
||||
generic/map/set syntax.
|
||||
- Docs and whitepapers describe beta9 as alias-backed collection cleanup and
|
||||
diagnostic reservation only. They do not claim executable generics, maps,
|
||||
sets, traits, inference, monomorphization, iterators, stable ABI/layout, or a
|
||||
stable stdlib API freeze.
|
||||
|
||||
## Verification
|
||||
|
||||
- `cargo fmt --check`
|
||||
- `cargo test --test diagnostics_contract current_negative_cases_match_machine_diagnostic_snapshots`
|
||||
- `cargo test --test formatter formatter_re`
|
||||
- `cargo test --test project_mode`
|
||||
- `cargo test --test concrete_type_aliases_beta`
|
||||
- `cargo test --test promotion_gate`
|
||||
- `cargo test standard_`
|
||||
- `./scripts/render-stdlib-api-doc.sh`
|
||||
- `./scripts/render-doc-pdfs.sh`
|
||||
- `git diff --check`
|
||||
- `./scripts/release-gate.sh`
|
||||
|
||||
Final gate result: passed. The release gate completed docs, generated stdlib
|
||||
API catalog, formatter, default tests, ignored promotion checks, binary smoke,
|
||||
and LLVM smoke checks.
|
||||
|
||||
## Residual Risk
|
||||
|
||||
Beta9 intentionally reserves generics without implementing them. Future
|
||||
generic work still needs typed-core representation, monomorphization policy,
|
||||
project/import semantics, formatter stability, stdlib migration rules, and
|
||||
explicit compatibility gates before any executable generic collection surface
|
||||
is promoted.
|
||||
527
README.md
527
README.md
@ -6,7 +6,7 @@ This repository is the canonical public monorepo for the language design,
|
||||
standard library source, compiler, runtime, examples, benchmarks, and technical
|
||||
documents.
|
||||
|
||||
Current release: `1.0.0-beta`.
|
||||
Current release: `1.0.0-beta.24`.
|
||||
|
||||
## Repository Layout
|
||||
|
||||
@ -24,20 +24,203 @@ scripts/ local release and document tooling
|
||||
|
||||
## Beta Scope
|
||||
|
||||
`1.0.0-beta` supports practical local command-line programs and libraries with:
|
||||
`1.0.0-beta.24` keeps the `1.0.0-beta` language baseline, includes the
|
||||
`1.0.0-beta.1` tooling/install hardening slice, the `1.0.0-beta.2`
|
||||
runtime/resource foundation bundle, the `1.0.0-beta.3` standard-library
|
||||
stabilization bundle, the `1.0.0-beta.4` language-usability diagnostics
|
||||
bundle, the `1.0.0-beta.5` local package/workspace discipline bundle, and the
|
||||
`1.0.0-beta.6` loopback networking foundation, plus the `1.0.0-beta.7`
|
||||
serialization/data-interchange foundation and the `1.0.0-beta.8` concrete type
|
||||
alias foundation, the `1.0.0-beta.9` collection alias unification and
|
||||
generic reservation slice, the `1.0.0-beta.10` developer-experience API
|
||||
discovery slice, and the `1.0.0-beta.11` local package API documentation
|
||||
slice, plus the `1.0.0-beta.12` concrete vector query and prefix parity
|
||||
slice, the `1.0.0-beta.13` diagnostic catalog and schema policy slice, the
|
||||
`1.0.0-beta.14` benchmark suite catalog and metadata gate, and the
|
||||
`1.0.0-beta.15` reserved generic collection boundary hardening and collection
|
||||
ledger, the `1.0.0-beta.16` string scanning and token boundary foundation,
|
||||
the `1.0.0-beta.17` JSON primitive scalar parsing foundation, the
|
||||
`1.0.0-beta.18` JSON string token parsing foundation, the
|
||||
`1.0.0-beta.19` test discovery and user-project conformance foundation, the
|
||||
`1.0.0-beta.20` string search and ASCII trim foundation, and the
|
||||
`1.0.0-beta.21` JSON document scalar parsing foundation, plus the
|
||||
`1.0.0-beta.22` run manifest and execution report hardening slice, and the
|
||||
`1.0.0-beta.23` standard-library stability tier ledger and catalog alignment
|
||||
slice, and the `1.0.0-beta.24` package manifest identity and local dependency
|
||||
diagnostic hardening slice.
|
||||
|
||||
The language baseline supports practical local command-line, file, and
|
||||
loopback-network programs with:
|
||||
|
||||
- modules, explicit imports, packages, and local workspaces
|
||||
- `new`, `check`, `fmt`, `test`, `doc`, and `build`
|
||||
- `new`, `check`, `fmt`, `test`, `doc`, `symbols`, `build`, `run`, and
|
||||
`clean`
|
||||
- `i32`, `i64`, `u32`, `u64`, `f64`, `bool`, `string`, and internal `unit`
|
||||
- structs, enums, fixed arrays, concrete vectors, option/result families, and
|
||||
current `match`
|
||||
- module-local transparent concrete type aliases
|
||||
- explicit `std/*.slo` imports from `lib/std`, installed `share/slovo/std`, or
|
||||
`SLOVO_STD_PATH`
|
||||
- beta-scoped loopback TCP handles through `std.net`
|
||||
- JSON string quoting, compact JSON text construction, primitive scalar token
|
||||
parsing, ASCII JSON string-token parsing, and scalar JSON document parsing
|
||||
through `std.json`
|
||||
- byte-oriented string search and ASCII edge trimming through `std.string`
|
||||
- hosted native builds through LLVM IR, Clang, and `runtime/runtime.c`
|
||||
|
||||
Still deferred before stable: generics, maps/sets, broad package registry
|
||||
semantics, networking/async, LSP/watch/debug-adapter guarantees, stable ABI and
|
||||
layout, and a stable standard-library compatibility freeze.
|
||||
The generated standard-library API catalog is a beta discovery aid: it lists
|
||||
exported helper signatures from `lib/std/*.slo`, normalizes module-local
|
||||
concrete aliases such as `VecI32` and `ResultU64` to their concrete public
|
||||
types, and omits non-exported helpers and `(type ...)` aliases.
|
||||
The companion
|
||||
[`docs/language/STDLIB_TIERS.md`](docs/language/STDLIB_TIERS.md) ledger
|
||||
defines the public tier labels `beta-supported`, `experimental`, and
|
||||
`internal`, marks JSON, loopback networking, random/time, and filesystem
|
||||
resource-handle helpers as experimental domains, and keeps concrete vector
|
||||
modules as beta-supported concrete lanes rather than a generic collections
|
||||
freeze.
|
||||
`glagol symbols <file.slo|project|workspace>` emits deterministic
|
||||
editor-facing S-expression metadata for modules, imports, exports, aliases,
|
||||
structs, enums, functions, tests, source spans, and workspace package names.
|
||||
`glagol doc <file|project|workspace> -o <dir>` now includes deterministic public
|
||||
API sections for local package and module documentation: exact exported
|
||||
function signatures, exported struct fields, exported enum variants and payload
|
||||
types, non-export filtering, and module-local alias normalization.
|
||||
The `1.0.0-beta.22` tooling slice adds an additive run-report block to
|
||||
`glagol run --manifest` artifact manifests so local evidence can record the
|
||||
program exit status, captured stdout, captured stderr, and forwarded program
|
||||
arguments. This is beta CLI evidence hardening only: it does not add language
|
||||
or stdlib features and does not freeze the artifact-manifest schema.
|
||||
The `1.0.0-beta.12` vector parity slice adds source-authored helper coverage
|
||||
only: `std.vec_i64` gains `count_of`, `starts_with`, `without_prefix`,
|
||||
`ends_with`, and `without_suffix`, while `std.vec_f64` gains `count_of`.
|
||||
The `1.0.0-beta.13` diagnostics slice documents the beta
|
||||
`slovo.diagnostic` version `1` policy in
|
||||
[`docs/language/DIAGNOSTICS.md`](docs/language/DIAGNOSTICS.md): the
|
||||
S-expression/JSON relationship, required and optional fields, JSON-line
|
||||
discipline, source-less diagnostics, manifest diagnostic metadata,
|
||||
compatibility and migration classes, and the current golden diagnostic code
|
||||
catalog.
|
||||
The `1.0.0-beta.14` benchmark metadata slice documents the existing benchmark
|
||||
suite catalog in [`benchmarks/README.md`](benchmarks/README.md). It explains
|
||||
`python3 benchmarks/runner.py --suite-list` for the human-readable suite
|
||||
inventory and `python3 benchmarks/runner.py --suite-list --json` for beta
|
||||
tooling metadata, with required scaffold-file verification for each current
|
||||
suite. Benchmark timings remain local-machine evidence only; the JSON field
|
||||
set is not a stable public schema.
|
||||
The `1.0.0-beta.15` collection ledger and reserved diagnostic hardening slice
|
||||
adds
|
||||
[`docs/language/COLLECTIONS.md`](docs/language/COLLECTIONS.md) as the
|
||||
docs/design ledger for current concrete collection and value-family
|
||||
boundaries. It links to the generated
|
||||
[`docs/language/STDLIB_API.md`](docs/language/STDLIB_API.md) catalog for exact
|
||||
public helper signatures, records design pressure from duplicated concrete
|
||||
vector/option/result families, defines prerequisites before executable
|
||||
generics, generic aliases, maps, sets, iterators, mutable vectors, or
|
||||
slice/view APIs can be promoted, and treats current unsupported diagnostics as
|
||||
boundaries. It rewords affected reserved-boundary diagnostics from
|
||||
beta.9-specific text to current-beta wording while preserving diagnostic
|
||||
codes, schema, spans, expected/found values, hints, and output shape. It
|
||||
changes no source language, runtime, stdlib/API surface, benchmark metadata
|
||||
schema, ABI/layout behavior, or performance claim.
|
||||
The `1.0.0-beta.16` string scanning and token boundary foundation adds
|
||||
source facades and explicit examples for `std.string.byte_at_result`,
|
||||
`std.string.slice_result`, `std.string.starts_with`, and
|
||||
`std.string.ends_with`. These helpers are byte-oriented over the current
|
||||
NUL-terminated runtime string representation; invalid indexes and ranges return
|
||||
`err 1`, and substring allocation failure follows the existing string
|
||||
allocation trap policy. This release does not promise Unicode scalar,
|
||||
grapheme, display-width, or locale semantics; does not add full JSON parsing,
|
||||
object/array parsing, tokenizers, a language slice/view feature, or a stable
|
||||
stdlib/API freeze.
|
||||
|
||||
The `1.0.0-beta.17` JSON primitive scalar parsing foundation adds
|
||||
result-returning `std.json.parse_*_value_result` helpers for booleans,
|
||||
concrete numeric primitives, and exact `null` only. Numeric and boolean parse
|
||||
helpers consume one isolated JSON primitive token: no leading/trailing
|
||||
whitespace, no leading `+`, no leading-zero integer form except `0`, and no
|
||||
non-finite f64 values. This is not full JSON parsing: object/array parsing,
|
||||
tokenizers, recursive `JsonValue`, document parsing beyond the beta21 scalar
|
||||
document helpers, schema validation, streaming, Unicode escape handling,
|
||||
stable ABI/layout, and a stable stdlib/API freeze remain deferred.
|
||||
|
||||
The `1.0.0-beta.18` JSON string token parsing foundation adds
|
||||
`std.json.parse_string_value_result` as a thin source facade over the matching
|
||||
promoted runtime name. It consumes one already-isolated ASCII JSON string token
|
||||
with exact quotes and no leading/trailing whitespace, decodes the simple JSON
|
||||
escapes `\"`, `\\`, `\/`, `\b`, `\f`, `\n`, `\r`, and `\t`, and returns
|
||||
`err 1` for ordinary parse failure. Complete JSON document parsing beyond the
|
||||
beta21 scalar document helpers, object/array parsing, tokenizer APIs, Unicode
|
||||
escape decoding/normalization, embedded NUL policy, stable ABI/layout, and a
|
||||
stable stdlib/API freeze remain deferred.
|
||||
|
||||
The `1.0.0-beta.19` test discovery and user-project conformance foundation
|
||||
adds `glagol test --list <file|project|workspace>` plus legacy
|
||||
`glagol --run-tests --list <file>` support for listing checked and discovered
|
||||
tests without executing test bodies. The list mode preserves existing file,
|
||||
project, and workspace test ordering, honors `--filter <substring>`, and
|
||||
remains beta tooling rather than a stable output schema.
|
||||
|
||||
The `1.0.0-beta.20` string search and ASCII trim foundation adds
|
||||
`std.string.contains`, `std.string.index_of_option`,
|
||||
`std.string.last_index_of_option`, `std.string.trim_ascii_start`,
|
||||
`std.string.trim_ascii_end`, and `std.string.trim_ascii` as ordinary source
|
||||
helpers over the existing byte string primitives. Search is byte-oriented,
|
||||
empty needles match at the first index and at `(len value)` for last search,
|
||||
and ASCII trimming removes only bytes `9`, `10`, `11`, `12`, `13`, and `32`.
|
||||
|
||||
The `1.0.0-beta.21` JSON document scalar parsing foundation adds
|
||||
source-authored `std.json.parse_*_document_result` helpers for string, bool,
|
||||
`i32`, `u32`, `i64`, `u64`, `f64`, and exact `null` scalar JSON documents.
|
||||
Each helper trims ASCII whitespace around the whole document with
|
||||
`std.string.trim_ascii`, then delegates to the already released exact JSON
|
||||
value-token parser for that scalar family. This scope does not add new
|
||||
compiler-known runtime names, object/array parsing, recursive `JsonValue`,
|
||||
tokenizer objects, maps/sets, streaming, Unicode escape decoding beyond the
|
||||
existing string-token behavior, embedded NUL policy, stable ABI/layout, or a
|
||||
stable stdlib/API freeze.
|
||||
|
||||
The `1.0.0-beta.22` run manifest and execution report hardening slice extends
|
||||
`glagol run --manifest` artifact manifests with additive run-report evidence:
|
||||
the executed program's exit status, captured stdout, captured stderr, and
|
||||
forwarded user-program arguments. This release does not add source-language
|
||||
syntax, standard-library helpers, compiler-known runtime names, runtime
|
||||
capabilities, package behavior, stable artifact-manifest schema guarantees,
|
||||
stable ABI/layout, or a stable stdlib/API freeze.
|
||||
|
||||
The `1.0.0-beta.23` standard-library stability tier ledger and catalog
|
||||
alignment slice adds the public
|
||||
[`STDLIB_TIERS.md`](docs/language/STDLIB_TIERS.md) maturity ledger beside the
|
||||
generated [`STDLIB_API.md`](docs/language/STDLIB_API.md) signature catalog.
|
||||
It is documentation/catalog tooling clarity only: no source-language syntax,
|
||||
stdlib helpers, compiler-known runtime names, runtime behavior, stable manifest
|
||||
schema, stable Markdown schema, stable ABI/layout, or stable stdlib/API freeze
|
||||
changes. The generated catalog and release gate now expose and check tier
|
||||
metadata.
|
||||
|
||||
The `1.0.0-beta.24` package manifest identity and local dependency discipline
|
||||
slice tightens local manifest diagnostics only: duplicate package manifest
|
||||
keys, invalid dependency keys, and duplicate dependency keys are explicit beta
|
||||
package/workspace diagnostics. It does not add remote registries, lockfiles,
|
||||
semantic-version solving, package publishing, optional/dev/target
|
||||
dependencies, stable package ABI/layout, source-language changes, runtime
|
||||
changes, or standard-library changes.
|
||||
|
||||
Still deferred before stable: executable generics, generic aliases, maps/sets,
|
||||
broad package registry semantics, stable artifact-manifest schema, stable
|
||||
Markdown schema, stable stdlib/API compatibility freeze, DNS/TLS/async
|
||||
networking, LSP/watch guarantees, SARIF and daemon protocols, stable `1.0.0`
|
||||
diagnostics freeze,
|
||||
re-exports/globs/hierarchical modules, mutable vectors, slice/view APIs,
|
||||
iterators, additional compiler-known runtime names, stable ABI and layout,
|
||||
performance claims, stable benchmark JSON metadata schema, and runtime changes
|
||||
for generic collections.
|
||||
|
||||
The beta19 tooling scope is deliberately tooling-only. It does not add parallel
|
||||
test execution, retries, tags/groups, coverage reports, event streams, stable
|
||||
manifest or Markdown schema guarantees, LSP/watch behavior, SARIF/daemon
|
||||
protocols, JSON expansion, runtime helper names, source-language syntax,
|
||||
remote package registries, semver solving, or performance claims.
|
||||
|
||||
## Build And Test
|
||||
|
||||
@ -72,11 +255,343 @@ Build a native executable when Clang is available:
|
||||
SLOVO_STD_PATH="$PWD/lib/std" ./compiler/target/debug/glagol build hello -o hello/bin
|
||||
```
|
||||
|
||||
## 1.0.0-beta.1 Tooling Additions
|
||||
|
||||
The `1.0.0-beta.1` release improves the common local development and install
|
||||
loop without adding new source-language syntax.
|
||||
|
||||
Build and execute in one step:
|
||||
|
||||
```bash
|
||||
SLOVO_STD_PATH="$PWD/lib/std" ./compiler/target/debug/glagol run hello
|
||||
SLOVO_STD_PATH="$PWD/lib/std" ./compiler/target/debug/glagol clean hello
|
||||
```
|
||||
|
||||
Create alternate project shapes:
|
||||
|
||||
```bash
|
||||
./compiler/target/debug/glagol new numbers --template library
|
||||
./compiler/target/debug/glagol new workspace-demo --template workspace
|
||||
```
|
||||
|
||||
Install the current checkout:
|
||||
|
||||
```bash
|
||||
PREFIX="$HOME/.local" ./scripts/install.sh
|
||||
```
|
||||
|
||||
The installed layout is:
|
||||
|
||||
```text
|
||||
<prefix>/bin/glagol
|
||||
<prefix>/share/slovo/std/*.slo
|
||||
<prefix>/share/slovo/runtime/runtime.c
|
||||
```
|
||||
|
||||
Installed `glagol` discovers `share/slovo/std` and
|
||||
`share/slovo/runtime/runtime.c` relative to its executable. `SLOVO_STD_PATH`
|
||||
can still override standard-library search, `SLOVO_RUNTIME_C` or
|
||||
`GLAGOL_RUNTIME_C` can override the runtime C input, and `GLAGOL_CLANG` can
|
||||
select the Clang-compatible compiler.
|
||||
|
||||
## 1.0.0-beta.2 Runtime Resource Foundation
|
||||
|
||||
The `1.0.0-beta.2` release adds beta-scoped runtime/resource foundation work:
|
||||
|
||||
- `std.fs.open_text_read_result`
|
||||
- `std.fs.read_open_text_result`
|
||||
- `std.fs.close_result`
|
||||
- `std.fs.exists`
|
||||
- `std.fs.is_file`
|
||||
- `std.fs.is_dir`
|
||||
- `std.fs.remove_file_result`
|
||||
- `std.fs.create_dir_result`
|
||||
- matching explicit `lib/std/fs.slo` facades
|
||||
|
||||
These APIs use beta-scoped opaque `i32` file handles. They do not claim stable
|
||||
file descriptors, writable streams, binary IO, directory handles, sockets,
|
||||
async IO, platform error codes, or stable handle ABI/layout. Directory creation
|
||||
is intentionally narrow and does not imply directory enumeration or recursive
|
||||
filesystem APIs.
|
||||
|
||||
## 1.0.0-beta.3 Standard Library Stabilization
|
||||
|
||||
The `1.0.0-beta.3` release starts the standard-library stabilization slice. It
|
||||
adds a generated standard-library API catalog and
|
||||
`examples/projects/stdlib-composition`, a checked/tested/run-capable program
|
||||
that composes `std.fs`, `std.string`, `std.math`, and `std.io`.
|
||||
|
||||
## 1.0.0-beta.4 Language Usability Diagnostics
|
||||
|
||||
The `1.0.0-beta.4` release improves diagnostics without changing the source
|
||||
language surface. Project/workspace build and run entry failures now use
|
||||
entry-specific diagnostic codes, and non-exhaustive `match` diagnostics have
|
||||
clearer wording with deterministic found-arm output.
|
||||
|
||||
## 1.0.0-beta.5 Package And Workspace Discipline
|
||||
|
||||
The `1.0.0-beta.5` release tightens local package/workspace behavior. Local
|
||||
workspaces may declare `[workspace] default_package = "name"` to select the
|
||||
build/run entry package when multiple packages have entry modules. Duplicate
|
||||
normalized workspace members and missing default-package references are now
|
||||
dedicated diagnostics. `glagol doc <workspace> -o <dir>` includes a workspace
|
||||
package/dependency summary, new workspace templates declare
|
||||
`default_package = "app"`, and `docs/language/PACKAGES.md` documents the beta
|
||||
local-package rules. Remote registries, lockfiles, semantic-version solving,
|
||||
package publishing, and stable package ABI/layout remain deferred.
|
||||
|
||||
## 1.0.0-beta.24 Package Manifest Identity And Dependency Discipline
|
||||
|
||||
The `1.0.0-beta.24` release keeps the local package model closed and local. It
|
||||
hardens diagnostics for manifest identity/dependency mistakes only: duplicate
|
||||
package manifest keys, invalid dependency keys, and duplicate dependency keys
|
||||
are reported explicitly.
|
||||
|
||||
This scope does not add a remote registry, lockfile, semantic-version solver,
|
||||
package publishing flow, optional/dev/target dependencies, stable package
|
||||
ABI/layout, source-language behavior, runtime behavior, or standard-library
|
||||
behavior.
|
||||
|
||||
## 1.0.0-beta.6 Networking Foundation
|
||||
|
||||
The `1.0.0-beta.6` release adds a narrow blocking loopback TCP foundation:
|
||||
|
||||
- compiler-known `std.net.tcp_*_result` calls for connect, listen,
|
||||
bound-port lookup, accept, read-all, write-text, and close
|
||||
- `lib/std/net.slo` source facades and explicit std/local example projects
|
||||
- opaque beta-scoped `i32` socket handles with concrete `result` values
|
||||
|
||||
This is not a general networking stack. DNS, TLS, UDP, non-loopback binding,
|
||||
async IO, HTTP frameworks, rich host-error ADTs, stable socket ABI/layout, and
|
||||
automatic resource ownership remain deferred.
|
||||
|
||||
## 1.0.0-beta.7 Serialization And Data Interchange
|
||||
|
||||
The `1.0.0-beta.7` release adds a narrow JSON text-construction foundation:
|
||||
|
||||
- compiler-known `std.json.quote_string` for deterministic compact JSON string
|
||||
quoting
|
||||
- `lib/std/json.slo` source helpers for scalar values, fields, small arrays,
|
||||
and small objects
|
||||
- explicit std/local JSON example projects and a `json-quote-loop` benchmark
|
||||
scaffold
|
||||
|
||||
This is not a complete JSON library. Full parsing beyond primitive scalar
|
||||
tokens and the ASCII JSON string-token helper, object/array parsing,
|
||||
recursive JSON values, maps/sets, streaming encoders or decoders, schema
|
||||
validation, Unicode normalization, and a stable data-interchange API freeze
|
||||
remain deferred.
|
||||
|
||||
## 1.0.0-beta.8 Concrete Type Alias Foundation
|
||||
|
||||
The `1.0.0-beta.8` release adds transparent concrete type aliases:
|
||||
|
||||
```slo
|
||||
(type JsonText string)
|
||||
```
|
||||
|
||||
Aliases are module-local names for already supported concrete target types.
|
||||
They may appear in local signatures and annotations, but they do not create new
|
||||
runtime representations or stable ABI names. Project imports of functions that
|
||||
use aliases see the resolved concrete target type. Alias exports, imports,
|
||||
re-exports, generic aliases, parameterized aliases, maps/sets, and new
|
||||
compiler-known runtime names remain out of scope.
|
||||
|
||||
## 1.0.0-beta.9 Collection Alias Unification And Generic Reservation
|
||||
|
||||
The `1.0.0-beta.9` release applies beta.8 concrete aliases inside the current
|
||||
source-authored collection/value-family facades. The public helper names and
|
||||
runtime behavior remain concrete: current vectors, options, and results are
|
||||
still concrete families, and the local aliases are erased before lowering.
|
||||
|
||||
This release also reserves the generic/map/set direction through diagnostics
|
||||
and documentation only. It does not implement executable generics, maps, sets,
|
||||
traits, inference, monomorphization, iterators, stable ABI/layout promises, or
|
||||
a stable standard-library API freeze.
|
||||
|
||||
## 1.0.0-beta.10 Developer Experience API Discovery
|
||||
|
||||
The `1.0.0-beta.10` release upgrades the generated standard-library API
|
||||
catalog from exported helper names to exact exported helper signatures. The
|
||||
renderer verifies that every exported helper has a matching `(fn ...)` form,
|
||||
normalizes module-local concrete aliases from the public signatures, omits
|
||||
non-exported helpers and `(type ...)` aliases, and keeps the catalog generated
|
||||
from `lib/std/*.slo`.
|
||||
|
||||
It also adds `glagol symbols <file.slo|project|workspace>` as an
|
||||
editor-facing metadata command. The output is deterministic S-expression text
|
||||
using `slovo.symbols` schema version `1.0.0-beta.10`; it includes module paths,
|
||||
source spans/ranges, imports, exports, aliases, structs, enums, functions,
|
||||
tests, and workspace package labels.
|
||||
|
||||
This release is tooling, documentation, and API-discovery work. It does not
|
||||
add executable generics, maps, sets, new runtime helpers, new compiler-known
|
||||
runtime names, ABI/layout guarantees, an LSP server, watch mode, or a stable
|
||||
standard-library API freeze.
|
||||
|
||||
## 1.0.0-beta.11 Local Package API Documentation
|
||||
|
||||
The `1.0.0-beta.11` release extends the beta.10 API discovery lane to local
|
||||
package and module documentation. `glagol doc <file|project|workspace> -o <dir>`
|
||||
includes deterministic exported/public API sections for local modules and
|
||||
workspace packages.
|
||||
|
||||
Those sections list exact exported function signatures, exported struct fields,
|
||||
and exported enum variants with payload types. They omit non-exported
|
||||
functions, structs, enums, tests, and aliases from the public API surface, and
|
||||
they normalize module-local concrete aliases before rendering public types.
|
||||
|
||||
This remains beta API discovery. It does not freeze the Markdown schema, create
|
||||
a stable stdlib/API compatibility freeze, add LSP/watch behavior, define
|
||||
SARIF or daemon protocols, set diagnostics schema policy, implement executable
|
||||
generics, maps, or sets, add re-exports, globs, or hierarchical modules, or
|
||||
define package registry semantics.
|
||||
|
||||
## 1.0.0-beta.12 Concrete Vector Query And Prefix Parity
|
||||
|
||||
The `1.0.0-beta.12` release is a source-authored stdlib/helper parity update
|
||||
for concrete vectors. It adds `count_of`, `starts_with`, `without_prefix`,
|
||||
`ends_with`, and `without_suffix` to `std.vec_i64`, and adds `count_of` to
|
||||
`std.vec_f64`, matching the already staged concrete helper style.
|
||||
|
||||
This release does not change the source language, runtime, compiler-known
|
||||
`std.vec.*` names, ABI/layout contract, or performance contract. Generics,
|
||||
maps/sets, iterators, mutable vectors, slice/view APIs, new runtime helper
|
||||
names, stable stdlib API freeze, and broader collection abstractions remain
|
||||
deferred.
|
||||
|
||||
## 1.0.0-beta.13 Diagnostic Catalog And Schema Policy
|
||||
|
||||
The `1.0.0-beta.13` release is a docs/tooling policy update for the existing
|
||||
diagnostic surface. It adds
|
||||
[`docs/language/DIAGNOSTICS.md`](docs/language/DIAGNOSTICS.md) as the beta
|
||||
policy for `slovo.diagnostic` version `1`.
|
||||
|
||||
The policy documents the S-expression and JSON encodings, required and optional
|
||||
machine fields, severity/source/range/related-span semantics, JSON-line
|
||||
discipline, source-less diagnostics, artifact-manifest diagnostic metadata,
|
||||
compatibility and migration classes, and the current code catalog covered by
|
||||
the golden diagnostics contract.
|
||||
|
||||
This release does not change the source language, runtime, stdlib API,
|
||||
diagnostic output shape, compiler CLI, LSP/watch behavior, SARIF/daemon
|
||||
protocols, stable Markdown schema, or stable `1.0.0` diagnostics freeze.
|
||||
|
||||
## 1.0.0-beta.16 String Scanning And Token Boundary Foundation
|
||||
|
||||
The `1.0.0-beta.16` release adds the first explicit byte-oriented string
|
||||
scanning and token-boundary helpers to the `std.string` source facade:
|
||||
`byte_at_result`, `slice_result`, `starts_with`, and `ends_with`.
|
||||
|
||||
The helpers operate over the current NUL-terminated runtime string bytes before
|
||||
the trailing NUL. `byte_at_result` and `slice_result` return `err 1` for
|
||||
ordinary invalid indexes or ranges. `slice_result` returns a runtime-owned
|
||||
string on success; allocation failure may trap with the existing string
|
||||
allocation policy.
|
||||
|
||||
This release does not add Unicode scalar, grapheme, display-width, or locale
|
||||
semantics; full JSON parsing; object/array parsing; tokenizer APIs; a language
|
||||
slice/view feature; mutable strings; stable ABI/layout; or a stable stdlib/API
|
||||
freeze.
|
||||
|
||||
## 1.0.0-beta.20 String Search And ASCII Trim Foundation
|
||||
|
||||
The `1.0.0-beta.20` scope extends the source-authored `std.string` facade
|
||||
with byte-oriented search and ASCII trim helpers:
|
||||
`contains`, `index_of_option`, `last_index_of_option`, `trim_ascii_start`,
|
||||
`trim_ascii_end`, and `trim_ascii`.
|
||||
|
||||
This scope adds no compiler-known runtime names. It does not claim Unicode
|
||||
scalar, grapheme, case-folding, locale, regex, tokenizer, mutable string,
|
||||
language slice/view, stable ABI/layout, or stable stdlib/API semantics.
|
||||
|
||||
## 1.0.0-beta.21 JSON Document Scalar Parsing Foundation
|
||||
|
||||
The `1.0.0-beta.21` scope extends `std.json` with source-authored
|
||||
whole-document scalar helpers:
|
||||
`parse_string_document_result`, `parse_bool_document_result`,
|
||||
`parse_i32_document_result`, `parse_u32_document_result`,
|
||||
`parse_i64_document_result`, `parse_u64_document_result`,
|
||||
`parse_f64_document_result`, and `parse_null_document_result`.
|
||||
|
||||
The helpers accept leading and trailing ASCII whitespace around a single
|
||||
scalar document by composing with `std.string.trim_ascii`; trailing
|
||||
non-whitespace still fails through the existing exact value-token parsers.
|
||||
|
||||
This scope does not add object/array parsing, recursive JSON values, parser
|
||||
objects, maps/sets, streaming, Unicode escape decoding beyond the existing
|
||||
string-token behavior, embedded NUL policy, new compiler-known runtime names,
|
||||
stable ABI/layout, or stable stdlib/API semantics.
|
||||
|
||||
## 1.0.0-beta.22 Run Manifest And Execution Report Hardening
|
||||
|
||||
The `1.0.0-beta.22` scope hardens `glagol run --manifest` evidence by adding
|
||||
an additive run-report block to run-mode artifact manifests. The block records
|
||||
the executed program's exit status, captured stdout, captured stderr, and
|
||||
forwarded program arguments.
|
||||
|
||||
This is tooling/CLI evidence work only. It does not add language syntax,
|
||||
stdlib helpers, compiler-known runtime names, runtime C capabilities, package
|
||||
or import behavior, stable artifact-manifest schema guarantees, stable
|
||||
ABI/layout, or stable stdlib/API semantics.
|
||||
|
||||
## 1.0.0-beta.15 Reserved Generic Collection Boundary Hardening And Collection Ledger
|
||||
|
||||
The `1.0.0-beta.15` release documents the current concrete collection and
|
||||
value-family boundary. It adds
|
||||
[`docs/language/COLLECTIONS.md`](docs/language/COLLECTIONS.md), which links to
|
||||
the generated
|
||||
[`docs/language/STDLIB_API.md`](docs/language/STDLIB_API.md) catalog instead
|
||||
of duplicating generated helper counts.
|
||||
|
||||
The ledger inventories concrete vector, option, result, and related
|
||||
option/result-returning facade surfaces; records the design pressure from
|
||||
duplicated concrete vector/option/result helpers; and defines prerequisites
|
||||
before executable generics, generic aliases, maps, sets, iterators, mutable
|
||||
vectors, or slice/view APIs can be promoted.
|
||||
|
||||
Current unsupported diagnostics are documented as release boundaries, not as
|
||||
new behavior. This release does not change the source language, runtime,
|
||||
stdlib/API surface, diagnostic output shape/codes/schema, benchmark metadata
|
||||
schema, compiler ABI/layout behavior, or performance claims, and it does not
|
||||
create a stable stdlib/API freeze. It does reword affected reserved-boundary
|
||||
diagnostic messages from beta.9-specific text to current-beta wording while
|
||||
preserving diagnostic codes, schema, spans, expected/found values, hints, and
|
||||
output shape.
|
||||
|
||||
## 1.0.0-beta.14 Benchmark Suite Catalog And Metadata Gate
|
||||
|
||||
The `1.0.0-beta.14` release documents the existing benchmark suite catalog as
|
||||
beta-scoped metadata tooling. It adds
|
||||
[`benchmarks/README.md`](benchmarks/README.md) with the current ten-suite
|
||||
inventory, local evidence policy, suite-list commands, and exclusions.
|
||||
|
||||
The root suite catalog commands are:
|
||||
|
||||
```bash
|
||||
python3 benchmarks/runner.py --suite-list
|
||||
python3 benchmarks/runner.py --suite-list --json
|
||||
```
|
||||
|
||||
The non-JSON listing is for local review. The JSON listing is beta tooling
|
||||
metadata for local gates and adapters, not a stable public schema.
|
||||
|
||||
This release does not add benchmark kernels, publish timing numbers, define
|
||||
performance thresholds, change the source language, runtime, stdlib/API
|
||||
surface, diagnostic output, compiler ABI/layout behavior, or make cross-machine
|
||||
performance claims.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Language Manifest](docs/language/MANIFEST.md)
|
||||
- [Language Specification](docs/language/SPEC-v1.md)
|
||||
- [Diagnostics Policy](docs/language/DIAGNOSTICS.md)
|
||||
- [Local Package And Workspace Guide](docs/language/PACKAGES.md)
|
||||
- [Standard Library API Catalog](docs/language/STDLIB_API.md)
|
||||
- [Collection Ledger](docs/language/COLLECTIONS.md)
|
||||
- [Benchmark Suite Catalog](benchmarks/README.md)
|
||||
- [Compiler Manifest](docs/compiler/GLAGOL_COMPILER_MANIFEST.md)
|
||||
- [Post-Beta Roadmap](docs/POST_BETA_ROADMAP.md)
|
||||
- [Slovo Whitepaper](docs/papers/SLOVO_WHITEPAPER.md)
|
||||
- [Glagol Whitepaper](docs/papers/GLAGOL_WHITEPAPER.md)
|
||||
|
||||
|
||||
83
benchmarks/README.md
Normal file
83
benchmarks/README.md
Normal file
@ -0,0 +1,83 @@
|
||||
# Slovo Benchmark Suite Catalog
|
||||
|
||||
Release stage: `1.0.0-beta.14`.
|
||||
|
||||
The benchmark suite is beta-scoped local tooling. It catalogs deterministic
|
||||
same-machine comparison scaffolds for Slovo and sibling implementations. It
|
||||
does not publish benchmark results, set performance thresholds, define optimizer
|
||||
claims, or create cross-machine comparisons.
|
||||
|
||||
## Suite Listing
|
||||
|
||||
From the repository root, list the suite catalog for humans:
|
||||
|
||||
```bash
|
||||
python3 benchmarks/runner.py --suite-list
|
||||
```
|
||||
|
||||
List the same catalog as beta tooling metadata:
|
||||
|
||||
```bash
|
||||
python3 benchmarks/runner.py --suite-list --json
|
||||
```
|
||||
|
||||
The non-JSON listing is for local review. The JSON listing is for local gates
|
||||
and tooling adapters that need the current suite inventory. The JSON field set
|
||||
is not a stable public schema; it may change during beta releases.
|
||||
Both forms verify the required scaffold files for each suite:
|
||||
`benchmark.json`, `run.py`, `slovo.toml`, and `src/main.slo`.
|
||||
|
||||
Each suite still owns its local metadata and run commands:
|
||||
|
||||
```bash
|
||||
python3 benchmarks/<suite>/run.py --list
|
||||
python3 benchmarks/<suite>/run.py --list --json
|
||||
python3 benchmarks/<suite>/run.py --dry-run
|
||||
```
|
||||
|
||||
## Current Suites
|
||||
|
||||
All current suites provide Slovo, C, Rust, Python, Clojure, and Common
|
||||
Lisp/SBCL source slots. Missing local toolchains are skipped by the per-suite
|
||||
runner where possible.
|
||||
|
||||
| Suite | Focus | Base checksum | Hot-loop checksum | Runtime args |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `math-loop` | arithmetic and scalar accumulation | `5000001` | `50000001` | none |
|
||||
| `branch-loop` | deterministic branch-heavy integer loop | `1185071` | `220775` | none |
|
||||
| `parse-loop` | repeated signed decimal `i32` parsing | `345000001` | `450000001` | `12345` |
|
||||
| `array-index-loop` | immutable fixed-array indexing | `3875007` | `38750007` | none |
|
||||
| `string-eq-loop` | fixed string lookup and equality | `4600001` | `46000001` | `omega` |
|
||||
| `array-struct-field-loop` | fixed-array access through an immutable struct field | `3875011` | `38750011` | none |
|
||||
| `enum-struct-payload-loop` | enum payload matching with struct and array access | `3500013` | `35000013` | none |
|
||||
| `vec-i32-index-loop` | runtime-owned `(vec i32)` indexing | `3875007` | `38750007` | none |
|
||||
| `vec-string-eq-loop` | runtime-owned `(vec string)` lookup and equality | `4600001` | `46000001` | `omega` |
|
||||
| `json-quote-loop` | compact JSON string quoting | `15000001` | `150000001` | `slo"vo\path` |
|
||||
|
||||
The base loop count is `1000000` for every current suite. The hot-loop count is
|
||||
`10000000` for every current suite. The runner supplies loop counts and runtime
|
||||
arguments at execution time so native compilers cannot fold the work into a
|
||||
constant answer.
|
||||
|
||||
## Local Evidence Only
|
||||
|
||||
Benchmark output is local-machine evidence only:
|
||||
|
||||
- cold-process mode measures execution after each implementation has been built
|
||||
once; it does not include compile time
|
||||
- hot-loop mode is startup-amortized local evidence and reports total time plus
|
||||
normalized timing for the base loop count
|
||||
- Clojure timings include JVM and Clojure startup
|
||||
- Common Lisp timings include SBCL script startup
|
||||
- reported timings depend on the local CPU, OS, compiler versions, toolchain
|
||||
availability, thermal/load state, and runner configuration
|
||||
|
||||
This catalog intentionally publishes no timing numbers.
|
||||
|
||||
## Exclusions
|
||||
|
||||
`1.0.0-beta.14` does not add benchmark kernels, publish timings, define
|
||||
performance thresholds, define a stable JSON schema, change the Slovo source
|
||||
language, change runtime behavior, change standard-library or API contracts,
|
||||
change diagnostic output, change ABI/layout behavior, or make cross-machine
|
||||
performance claims.
|
||||
1
benchmarks/json-quote-loop/.gitignore
vendored
Normal file
1
benchmarks/json-quote-loop/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
build/
|
||||
71
benchmarks/json-quote-loop/README.md
Normal file
71
benchmarks/json-quote-loop/README.md
Normal file
@ -0,0 +1,71 @@
|
||||
# JSON Quote Loop Benchmark Scaffold
|
||||
|
||||
Release: `1.0.0-beta.7`.
|
||||
|
||||
This benchmark compares compact JSON string quoting plus checksum accumulation
|
||||
across Slovo, C, Rust, Python, Clojure, and Common Lisp/SBCL on the same
|
||||
machine. It is same machine local evidence only.
|
||||
|
||||
It is not a published benchmark result, performance threshold, optimizer
|
||||
claim, or cross-machine comparison.
|
||||
|
||||
## Files
|
||||
|
||||
- `src/main.slo`: Slovo project benchmark fixture
|
||||
- `c/json_quote_loop.c`: C comparison implementation
|
||||
- `rust/json_quote_loop.rs`: Rust comparison implementation
|
||||
- `python/json_quote_loop.py`: Python comparison implementation
|
||||
- `clojure/json_quote_loop.clj`: Clojure comparison implementation
|
||||
- `common-lisp/json_quote_loop.lisp`: Common Lisp/SBCL comparison implementation
|
||||
- `run.py`: build/run/timing harness
|
||||
|
||||
All implementations print checksum `15000001` for loop count `1000000` and
|
||||
target string `slo"vo\path`. Hot-loop mode uses loop count `10000000` and
|
||||
checksum `150000001`.
|
||||
|
||||
## Commands
|
||||
|
||||
Run from the repository root:
|
||||
|
||||
```bash
|
||||
python3 benchmarks/json-quote-loop/run.py --list
|
||||
python3 benchmarks/json-quote-loop/run.py --dry-run
|
||||
python3 benchmarks/json-quote-loop/run.py --only python --repeats 3 --warmups 1
|
||||
python3 benchmarks/json-quote-loop/run.py --mode hot-loop --only slovo --only c --only rust
|
||||
```
|
||||
|
||||
To include Slovo, build or point at a Glagol binary and make sure host Clang is
|
||||
available:
|
||||
|
||||
```bash
|
||||
cargo build --manifest-path compiler/Cargo.toml --bin glagol
|
||||
python3 benchmarks/json-quote-loop/run.py --glagol compiler/target/debug/glagol
|
||||
```
|
||||
|
||||
The runner skips missing C/Rust/Slovo/Clojure/SBCL toolchains where possible.
|
||||
Use `--only` multiple times to select implementations:
|
||||
|
||||
```bash
|
||||
python3 benchmarks/json-quote-loop/run.py --only slovo --only c --only rust --only clojure --only common_lisp
|
||||
```
|
||||
|
||||
Clojure is detected with `clojure` on PATH, `CLOJURE`, or `CLOJURE_JAR`.
|
||||
Common Lisp is detected with `sbcl` on PATH, `SBCL`, or `--sbcl`.
|
||||
|
||||
## Comparison Method
|
||||
|
||||
- The runner builds each implementation once before timing. The reported
|
||||
numbers measure execution only, not compile time.
|
||||
- Slovo timings use `glagol build`, which currently lowers to LLVM and then
|
||||
invokes host `clang -O2` with `runtime/runtime.c`.
|
||||
- C timings use `clang -O2 -std=c11`.
|
||||
- Rust timings use `rustc -C opt-level=3 -C debuginfo=0`.
|
||||
- The measured loop quotes one runtime-supplied ASCII string containing a
|
||||
quote and a backslash, then accumulates the quoted byte length.
|
||||
|
||||
Timing is cold-process local-machine evidence only. Clojure timings include
|
||||
JVM and Clojure startup, while Common Lisp timings include SBCL script
|
||||
startup.
|
||||
|
||||
Hot-loop mode is startup-amortized local evidence. It runs a larger loop count
|
||||
and reports total time plus normalized time for the base `1000000` loop count.
|
||||
11
benchmarks/json-quote-loop/benchmark.json
Normal file
11
benchmarks/json-quote-loop/benchmark.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"benchmark": "json-quote-loop",
|
||||
"source_stem": "json_quote_loop",
|
||||
"loop_count": 1000000,
|
||||
"expected_checksum": "15000001",
|
||||
"stdin": "1000000\n",
|
||||
"hot_loop_count": 10000000,
|
||||
"hot_expected_checksum": "150000001",
|
||||
"hot_stdin": "10000000\n",
|
||||
"run_args": ["slo\"vo\\path"]
|
||||
}
|
||||
118
benchmarks/json-quote-loop/c/json_quote_loop.c
Normal file
118
benchmarks/json-quote-loop/c/json_quote_loop.c
Normal file
@ -0,0 +1,118 @@
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define LOOP_COUNT 1000000
|
||||
#define EXPECTED_CHECKSUM 15000001
|
||||
|
||||
static int32_t configured_loop_count(void) {
|
||||
int32_t value = LOOP_COUNT;
|
||||
if (scanf("%d", &value) != 1 || value <= 0) {
|
||||
return LOOP_COUNT;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static const char *configured_target(int argc, char **argv) {
|
||||
return argc > 1 ? argv[1] : "slo\"vo\\path";
|
||||
}
|
||||
|
||||
static char json_hex_digit(unsigned char value) {
|
||||
return value < 10 ? (char)('0' + value) : (char)('A' + (value - 10));
|
||||
}
|
||||
|
||||
static char *quote_json_string(const char *text) {
|
||||
size_t len = 2;
|
||||
for (const unsigned char *cursor = (const unsigned char *)text; *cursor != '\0'; cursor++) {
|
||||
switch (*cursor) {
|
||||
case '"':
|
||||
case '\\':
|
||||
case '\n':
|
||||
case '\t':
|
||||
case '\r':
|
||||
case '\b':
|
||||
case '\f':
|
||||
len += 2;
|
||||
break;
|
||||
default:
|
||||
len += *cursor < 0x20 ? 6 : 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
char *out = malloc(len + 1);
|
||||
if (out == NULL) {
|
||||
exit(2);
|
||||
}
|
||||
|
||||
char *write = out;
|
||||
*write++ = '"';
|
||||
for (const unsigned char *cursor = (const unsigned char *)text; *cursor != '\0'; cursor++) {
|
||||
switch (*cursor) {
|
||||
case '"':
|
||||
*write++ = '\\';
|
||||
*write++ = '"';
|
||||
break;
|
||||
case '\\':
|
||||
*write++ = '\\';
|
||||
*write++ = '\\';
|
||||
break;
|
||||
case '\n':
|
||||
*write++ = '\\';
|
||||
*write++ = 'n';
|
||||
break;
|
||||
case '\t':
|
||||
*write++ = '\\';
|
||||
*write++ = 't';
|
||||
break;
|
||||
case '\r':
|
||||
*write++ = '\\';
|
||||
*write++ = 'r';
|
||||
break;
|
||||
case '\b':
|
||||
*write++ = '\\';
|
||||
*write++ = 'b';
|
||||
break;
|
||||
case '\f':
|
||||
*write++ = '\\';
|
||||
*write++ = 'f';
|
||||
break;
|
||||
default:
|
||||
if (*cursor < 0x20) {
|
||||
*write++ = '\\';
|
||||
*write++ = 'u';
|
||||
*write++ = '0';
|
||||
*write++ = '0';
|
||||
*write++ = json_hex_digit((unsigned char)(*cursor >> 4));
|
||||
*write++ = json_hex_digit((unsigned char)(*cursor & 0x0F));
|
||||
} else {
|
||||
*write++ = (char)*cursor;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
*write++ = '"';
|
||||
*write = '\0';
|
||||
return out;
|
||||
}
|
||||
|
||||
static int32_t json_quote_loop(int32_t limit, const char *target) {
|
||||
int32_t i = 0;
|
||||
int32_t acc = 1;
|
||||
|
||||
while (i < limit) {
|
||||
char *quoted = quote_json_string(target);
|
||||
acc += (int32_t)strlen(quoted);
|
||||
free(quoted);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int32_t result = json_quote_loop(configured_loop_count(), configured_target(argc, argv));
|
||||
printf("%d\n", result);
|
||||
return result == EXPECTED_CHECKSUM ? 0 : 1;
|
||||
}
|
||||
42
benchmarks/json-quote-loop/clojure/json_quote_loop.clj
Normal file
42
benchmarks/json-quote-loop/clojure/json_quote_loop.clj
Normal file
@ -0,0 +1,42 @@
|
||||
(set! *warn-on-reflection* true)
|
||||
(set! *unchecked-math* :warn-on-boxed)
|
||||
|
||||
(def loop-count 1000000)
|
||||
(def expected-checksum 15000001)
|
||||
|
||||
(defn configured-loop-count []
|
||||
(try
|
||||
(let [line (read-line)
|
||||
value (Integer/parseInt (.trim ^String line))]
|
||||
(if (pos? value) value loop-count))
|
||||
(catch Exception _
|
||||
loop-count)))
|
||||
|
||||
(defn configured-target []
|
||||
(or (first *command-line-args*) "slo\"vo\\path"))
|
||||
|
||||
(defn quote-json-string [^String value]
|
||||
(let [builder (StringBuilder.)]
|
||||
(.append builder \")
|
||||
(dotimes [index (.length value)]
|
||||
(let [ch (.charAt value index)]
|
||||
(case ch
|
||||
\" (.append builder "\\\"")
|
||||
\\ (.append builder "\\\\")
|
||||
\newline (.append builder "\\n")
|
||||
\tab (.append builder "\\t")
|
||||
\return (.append builder "\\r")
|
||||
(.append builder ch))))
|
||||
(.append builder \")
|
||||
(.toString builder)))
|
||||
|
||||
(defn json-quote-loop [limit target]
|
||||
(loop [i 0
|
||||
acc 1]
|
||||
(if (< i limit)
|
||||
(recur (inc i) (+ acc (.length ^String (quote-json-string target))))
|
||||
acc)))
|
||||
|
||||
(let [result (json-quote-loop (configured-loop-count) (configured-target))]
|
||||
(println result)
|
||||
(System/exit (if (= result expected-checksum) 0 1)))
|
||||
47
benchmarks/json-quote-loop/common-lisp/json_quote_loop.lisp
Normal file
47
benchmarks/json-quote-loop/common-lisp/json_quote_loop.lisp
Normal file
@ -0,0 +1,47 @@
|
||||
(declaim (optimize (speed 3) (safety 0) (debug 0)))
|
||||
|
||||
(defconstant +loop-count+ 1000000)
|
||||
(defconstant +expected-checksum+ 15000001)
|
||||
|
||||
(declaim (ftype (function () fixnum) configured-loop-count))
|
||||
(defun configured-loop-count ()
|
||||
(handler-case
|
||||
(let ((line (read-line *standard-input* nil nil)))
|
||||
(if line
|
||||
(let ((value (parse-integer line :junk-allowed t)))
|
||||
(if (> value 0) value +loop-count+))
|
||||
+loop-count+))
|
||||
(error () +loop-count+)))
|
||||
|
||||
(declaim (ftype (function () string) configured-target))
|
||||
(defun configured-target ()
|
||||
(or (second sb-ext:*posix-argv*) "slo\"vo\\path"))
|
||||
|
||||
(declaim (ftype (function (string) string) quote-json-string))
|
||||
(defun quote-json-string (value)
|
||||
(with-output-to-string (out)
|
||||
(write-char #\" out)
|
||||
(loop for ch across value
|
||||
do (case ch
|
||||
(#\" (write-string "\\\"" out))
|
||||
(#\\ (write-string "\\\\" out))
|
||||
(#\Newline (write-string "\\n" out))
|
||||
(#\Tab (write-string "\\t" out))
|
||||
(#\Return (write-string "\\r" out))
|
||||
(otherwise (write-char ch out))))
|
||||
(write-char #\" out)))
|
||||
|
||||
(declaim (ftype (function (fixnum string) fixnum) json-quote-loop))
|
||||
(defun json-quote-loop (limit target)
|
||||
(declare (type fixnum limit)
|
||||
(type string target))
|
||||
(loop with i of-type fixnum = 0
|
||||
with acc of-type fixnum = 1
|
||||
while (< i limit)
|
||||
do (setf acc (+ acc (length (quote-json-string target)))
|
||||
i (+ i 1))
|
||||
finally (return acc)))
|
||||
|
||||
(let ((result (json-quote-loop (configured-loop-count) (configured-target))))
|
||||
(format t "~D~%" result)
|
||||
(sb-ext:exit :code (if (= result +expected-checksum+) 0 1)))
|
||||
53
benchmarks/json-quote-loop/python/json_quote_loop.py
Normal file
53
benchmarks/json-quote-loop/python/json_quote_loop.py
Normal file
@ -0,0 +1,53 @@
|
||||
import sys
|
||||
|
||||
|
||||
LOOP_COUNT = 1_000_000
|
||||
EXPECTED_CHECKSUM = 15_000_001
|
||||
|
||||
|
||||
def configured_loop_count() -> int:
|
||||
try:
|
||||
value = int(input().strip())
|
||||
except (EOFError, ValueError):
|
||||
return LOOP_COUNT
|
||||
|
||||
return value if value > 0 else LOOP_COUNT
|
||||
|
||||
|
||||
def configured_target() -> str:
|
||||
return sys.argv[1] if len(sys.argv) > 1 else 'slo"vo\\path'
|
||||
|
||||
|
||||
def quote_json_string(value: str) -> str:
|
||||
return (
|
||||
'"'
|
||||
+ value.replace("\\", "\\\\")
|
||||
.replace('"', '\\"')
|
||||
.replace("\n", "\\n")
|
||||
.replace("\t", "\\t")
|
||||
.replace("\r", "\\r")
|
||||
.replace("\b", "\\b")
|
||||
.replace("\f", "\\f")
|
||||
+ '"'
|
||||
)
|
||||
|
||||
|
||||
def json_quote_loop(limit: int, target: str) -> int:
|
||||
i = 0
|
||||
acc = 1
|
||||
|
||||
while i < limit:
|
||||
acc += len(quote_json_string(target))
|
||||
i += 1
|
||||
|
||||
return acc
|
||||
|
||||
|
||||
def main() -> int:
|
||||
result = json_quote_loop(configured_loop_count(), configured_target())
|
||||
print(result)
|
||||
return 0 if result == EXPECTED_CHECKSUM else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
13
benchmarks/json-quote-loop/run.py
Normal file
13
benchmarks/json-quote-loop/run.py
Normal file
@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the local json-quote-loop benchmark scaffold."""
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from runner import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(Path(__file__).resolve().parent, sys.argv[1:]))
|
||||
62
benchmarks/json-quote-loop/rust/json_quote_loop.rs
Normal file
62
benchmarks/json-quote-loop/rust/json_quote_loop.rs
Normal file
@ -0,0 +1,62 @@
|
||||
use std::io::Read;
|
||||
|
||||
const LOOP_COUNT: i32 = 1_000_000;
|
||||
const EXPECTED_CHECKSUM: i32 = 15_000_001;
|
||||
|
||||
fn configured_loop_count() -> i32 {
|
||||
let mut input = String::new();
|
||||
if std::io::stdin().read_to_string(&mut input).is_err() {
|
||||
return LOOP_COUNT;
|
||||
}
|
||||
|
||||
input
|
||||
.trim()
|
||||
.parse::<i32>()
|
||||
.ok()
|
||||
.filter(|value| *value > 0)
|
||||
.unwrap_or(LOOP_COUNT)
|
||||
}
|
||||
|
||||
fn configured_target() -> String {
|
||||
std::env::args()
|
||||
.nth(1)
|
||||
.unwrap_or_else(|| "slo\"vo\\path".to_string())
|
||||
}
|
||||
|
||||
fn quote_json_string(value: &str) -> String {
|
||||
let mut out = String::with_capacity(value.len() + 2);
|
||||
out.push('"');
|
||||
for ch in value.chars() {
|
||||
match ch {
|
||||
'"' => out.push_str("\\\""),
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
'\r' => out.push_str("\\r"),
|
||||
'\u{08}' => out.push_str("\\b"),
|
||||
'\u{0c}' => out.push_str("\\f"),
|
||||
_ => out.push(ch),
|
||||
}
|
||||
}
|
||||
out.push('"');
|
||||
out
|
||||
}
|
||||
|
||||
fn json_quote_loop(limit: i32, target: &str) -> i32 {
|
||||
let mut i = 0;
|
||||
let mut acc = 1;
|
||||
|
||||
while i < limit {
|
||||
acc += quote_json_string(target).len() as i32;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
acc
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let target = configured_target();
|
||||
let result = json_quote_loop(configured_loop_count(), &target);
|
||||
println!("{}", result);
|
||||
std::process::exit(if result == EXPECTED_CHECKSUM { 0 } else { 1 });
|
||||
}
|
||||
4
benchmarks/json-quote-loop/slovo.toml
Normal file
4
benchmarks/json-quote-loop/slovo.toml
Normal file
@ -0,0 +1,4 @@
|
||||
[project]
|
||||
name = "json-quote-loop"
|
||||
source_root = "src"
|
||||
entry = "main"
|
||||
56
benchmarks/json-quote-loop/src/main.slo
Normal file
56
benchmarks/json-quote-loop/src/main.slo
Normal file
@ -0,0 +1,56 @@
|
||||
; Benchmark scaffold fixture for local-machine JSON string quoting comparisons only.
|
||||
; Keep LOOP_COUNT and EXPECTED_CHECKSUM aligned with the C/Rust/Python fixtures.
|
||||
; The runner supplies the target through argv and the loop count through stdin
|
||||
; or argv so the quoting path stays runtime-configured.
|
||||
|
||||
(module main)
|
||||
|
||||
(fn loop_count () -> i32
|
||||
1000000)
|
||||
|
||||
(fn expected_checksum () -> i32
|
||||
15000001)
|
||||
|
||||
(fn parse_stdin_loop_count () -> (result i32 i32)
|
||||
(let input (result string i32) (std.io.read_stdin_result))
|
||||
(match input
|
||||
((ok text)
|
||||
(std.string.parse_i32_result text))
|
||||
((err code)
|
||||
(err i32 i32 code))))
|
||||
|
||||
(fn parse_arg_loop_count () -> (result i32 i32)
|
||||
(std.string.parse_i32_result (std.process.arg 2)))
|
||||
|
||||
(fn configured_stdin_loop_count () -> i32
|
||||
(let parsed_stdin (result i32 i32) (parse_stdin_loop_count))
|
||||
(if (is_ok parsed_stdin)
|
||||
(unwrap_ok parsed_stdin)
|
||||
(loop_count)))
|
||||
|
||||
(fn configured_loop_count () -> i32
|
||||
(let parsed_arg (result i32 i32) (parse_arg_loop_count))
|
||||
(if (is_ok parsed_arg)
|
||||
(unwrap_ok parsed_arg)
|
||||
(configured_stdin_loop_count)))
|
||||
|
||||
(fn target_text () -> string
|
||||
(std.process.arg 1))
|
||||
|
||||
(fn json_quote_loop ((limit i32) (target string)) -> i32
|
||||
(var i i32 0)
|
||||
(var acc i32 1)
|
||||
(while (< i limit)
|
||||
(set acc (+ acc (std.string.len (std.json.quote_string target))))
|
||||
(set i (+ i 1)))
|
||||
acc)
|
||||
|
||||
(fn main () -> i32
|
||||
(let result i32 (json_quote_loop (configured_loop_count) (target_text)))
|
||||
(std.io.print_i32 result)
|
||||
(if (= result (expected_checksum))
|
||||
0
|
||||
1))
|
||||
|
||||
(test "json quote loop checksum is deterministic"
|
||||
(= (json_quote_loop 3 "slo\"vo\\path") 46))
|
||||
@ -18,6 +18,12 @@ from typing import Callable
|
||||
|
||||
TIMING_SCOPE = "local-machine comparison only"
|
||||
TIMING_MODES = ["cold-process", "hot-loop"]
|
||||
SUITE_NAME = "glagol-local-benchmark-suite"
|
||||
LOCAL_TIMING_DISCLAIMER = (
|
||||
"Local timing comparison only; not a published benchmark result and not a cross-machine performance claim."
|
||||
)
|
||||
REQUIRED_BENCHMARK_FILES = ["benchmark.json", "run.py", "slovo.toml", "src/main.slo"]
|
||||
EXPECTED_IMPLEMENTATION_NAMES = ["slovo", "c", "rust", "python", "clojure", "common_lisp"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -52,6 +58,14 @@ class Implementation:
|
||||
|
||||
|
||||
def main(root: Path, argv: list[str]) -> int:
|
||||
if any(arg == "--suite-list" for arg in argv):
|
||||
return main_suite(root, argv)
|
||||
if not (root / "benchmark.json").is_file():
|
||||
parser = argparse.ArgumentParser(description="Shared local Glagol benchmark runner.")
|
||||
parser.add_argument("--suite-list", action="store_true", help="list suite metadata and exit")
|
||||
parser.add_argument("--json", action="store_true", help="emit JSON for --suite-list")
|
||||
parser.error("run from a benchmark run.py, or pass --suite-list at the benchmark suite root")
|
||||
|
||||
spec = read_spec(root)
|
||||
implementations = available_implementations(root, spec)
|
||||
parser = argparse.ArgumentParser(description=f"Run local {spec.name} timing comparisons.")
|
||||
@ -89,6 +103,25 @@ def main(root: Path, argv: list[str]) -> int:
|
||||
return 1 if any(result["status"] == "failed" for result in results) else 0
|
||||
|
||||
|
||||
def main_suite(root: Path, argv: list[str]) -> int:
|
||||
parser = argparse.ArgumentParser(description="List local Glagol benchmark suite metadata.")
|
||||
parser.add_argument("--suite-list", action="store_true", help="list suite metadata and exit")
|
||||
parser.add_argument("--json", action="store_true", help="emit JSON for suite metadata")
|
||||
args = parser.parse_args(argv)
|
||||
if not args.suite_list:
|
||||
parser.error("pass --suite-list to list benchmark suite metadata")
|
||||
|
||||
suite_root = resolve_suite_root(root)
|
||||
emit_suite_list(build_suite_catalog(suite_root), args.json)
|
||||
return 0
|
||||
|
||||
|
||||
def resolve_suite_root(root: Path) -> Path:
|
||||
if (root / "benchmark.json").is_file():
|
||||
return root.parent
|
||||
return root
|
||||
|
||||
|
||||
def read_spec(root: Path) -> BenchmarkSpec:
|
||||
data = json.loads((root / "benchmark.json").read_text(encoding="utf-8"))
|
||||
loop_count = int(data["loop_count"])
|
||||
@ -278,6 +311,143 @@ def select_implementations(implementations: list[Implementation], names: list[st
|
||||
return [impl for impl in implementations if impl.name in selected_names]
|
||||
|
||||
|
||||
def build_suite_catalog(suite_root: Path) -> dict[str, object]:
|
||||
benchmarks: list[dict[str, object]] = []
|
||||
implementation_slot_count = 0
|
||||
missing_required_files: list[str] = []
|
||||
missing_implementation_slots: list[str] = []
|
||||
|
||||
for root in suite_benchmark_roots(suite_root):
|
||||
spec = read_spec(root)
|
||||
implementations = available_implementations(root, spec)
|
||||
implementation_slot_count += len(implementations)
|
||||
benchmark = suite_benchmark_metadata(suite_root, root, spec, implementations)
|
||||
benchmarks.append(benchmark)
|
||||
|
||||
directory = str(benchmark["directory"])
|
||||
for required_file in benchmark["required_files"]:
|
||||
assert isinstance(required_file, dict)
|
||||
if required_file["status"] != "present":
|
||||
missing_required_files.append(f"{directory}/{required_file['path']}")
|
||||
|
||||
present_implementations = {
|
||||
str(implementation["name"])
|
||||
for implementation in benchmark["implementation_slots"]
|
||||
if isinstance(implementation, dict)
|
||||
}
|
||||
for expected in EXPECTED_IMPLEMENTATION_NAMES:
|
||||
if expected not in present_implementations:
|
||||
missing_implementation_slots.append(f"{directory}:{expected}")
|
||||
|
||||
return {
|
||||
"suite": SUITE_NAME,
|
||||
"timing_scope": TIMING_SCOPE,
|
||||
"timing_modes": TIMING_MODES,
|
||||
"timing_disclaimer": LOCAL_TIMING_DISCLAIMER,
|
||||
"benchmark_count": len(benchmarks),
|
||||
"benchmarks": benchmarks,
|
||||
"verification": {
|
||||
"status": "ok" if not missing_required_files and not missing_implementation_slots else "incomplete",
|
||||
"benchmark_metadata_files": len(benchmarks),
|
||||
"required_files": len(benchmarks) * len(REQUIRED_BENCHMARK_FILES),
|
||||
"missing_required_files": missing_required_files,
|
||||
"implementation_slots": implementation_slot_count,
|
||||
"expected_implementation_slots": len(benchmarks) * len(EXPECTED_IMPLEMENTATION_NAMES),
|
||||
"missing_implementation_slots": missing_implementation_slots,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def suite_benchmark_roots(suite_root: Path) -> list[Path]:
|
||||
return sorted(
|
||||
[path for path in suite_root.iterdir() if path.is_dir() and (path / "benchmark.json").is_file()],
|
||||
key=lambda path: path.name,
|
||||
)
|
||||
|
||||
|
||||
def suite_benchmark_metadata(
|
||||
suite_root: Path,
|
||||
root: Path,
|
||||
spec: BenchmarkSpec,
|
||||
implementations: list[Implementation],
|
||||
) -> dict[str, object]:
|
||||
return {
|
||||
"name": spec.name,
|
||||
"directory": str(root.relative_to(suite_root)),
|
||||
"source_stem": spec.source_stem,
|
||||
"timing_modes": TIMING_MODES,
|
||||
"loop_count_source": "stdin",
|
||||
"loop_count": spec.loop_count,
|
||||
"hot_loop_count": spec.hot_loop_count,
|
||||
"expected_checksum": spec.expected_checksum,
|
||||
"hot_expected_checksum": spec.hot_expected_checksum,
|
||||
"required_files": [
|
||||
{
|
||||
"path": relative,
|
||||
"status": "present" if (root / relative).is_file() else "missing",
|
||||
}
|
||||
for relative in REQUIRED_BENCHMARK_FILES
|
||||
],
|
||||
"checksum_metadata": {
|
||||
"cold_process": {
|
||||
"expected_checksum": spec.expected_checksum,
|
||||
"stdin": spec.stdin_text,
|
||||
},
|
||||
"hot_loop": {
|
||||
"expected_checksum": spec.hot_expected_checksum,
|
||||
"stdin": spec.hot_stdin_text,
|
||||
},
|
||||
},
|
||||
"run_args": spec.run_args,
|
||||
"implementation_slots": [
|
||||
{
|
||||
"name": impl.name,
|
||||
"language": impl.language,
|
||||
"source": str(impl.source.relative_to(suite_root)),
|
||||
}
|
||||
for impl in implementations
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def emit_suite_list(metadata: dict[str, object], as_json: bool) -> None:
|
||||
if as_json:
|
||||
print(json.dumps(metadata, indent=2, sort_keys=True))
|
||||
return
|
||||
|
||||
print(f"{metadata['suite']}: {metadata['timing_scope']}")
|
||||
print(str(metadata["timing_disclaimer"]))
|
||||
print(f"benchmark_count={metadata['benchmark_count']}")
|
||||
print(f"timing_modes={','.join(TIMING_MODES)}")
|
||||
verification = metadata["verification"]
|
||||
assert isinstance(verification, dict)
|
||||
print(f"verification_status={verification['status']}")
|
||||
print(f"required_files={verification['required_files']}")
|
||||
print(f"implementation_slots={verification['implementation_slots']}")
|
||||
print("benchmarks:")
|
||||
for benchmark in metadata["benchmarks"]:
|
||||
assert isinstance(benchmark, dict)
|
||||
print(
|
||||
" {name} ({directory}): loop_count={loop_count} hot_loop_count={hot_loop_count} "
|
||||
"expected_checksum={expected_checksum} hot_expected_checksum={hot_expected_checksum}".format(
|
||||
name=benchmark["name"],
|
||||
directory=benchmark["directory"],
|
||||
loop_count=benchmark["loop_count"],
|
||||
hot_loop_count=benchmark["hot_loop_count"],
|
||||
expected_checksum=benchmark["expected_checksum"],
|
||||
hot_expected_checksum=benchmark["hot_expected_checksum"],
|
||||
)
|
||||
)
|
||||
print(" required_files:")
|
||||
for required_file in benchmark["required_files"]:
|
||||
assert isinstance(required_file, dict)
|
||||
print(f" {required_file['path']}: {required_file['status']}")
|
||||
print(" implementations:")
|
||||
for implementation in benchmark["implementation_slots"]:
|
||||
assert isinstance(implementation, dict)
|
||||
print(f" {implementation['name']}: {implementation['language']} ({implementation['source']})")
|
||||
|
||||
|
||||
def emit_list(root: Path, spec: BenchmarkSpec, implementations: list[Implementation], as_json: bool) -> None:
|
||||
metadata = {
|
||||
"benchmark": spec.name,
|
||||
@ -520,3 +690,7 @@ def shlex_quote(value: str) -> str:
|
||||
if value and all(char.isalnum() or char in "/._:-" for char in value):
|
||||
return value
|
||||
return "'" + value.replace("'", "'\"'\"'") + "'"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(Path(__file__).resolve().parent, sys.argv[1:]))
|
||||
|
||||
2
compiler/Cargo.lock
generated
2
compiler/Cargo.lock
generated
@ -4,4 +4,4 @@ version = 3
|
||||
|
||||
[[package]]
|
||||
name = "glagol"
|
||||
version = "1.0.0-beta"
|
||||
version = "1.0.0-beta.24"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "glagol"
|
||||
version = "1.0.0-beta"
|
||||
version = "1.0.0-beta.24"
|
||||
edition = "2021"
|
||||
description = "Glagol, the first compiler for the Slovo language"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
@ -3,6 +3,7 @@ use crate::{token::Span, types::Type};
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Program {
|
||||
pub module: String,
|
||||
pub type_aliases: Vec<TypeAliasDecl>,
|
||||
pub enums: Vec<EnumDecl>,
|
||||
pub structs: Vec<StructDecl>,
|
||||
pub c_imports: Vec<CImportDecl>,
|
||||
@ -10,6 +11,15 @@ pub struct Program {
|
||||
pub tests: Vec<Test>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TypeAliasDecl {
|
||||
pub name: String,
|
||||
pub name_span: Span,
|
||||
pub target: Type,
|
||||
pub target_span: Span,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EnumDecl {
|
||||
pub name: String,
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
use std::collections::{hash_map::Entry, HashMap, HashSet};
|
||||
use std::collections::{hash_map::Entry, BTreeSet, HashMap, HashSet};
|
||||
|
||||
use crate::{
|
||||
ast::{
|
||||
BinaryOp, EnumDecl, Expr, ExprKind, Function, MatchArm, MatchPatternKind, Program,
|
||||
StructDecl, StructInitField, Test,
|
||||
StructDecl, StructInitField, Test, TypeAliasDecl,
|
||||
},
|
||||
diag::Diagnostic,
|
||||
lower, std_runtime,
|
||||
lower,
|
||||
reserved::{is_generic_type_parameter_name, unsupported_generic_type_parameter},
|
||||
std_runtime,
|
||||
token::Span,
|
||||
types::Type,
|
||||
unsafe_ops,
|
||||
@ -294,13 +296,23 @@ pub fn check_program_with_imports(
|
||||
|
||||
fn check_program_inner(
|
||||
file: &str,
|
||||
program: Program,
|
||||
mut program: Program,
|
||||
external_functions: &[ExternalFunction],
|
||||
external_structs: &[ExternalStruct],
|
||||
external_enums: &[ExternalEnum],
|
||||
project_resolution: bool,
|
||||
) -> Result<CheckedProgram, Vec<Diagnostic>> {
|
||||
let mut errors = Vec::new();
|
||||
errors.extend(resolve_type_aliases(
|
||||
file,
|
||||
&mut program,
|
||||
external_structs,
|
||||
external_enums,
|
||||
));
|
||||
if !errors.is_empty() {
|
||||
return Err(errors);
|
||||
}
|
||||
|
||||
let mut functions = HashMap::new();
|
||||
let mut structs = HashMap::new();
|
||||
let mut enums = HashMap::new();
|
||||
@ -580,6 +592,645 @@ fn is_reserved_callable_name(name: &str) -> bool {
|
||||
std_runtime::is_reserved_name(name) || unsafe_ops::is_reserved_head(name)
|
||||
}
|
||||
|
||||
fn resolve_type_aliases(
|
||||
file: &str,
|
||||
program: &mut Program,
|
||||
external_structs: &[ExternalStruct],
|
||||
external_enums: &[ExternalEnum],
|
||||
) -> Vec<Diagnostic> {
|
||||
if program.type_aliases.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut errors = Vec::new();
|
||||
let mut struct_names = HashMap::new();
|
||||
for struct_decl in &program.structs {
|
||||
struct_names
|
||||
.entry(struct_decl.name.clone())
|
||||
.or_insert(struct_decl.name_span);
|
||||
}
|
||||
for struct_decl in external_structs {
|
||||
struct_names
|
||||
.entry(struct_decl.name.clone())
|
||||
.or_insert(struct_decl.span);
|
||||
}
|
||||
|
||||
let mut enum_names = HashMap::new();
|
||||
for enum_decl in &program.enums {
|
||||
enum_names
|
||||
.entry(enum_decl.name.clone())
|
||||
.or_insert(enum_decl.name_span);
|
||||
}
|
||||
for enum_decl in external_enums {
|
||||
enum_names
|
||||
.entry(enum_decl.name.clone())
|
||||
.or_insert(enum_decl.span);
|
||||
}
|
||||
|
||||
let mut function_names = HashMap::new();
|
||||
for function in &program.functions {
|
||||
function_names
|
||||
.entry(function.name.clone())
|
||||
.or_insert(function.span);
|
||||
}
|
||||
let mut c_import_names = HashMap::new();
|
||||
for import in &program.c_imports {
|
||||
c_import_names
|
||||
.entry(import.name.clone())
|
||||
.or_insert(import.name_span);
|
||||
}
|
||||
|
||||
let mut aliases = HashMap::<String, &TypeAliasDecl>::new();
|
||||
for alias in &program.type_aliases {
|
||||
match aliases.entry(alias.name.clone()) {
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(alias);
|
||||
}
|
||||
Entry::Occupied(entry) => errors.push(
|
||||
Diagnostic::new(
|
||||
file,
|
||||
"DuplicateTypeAlias",
|
||||
format!("duplicate type alias `{}`", alias.name),
|
||||
)
|
||||
.with_span(alias.name_span)
|
||||
.related("original type alias", entry.get().name_span),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
for alias in &program.type_aliases {
|
||||
if is_reserved_type_name(&alias.name) {
|
||||
errors.push(
|
||||
Diagnostic::new(
|
||||
file,
|
||||
"TypeAliasNameConflict",
|
||||
format!(
|
||||
"type alias `{}` conflicts with a built-in type name",
|
||||
alias.name
|
||||
),
|
||||
)
|
||||
.with_span(alias.name_span)
|
||||
.hint("choose an alias name distinct from built-in type names"),
|
||||
);
|
||||
}
|
||||
if let Some(span) = struct_names.get(&alias.name) {
|
||||
errors.push(
|
||||
Diagnostic::new(
|
||||
file,
|
||||
"TypeAliasNameConflict",
|
||||
format!("type alias `{}` conflicts with a struct", alias.name),
|
||||
)
|
||||
.with_span(alias.name_span)
|
||||
.related("struct declaration", *span),
|
||||
);
|
||||
}
|
||||
if let Some(span) = enum_names.get(&alias.name) {
|
||||
errors.push(
|
||||
Diagnostic::new(
|
||||
file,
|
||||
"TypeAliasNameConflict",
|
||||
format!("type alias `{}` conflicts with an enum", alias.name),
|
||||
)
|
||||
.with_span(alias.name_span)
|
||||
.related("enum declaration", *span),
|
||||
);
|
||||
}
|
||||
if let Some(span) = function_names.get(&alias.name) {
|
||||
errors.push(
|
||||
Diagnostic::new(
|
||||
file,
|
||||
"TypeAliasNameConflict",
|
||||
format!("type alias `{}` conflicts with a function", alias.name),
|
||||
)
|
||||
.with_span(alias.name_span)
|
||||
.related("function declaration", *span),
|
||||
);
|
||||
}
|
||||
if let Some(span) = c_import_names.get(&alias.name) {
|
||||
errors.push(
|
||||
Diagnostic::new(
|
||||
file,
|
||||
"TypeAliasNameConflict",
|
||||
format!("type alias `{}` conflicts with a C import", alias.name),
|
||||
)
|
||||
.with_span(alias.name_span)
|
||||
.related("C import declaration", *span),
|
||||
);
|
||||
}
|
||||
if matches!(&alias.target, Type::Named(name) if name == &alias.name) {
|
||||
errors.push(
|
||||
Diagnostic::new(
|
||||
file,
|
||||
"SelfTypeAlias",
|
||||
format!("type alias `{}` directly aliases itself", alias.name),
|
||||
)
|
||||
.with_span(alias.target_span)
|
||||
.hint("point the alias at an existing concrete type"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !errors.is_empty() {
|
||||
return errors;
|
||||
}
|
||||
|
||||
let known_structs = struct_names.keys().cloned().collect::<HashSet<_>>();
|
||||
let known_enums = enum_names.keys().cloned().collect::<HashSet<_>>();
|
||||
let mut resolved = HashMap::new();
|
||||
let mut failed = HashSet::new();
|
||||
let mut reported_cycles = BTreeSet::new();
|
||||
let mut names = aliases.keys().cloned().collect::<Vec<_>>();
|
||||
names.sort();
|
||||
for name in names {
|
||||
resolve_alias_target(
|
||||
file,
|
||||
&name,
|
||||
&aliases,
|
||||
&known_structs,
|
||||
&known_enums,
|
||||
&mut resolved,
|
||||
&mut failed,
|
||||
&mut Vec::new(),
|
||||
&mut reported_cycles,
|
||||
&mut errors,
|
||||
);
|
||||
}
|
||||
|
||||
if !errors.is_empty() {
|
||||
return errors;
|
||||
}
|
||||
|
||||
rewrite_program_alias_types(program, &resolved);
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn resolve_alias_target(
|
||||
file: &str,
|
||||
name: &str,
|
||||
aliases: &HashMap<String, &TypeAliasDecl>,
|
||||
structs: &HashSet<String>,
|
||||
enums: &HashSet<String>,
|
||||
resolved: &mut HashMap<String, Type>,
|
||||
failed: &mut HashSet<String>,
|
||||
stack: &mut Vec<String>,
|
||||
reported_cycles: &mut BTreeSet<String>,
|
||||
errors: &mut Vec<Diagnostic>,
|
||||
) -> Option<Type> {
|
||||
if let Some(ty) = resolved.get(name) {
|
||||
return Some(ty.clone());
|
||||
}
|
||||
if failed.contains(name) {
|
||||
return None;
|
||||
}
|
||||
if let Some(cycle_start) = stack.iter().position(|candidate| candidate == name) {
|
||||
let cycle = stack[cycle_start..].to_vec();
|
||||
let mut key = cycle.clone();
|
||||
key.sort();
|
||||
if reported_cycles.insert(key.join("|")) {
|
||||
errors.push(type_alias_cycle(file, &cycle, aliases));
|
||||
}
|
||||
failed.extend(cycle);
|
||||
return None;
|
||||
}
|
||||
|
||||
let alias = aliases.get(name).copied()?;
|
||||
stack.push(name.to_string());
|
||||
let target = resolve_alias_target_type(
|
||||
file,
|
||||
&alias.target,
|
||||
alias.target_span,
|
||||
aliases,
|
||||
structs,
|
||||
enums,
|
||||
resolved,
|
||||
failed,
|
||||
stack,
|
||||
reported_cycles,
|
||||
errors,
|
||||
);
|
||||
stack.pop();
|
||||
|
||||
if let Some(target) = target {
|
||||
resolved.insert(name.to_string(), target.clone());
|
||||
Some(target)
|
||||
} else {
|
||||
failed.insert(name.to_string());
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_alias_target_type(
|
||||
file: &str,
|
||||
ty: &Type,
|
||||
span: Span,
|
||||
aliases: &HashMap<String, &TypeAliasDecl>,
|
||||
structs: &HashSet<String>,
|
||||
enums: &HashSet<String>,
|
||||
resolved: &mut HashMap<String, Type>,
|
||||
failed: &mut HashSet<String>,
|
||||
stack: &mut Vec<String>,
|
||||
reported_cycles: &mut BTreeSet<String>,
|
||||
errors: &mut Vec<Diagnostic>,
|
||||
) -> Option<Type> {
|
||||
match ty {
|
||||
Type::Named(name) if aliases.contains_key(name) => resolve_alias_target(
|
||||
file,
|
||||
name,
|
||||
aliases,
|
||||
structs,
|
||||
enums,
|
||||
resolved,
|
||||
failed,
|
||||
stack,
|
||||
reported_cycles,
|
||||
errors,
|
||||
),
|
||||
Type::Named(name) if structs.contains(name) || enums.contains(name) => {
|
||||
Some(Type::Named(name.clone()))
|
||||
}
|
||||
Type::Named(name) => {
|
||||
if is_generic_type_parameter_name(name) {
|
||||
errors.push(unsupported_generic_type_parameter(file, span, name));
|
||||
return None;
|
||||
}
|
||||
errors.push(
|
||||
Diagnostic::new(
|
||||
file,
|
||||
"UnknownTypeAliasTarget",
|
||||
format!("type alias target `{}` is not a known concrete type", name),
|
||||
)
|
||||
.with_span(span)
|
||||
.expected("built-in type, known struct, known enum, or known type alias")
|
||||
.found(name.clone())
|
||||
.hint("declare the target type or alias before checking this alias set"),
|
||||
);
|
||||
None
|
||||
}
|
||||
Type::Array(inner, len) => {
|
||||
let resolved_inner = resolve_alias_target_type(
|
||||
file,
|
||||
inner,
|
||||
span,
|
||||
aliases,
|
||||
structs,
|
||||
enums,
|
||||
resolved,
|
||||
failed,
|
||||
stack,
|
||||
reported_cycles,
|
||||
errors,
|
||||
)?;
|
||||
let target = Type::Array(Box::new(resolved_inner), *len);
|
||||
if alias_target_type_supported(&target, structs, enums) {
|
||||
Some(target)
|
||||
} else {
|
||||
errors.push(unsupported_type_alias_target(file, span, &target));
|
||||
None
|
||||
}
|
||||
}
|
||||
Type::Vec(inner) => {
|
||||
let resolved_inner = resolve_alias_target_type(
|
||||
file,
|
||||
inner,
|
||||
span,
|
||||
aliases,
|
||||
structs,
|
||||
enums,
|
||||
resolved,
|
||||
failed,
|
||||
stack,
|
||||
reported_cycles,
|
||||
errors,
|
||||
)?;
|
||||
let target = Type::Vec(Box::new(resolved_inner));
|
||||
if alias_target_type_supported(&target, structs, enums) {
|
||||
Some(target)
|
||||
} else {
|
||||
errors.push(unsupported_type_alias_target(file, span, &target));
|
||||
None
|
||||
}
|
||||
}
|
||||
Type::Option(inner) => {
|
||||
let resolved_inner = resolve_alias_target_type(
|
||||
file,
|
||||
inner,
|
||||
span,
|
||||
aliases,
|
||||
structs,
|
||||
enums,
|
||||
resolved,
|
||||
failed,
|
||||
stack,
|
||||
reported_cycles,
|
||||
errors,
|
||||
)?;
|
||||
let target = Type::Option(Box::new(resolved_inner));
|
||||
if alias_target_type_supported(&target, structs, enums) {
|
||||
Some(target)
|
||||
} else {
|
||||
errors.push(unsupported_type_alias_target(file, span, &target));
|
||||
None
|
||||
}
|
||||
}
|
||||
Type::Result(ok, err) => {
|
||||
let resolved_ok = resolve_alias_target_type(
|
||||
file,
|
||||
ok,
|
||||
span,
|
||||
aliases,
|
||||
structs,
|
||||
enums,
|
||||
resolved,
|
||||
failed,
|
||||
stack,
|
||||
reported_cycles,
|
||||
errors,
|
||||
)?;
|
||||
let resolved_err = resolve_alias_target_type(
|
||||
file,
|
||||
err,
|
||||
span,
|
||||
aliases,
|
||||
structs,
|
||||
enums,
|
||||
resolved,
|
||||
failed,
|
||||
stack,
|
||||
reported_cycles,
|
||||
errors,
|
||||
)?;
|
||||
let target = Type::Result(Box::new(resolved_ok), Box::new(resolved_err));
|
||||
if alias_target_type_supported(&target, structs, enums) {
|
||||
Some(target)
|
||||
} else {
|
||||
errors.push(unsupported_type_alias_target(file, span, &target));
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if alias_target_type_supported(ty, structs, enums) {
|
||||
Some(ty.clone())
|
||||
} else {
|
||||
errors.push(unsupported_type_alias_target(file, span, ty));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn alias_target_type_supported(
|
||||
ty: &Type,
|
||||
structs: &HashSet<String>,
|
||||
enums: &HashSet<String>,
|
||||
) -> bool {
|
||||
match ty {
|
||||
Type::I32 | Type::I64 | Type::U32 | Type::U64 | Type::F64 | Type::Bool | Type::String => {
|
||||
true
|
||||
}
|
||||
Type::Named(name) => structs.contains(name) || enums.contains(name),
|
||||
Type::Array(inner, len) => {
|
||||
*len > 0
|
||||
&& (matches!(
|
||||
&**inner,
|
||||
Type::I32
|
||||
| Type::I64
|
||||
| Type::U32
|
||||
| Type::U64
|
||||
| Type::F64
|
||||
| Type::Bool
|
||||
| Type::String
|
||||
) || matches!(&**inner, Type::Named(name) if structs.contains(name) || enums.contains(name)))
|
||||
}
|
||||
Type::Vec(inner) => matches!(
|
||||
&**inner,
|
||||
Type::I32 | Type::I64 | Type::F64 | Type::Bool | Type::String
|
||||
),
|
||||
Type::Option(inner) => option_payload_type_supported(inner),
|
||||
Type::Result(ok, err) => result_payload_types_supported(ok, err),
|
||||
Type::Unit | Type::Ptr(_) | Type::Slice(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn unsupported_type_alias_target(file: &str, span: Span, ty: &Type) -> Diagnostic {
|
||||
Diagnostic::new(
|
||||
file,
|
||||
"UnsupportedTypeAliasTarget",
|
||||
"type alias target type is not supported in the current beta",
|
||||
)
|
||||
.with_span(span)
|
||||
.expected("i32, i64, u32, u64, f64, bool, string, known struct, known enum, supported array, supported option, supported result, or supported vec")
|
||||
.found(ty.to_string())
|
||||
.hint("aliases are transparent and may only target concrete types already supported in the target use positions")
|
||||
}
|
||||
|
||||
fn type_alias_cycle(
|
||||
file: &str,
|
||||
cycle: &[String],
|
||||
aliases: &HashMap<String, &TypeAliasDecl>,
|
||||
) -> Diagnostic {
|
||||
let first = &cycle[0];
|
||||
let first_alias = aliases[first];
|
||||
let mut diag = Diagnostic::new(
|
||||
file,
|
||||
"TypeAliasCycle",
|
||||
format!("type alias cycle includes `{}`", first),
|
||||
)
|
||||
.with_span(first_alias.name_span)
|
||||
.hint("type aliases must resolve to an existing non-alias concrete type");
|
||||
|
||||
for name in &cycle[1..] {
|
||||
diag = diag.related(
|
||||
format!("cycle also includes `{}`", name),
|
||||
aliases[name].name_span,
|
||||
);
|
||||
}
|
||||
|
||||
diag
|
||||
}
|
||||
|
||||
fn rewrite_program_alias_types(program: &mut Program, aliases: &HashMap<String, Type>) {
|
||||
for enum_decl in &mut program.enums {
|
||||
for variant in &mut enum_decl.variants {
|
||||
if let Some(payload_ty) = &mut variant.payload_ty {
|
||||
*payload_ty = resolve_use_type(payload_ty, aliases);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for struct_decl in &mut program.structs {
|
||||
for field in &mut struct_decl.fields {
|
||||
field.ty = resolve_use_type(&field.ty, aliases);
|
||||
}
|
||||
}
|
||||
|
||||
for import in &mut program.c_imports {
|
||||
for param in &mut import.params {
|
||||
param.ty = resolve_use_type(¶m.ty, aliases);
|
||||
}
|
||||
import.return_type = resolve_use_type(&import.return_type, aliases);
|
||||
}
|
||||
|
||||
for function in &mut program.functions {
|
||||
for param in &mut function.params {
|
||||
param.ty = resolve_use_type(¶m.ty, aliases);
|
||||
}
|
||||
function.return_type = resolve_use_type(&function.return_type, aliases);
|
||||
for expr in &mut function.body {
|
||||
rewrite_expr_alias_types(expr, aliases);
|
||||
}
|
||||
}
|
||||
|
||||
for test in &mut program.tests {
|
||||
for expr in &mut test.body {
|
||||
rewrite_expr_alias_types(expr, aliases);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rewrite_expr_alias_types(expr: &mut Expr, aliases: &HashMap<String, Type>) {
|
||||
match &mut expr.kind {
|
||||
ExprKind::StructInit { fields, .. } => {
|
||||
for field in fields {
|
||||
rewrite_expr_alias_types(&mut field.expr, aliases);
|
||||
}
|
||||
}
|
||||
ExprKind::ArrayInit {
|
||||
elem_ty, elements, ..
|
||||
} => {
|
||||
*elem_ty = resolve_use_type(elem_ty, aliases);
|
||||
for element in elements {
|
||||
rewrite_expr_alias_types(element, aliases);
|
||||
}
|
||||
}
|
||||
ExprKind::OptionSome {
|
||||
payload_ty, value, ..
|
||||
} => {
|
||||
*payload_ty = resolve_use_type(payload_ty, aliases);
|
||||
rewrite_expr_alias_types(value, aliases);
|
||||
}
|
||||
ExprKind::OptionNone { payload_ty, .. } => {
|
||||
*payload_ty = resolve_use_type(payload_ty, aliases);
|
||||
}
|
||||
ExprKind::ResultOk {
|
||||
ok_ty,
|
||||
err_ty,
|
||||
value,
|
||||
..
|
||||
}
|
||||
| ExprKind::ResultErr {
|
||||
ok_ty,
|
||||
err_ty,
|
||||
value,
|
||||
..
|
||||
} => {
|
||||
*ok_ty = resolve_use_type(ok_ty, aliases);
|
||||
*err_ty = resolve_use_type(err_ty, aliases);
|
||||
rewrite_expr_alias_types(value, aliases);
|
||||
}
|
||||
ExprKind::OptionIsSome { value }
|
||||
| ExprKind::OptionIsNone { value }
|
||||
| ExprKind::OptionUnwrapSome { value }
|
||||
| ExprKind::ResultIsOk { value, .. }
|
||||
| ExprKind::ResultIsErr { value, .. }
|
||||
| ExprKind::ResultUnwrapOk { value, .. }
|
||||
| ExprKind::ResultUnwrapErr { value, .. } => rewrite_expr_alias_types(value, aliases),
|
||||
ExprKind::EnumVariant { args, .. } | ExprKind::Call { args, .. } => {
|
||||
for arg in args {
|
||||
rewrite_expr_alias_types(arg, aliases);
|
||||
}
|
||||
}
|
||||
ExprKind::FieldAccess { value, .. } => rewrite_expr_alias_types(value, aliases),
|
||||
ExprKind::Index { array, index } => {
|
||||
rewrite_expr_alias_types(array, aliases);
|
||||
rewrite_expr_alias_types(index, aliases);
|
||||
}
|
||||
ExprKind::Local { ty, expr, .. } => {
|
||||
*ty = resolve_use_type(ty, aliases);
|
||||
rewrite_expr_alias_types(expr, aliases);
|
||||
}
|
||||
ExprKind::Set { expr, .. } => rewrite_expr_alias_types(expr, aliases),
|
||||
ExprKind::Binary { left, right, .. } => {
|
||||
rewrite_expr_alias_types(left, aliases);
|
||||
rewrite_expr_alias_types(right, aliases);
|
||||
}
|
||||
ExprKind::If {
|
||||
condition,
|
||||
then_expr,
|
||||
else_expr,
|
||||
} => {
|
||||
rewrite_expr_alias_types(condition, aliases);
|
||||
rewrite_expr_alias_types(then_expr, aliases);
|
||||
rewrite_expr_alias_types(else_expr, aliases);
|
||||
}
|
||||
ExprKind::Match { subject, arms } => {
|
||||
rewrite_expr_alias_types(subject, aliases);
|
||||
for arm in arms {
|
||||
for expr in &mut arm.body {
|
||||
rewrite_expr_alias_types(expr, aliases);
|
||||
}
|
||||
}
|
||||
}
|
||||
ExprKind::While { condition, body } => {
|
||||
rewrite_expr_alias_types(condition, aliases);
|
||||
for expr in body {
|
||||
rewrite_expr_alias_types(expr, aliases);
|
||||
}
|
||||
}
|
||||
ExprKind::Unsafe { body } => {
|
||||
for expr in body {
|
||||
rewrite_expr_alias_types(expr, aliases);
|
||||
}
|
||||
}
|
||||
ExprKind::Int(_)
|
||||
| ExprKind::Int64(_)
|
||||
| ExprKind::UInt32(_)
|
||||
| ExprKind::UInt64(_)
|
||||
| ExprKind::Float(_)
|
||||
| ExprKind::Bool(_)
|
||||
| ExprKind::String(_)
|
||||
| ExprKind::Var(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_use_type(ty: &Type, aliases: &HashMap<String, Type>) -> Type {
|
||||
match ty {
|
||||
Type::Named(name) => aliases
|
||||
.get(name)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Type::Named(name.clone())),
|
||||
Type::Ptr(inner) => Type::Ptr(Box::new(resolve_use_type(inner, aliases))),
|
||||
Type::Array(inner, len) => Type::Array(Box::new(resolve_use_type(inner, aliases)), *len),
|
||||
Type::Vec(inner) => Type::Vec(Box::new(resolve_use_type(inner, aliases))),
|
||||
Type::Slice(inner) => Type::Slice(Box::new(resolve_use_type(inner, aliases))),
|
||||
Type::Option(inner) => Type::Option(Box::new(resolve_use_type(inner, aliases))),
|
||||
Type::Result(ok, err) => Type::Result(
|
||||
Box::new(resolve_use_type(ok, aliases)),
|
||||
Box::new(resolve_use_type(err, aliases)),
|
||||
),
|
||||
_ => ty.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_reserved_type_name(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"i32"
|
||||
| "i64"
|
||||
| "u32"
|
||||
| "u64"
|
||||
| "f64"
|
||||
| "bool"
|
||||
| "unit"
|
||||
| "string"
|
||||
| "ptr"
|
||||
| "slice"
|
||||
| "option"
|
||||
| "result"
|
||||
| "array"
|
||||
| "vec"
|
||||
)
|
||||
}
|
||||
|
||||
fn check_struct_decl(
|
||||
file: &str,
|
||||
struct_decl: &StructDecl,
|
||||
@ -2008,6 +2659,10 @@ fn check_local_type(
|
||||
}
|
||||
|
||||
fn unknown_named_type(file: &str, span: Span, name: &str, context: &str) -> Diagnostic {
|
||||
if is_generic_type_parameter_name(name) {
|
||||
return unsupported_generic_type_parameter(file, span, name);
|
||||
}
|
||||
|
||||
Diagnostic::new(
|
||||
file,
|
||||
"UnknownStructType",
|
||||
@ -3690,10 +4345,18 @@ fn validate_match_arm_set(
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !missing.is_empty() {
|
||||
let found = required
|
||||
.iter()
|
||||
.filter(|pattern| seen.contains_key(*pattern))
|
||||
.map(lower::match_pattern_name)
|
||||
.collect::<Vec<_>>();
|
||||
return Err(Diagnostic::new(
|
||||
file,
|
||||
"NonExhaustiveMatch",
|
||||
format!("match is missing `{}` arm(s)", missing.join("`, `")),
|
||||
format!(
|
||||
"match is missing required arm(s): `{}`",
|
||||
missing.join("`, `")
|
||||
),
|
||||
)
|
||||
.with_span(span)
|
||||
.expected(
|
||||
@ -3703,12 +4366,11 @@ fn validate_match_arm_set(
|
||||
.collect::<Vec<_>>()
|
||||
.join(" and "),
|
||||
)
|
||||
.found(
|
||||
seen.keys()
|
||||
.map(lower::match_pattern_name)
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
));
|
||||
.found(if found.is_empty() {
|
||||
"no valid arms".to_string()
|
||||
} else {
|
||||
found.join(", ")
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -4851,6 +5513,12 @@ fn check_vector_type(file: &str, ty: &Type, span: Span) -> Result<(), Diagnostic
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if let Type::Named(name) = &**inner {
|
||||
if is_generic_type_parameter_name(name) {
|
||||
return Err(unsupported_generic_type_parameter(file, span, name));
|
||||
}
|
||||
}
|
||||
|
||||
if **inner != Type::I32
|
||||
&& **inner != Type::I64
|
||||
&& **inner != Type::F64
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
use crate::token::Span;
|
||||
|
||||
pub const DIAGNOSTIC_SCHEMA_NAME: &str = "slovo.diagnostic";
|
||||
pub const DIAGNOSTIC_SCHEMA_VERSION: u32 = 1;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Severity {
|
||||
Error,
|
||||
@ -184,8 +187,8 @@ impl Diagnostic {
|
||||
) -> String {
|
||||
let source = source_for(&self.file).unwrap_or("");
|
||||
let mut parts = vec![
|
||||
" (schema slovo.diagnostic)".to_string(),
|
||||
" (version 1)".to_string(),
|
||||
format!(" (schema {})", DIAGNOSTIC_SCHEMA_NAME),
|
||||
format!(" (version {})", DIAGNOSTIC_SCHEMA_VERSION),
|
||||
format!(" (severity {})", self.severity.as_str()),
|
||||
format!(" (code {})", self.code),
|
||||
format!(" (message {})", render_string(&self.message)),
|
||||
@ -310,8 +313,8 @@ fn render_json_diagnostic<'a>(
|
||||
related: impl Iterator<Item = JsonRelated<'a>>,
|
||||
) -> String {
|
||||
let mut fields = vec![
|
||||
"\"schema\":\"slovo.diagnostic\"".to_string(),
|
||||
"\"version\":1".to_string(),
|
||||
format!("\"schema\":{}", render_json_string(DIAGNOSTIC_SCHEMA_NAME)),
|
||||
format!("\"version\":{}", DIAGNOSTIC_SCHEMA_VERSION),
|
||||
format!("\"severity\":{}", render_json_string(severity)),
|
||||
format!("\"code\":{}", render_json_string(code)),
|
||||
format!("\"message\":{}", render_json_string(message)),
|
||||
|
||||
@ -1,17 +1,22 @@
|
||||
use std::{fs, path::Path};
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
fs,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
ast::Program,
|
||||
diag::Diagnostic,
|
||||
lexer, lower,
|
||||
project::{self, SourceFile, ToolFailure},
|
||||
project::{self, ProjectArtifact, SourceFile, ToolFailure, WorkspaceArtifact},
|
||||
sexpr::{Atom, SExpr, SExprKind},
|
||||
types::Type,
|
||||
};
|
||||
|
||||
pub fn generate(input: &str, output_dir: &str) -> Result<(), ToolFailure> {
|
||||
let docs = if project::is_project_input(input) {
|
||||
let loaded = project::load_project_sources_for_tools(input)?;
|
||||
render_project(&loaded.artifact.project_name, &loaded.sources)?
|
||||
render_project(&loaded.artifact, &loaded.sources)?
|
||||
} else {
|
||||
let source = fs::read_to_string(input).map_err(|err| ToolFailure {
|
||||
diagnostics: vec![Diagnostic::new(
|
||||
@ -52,18 +57,60 @@ pub fn generate(input: &str, output_dir: &str) -> Result<(), ToolFailure> {
|
||||
})
|
||||
}
|
||||
|
||||
fn render_project(title: &str, sources: &[SourceFile]) -> Result<String, ToolFailure> {
|
||||
fn render_project(
|
||||
artifact: &ProjectArtifact,
|
||||
sources: &[SourceFile],
|
||||
) -> Result<String, ToolFailure> {
|
||||
let modules = sources
|
||||
.iter()
|
||||
.map(document_source)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let mut out = String::new();
|
||||
out.push_str("# Project ");
|
||||
out.push_str(title);
|
||||
out.push_str(&artifact.project_name);
|
||||
out.push_str("\n\n");
|
||||
for source in sources {
|
||||
let module = document_source(source)?;
|
||||
render_module(&mut out, &module);
|
||||
if let Some(workspace) = &artifact.workspace {
|
||||
render_workspace(&mut out, workspace);
|
||||
render_workspace_package_public_api(&mut out, workspace, &modules);
|
||||
} else {
|
||||
render_project_package_public_api(&mut out, artifact, &modules);
|
||||
}
|
||||
for module in &modules {
|
||||
render_module(&mut out, module);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn render_workspace(out: &mut String, workspace: &WorkspaceArtifact) {
|
||||
out.push_str("## Workspace\n\n");
|
||||
render_list(out, "Members", &workspace.members);
|
||||
|
||||
let packages = workspace
|
||||
.packages
|
||||
.iter()
|
||||
.map(|package| {
|
||||
format!(
|
||||
"{} {} (entry {})",
|
||||
package.name, package.version, package.entry
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
render_list(out, "Packages", &packages);
|
||||
|
||||
let dependencies = workspace
|
||||
.dependencies
|
||||
.iter()
|
||||
.map(|dependency| {
|
||||
format!(
|
||||
"{} -> {} ({}, {})",
|
||||
dependency.from, dependency.to, dependency.kind, dependency.path
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
render_list(out, "Package Dependencies", &dependencies);
|
||||
}
|
||||
|
||||
fn render_file(title: &str, sources: &[SourceFile]) -> Result<String, ToolFailure> {
|
||||
let mut out = String::new();
|
||||
out.push_str("# File ");
|
||||
@ -85,27 +132,58 @@ fn document_source(source: &SourceFile) -> Result<DocModule, ToolFailure> {
|
||||
sources: vec![source.clone()],
|
||||
artifact: None,
|
||||
})?;
|
||||
let program = lower::lower_program(&source.path, &forms).ok();
|
||||
let lowerable_forms = forms
|
||||
.iter()
|
||||
.filter(|form| !matches!(list_head(form), Some("import")))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let program = lower::lower_program(&source.path, &lowerable_forms).ok();
|
||||
Ok(module_from_forms(&source.path, &forms, program.as_ref()))
|
||||
}
|
||||
|
||||
struct DocModule {
|
||||
path: String,
|
||||
title: String,
|
||||
imports: Vec<String>,
|
||||
exports: Vec<String>,
|
||||
structs: Vec<String>,
|
||||
functions: Vec<String>,
|
||||
tests: Vec<String>,
|
||||
public_api: PublicApi,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct PublicApi {
|
||||
functions: Vec<DocFunction>,
|
||||
structs: Vec<DocStruct>,
|
||||
enums: Vec<DocEnum>,
|
||||
}
|
||||
|
||||
struct DocFunction {
|
||||
name: String,
|
||||
signature: String,
|
||||
}
|
||||
|
||||
struct DocStruct {
|
||||
name: String,
|
||||
fields: Vec<String>,
|
||||
}
|
||||
|
||||
struct DocEnum {
|
||||
name: String,
|
||||
variants: Vec<String>,
|
||||
}
|
||||
|
||||
fn module_from_forms(file: &str, forms: &[SExpr], program: Option<&Program>) -> DocModule {
|
||||
let mut module = DocModule {
|
||||
path: file.to_string(),
|
||||
title: file.to_string(),
|
||||
imports: Vec::new(),
|
||||
exports: Vec::new(),
|
||||
structs: Vec::new(),
|
||||
functions: Vec::new(),
|
||||
tests: Vec::new(),
|
||||
public_api: PublicApi::default(),
|
||||
};
|
||||
|
||||
for form in forms {
|
||||
@ -170,6 +248,7 @@ fn module_from_forms(file: &str, forms: &[SExpr], program: Option<&Program>) ->
|
||||
})
|
||||
.collect();
|
||||
module.tests = program.tests.iter().map(|test| test.name.clone()).collect();
|
||||
module.public_api = public_api_from_program(program, &module.exports);
|
||||
}
|
||||
|
||||
module.imports.sort();
|
||||
@ -186,11 +265,120 @@ fn render_module(out: &mut String, module: &DocModule) {
|
||||
out.push_str("\n\n");
|
||||
render_list(out, "Imports", &module.imports);
|
||||
render_list(out, "Exports", &module.exports);
|
||||
render_public_api_section(out, "###", &module.public_api);
|
||||
render_list(out, "Structs", &module.structs);
|
||||
render_list(out, "Functions", &module.functions);
|
||||
render_list(out, "Tests", &module.tests);
|
||||
}
|
||||
|
||||
fn render_project_package_public_api(
|
||||
out: &mut String,
|
||||
artifact: &ProjectArtifact,
|
||||
modules: &[DocModule],
|
||||
) {
|
||||
let local_root = Path::new(&artifact.project_root).join(&artifact.source_root);
|
||||
let local_modules = modules
|
||||
.iter()
|
||||
.filter(|module| Path::new(&module.path).starts_with(&local_root))
|
||||
.collect::<Vec<_>>();
|
||||
render_package_public_api(out, &artifact.project_name, &local_modules);
|
||||
}
|
||||
|
||||
fn render_workspace_package_public_api(
|
||||
out: &mut String,
|
||||
workspace: &WorkspaceArtifact,
|
||||
modules: &[DocModule],
|
||||
) {
|
||||
let by_path = modules
|
||||
.iter()
|
||||
.map(|module| (module.path.as_str(), module))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
for package in &workspace.packages {
|
||||
let local_root = Path::new(&package.root).join(&package.source_root);
|
||||
let package_modules = package
|
||||
.modules
|
||||
.iter()
|
||||
.filter(|module| Path::new(&module.path).starts_with(&local_root))
|
||||
.filter_map(|module| by_path.get(module.path.as_str()).copied())
|
||||
.collect::<Vec<_>>();
|
||||
render_package_public_api(
|
||||
out,
|
||||
&format!("{} {}", package.name, package.version),
|
||||
&package_modules,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_package_public_api(out: &mut String, package_name: &str, modules: &[&DocModule]) {
|
||||
out.push_str("## Package API ");
|
||||
out.push_str(package_name);
|
||||
out.push_str("\n\n");
|
||||
|
||||
let public_modules = modules
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|module| !module.public_api.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
if public_modules.is_empty() {
|
||||
out.push_str("None.\n\n");
|
||||
return;
|
||||
}
|
||||
|
||||
for module in public_modules {
|
||||
out.push_str("### Module ");
|
||||
out.push_str(&module.title);
|
||||
out.push_str("\n\n");
|
||||
render_public_api_body(out, "####", &module.public_api);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_public_api_section(out: &mut String, heading: &str, public_api: &PublicApi) {
|
||||
out.push_str(heading);
|
||||
out.push_str(" Public API\n\n");
|
||||
render_public_api_body(out, "####", public_api);
|
||||
}
|
||||
|
||||
fn render_public_api_body(out: &mut String, heading: &str, public_api: &PublicApi) {
|
||||
if public_api.is_empty() {
|
||||
out.push_str("None.\n\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if !public_api.functions.is_empty() {
|
||||
out.push_str(heading);
|
||||
out.push_str(" Functions\n");
|
||||
for function in &public_api.functions {
|
||||
render_code_bullet(out, &function.signature);
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
|
||||
if !public_api.structs.is_empty() {
|
||||
out.push_str(heading);
|
||||
out.push_str(" Structs\n");
|
||||
for struct_decl in &public_api.structs {
|
||||
render_code_bullet(out, &format!("struct {}", struct_decl.name));
|
||||
for field in &struct_decl.fields {
|
||||
render_indented_code_bullet(out, field);
|
||||
}
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
|
||||
if !public_api.enums.is_empty() {
|
||||
out.push_str(heading);
|
||||
out.push_str(" Enums\n");
|
||||
for enum_decl in &public_api.enums {
|
||||
render_code_bullet(out, &format!("enum {}", enum_decl.name));
|
||||
for variant in &enum_decl.variants {
|
||||
render_indented_code_bullet(out, variant);
|
||||
}
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
fn render_list(out: &mut String, title: &str, values: &[String]) {
|
||||
out.push_str("### ");
|
||||
out.push_str(title);
|
||||
@ -207,6 +395,182 @@ fn render_list(out: &mut String, title: &str, values: &[String]) {
|
||||
out.push('\n');
|
||||
}
|
||||
|
||||
fn list_head(expr: &SExpr) -> Option<&str> {
|
||||
list(expr).and_then(|items| items.first()).and_then(ident)
|
||||
}
|
||||
|
||||
fn render_code_bullet(out: &mut String, value: &str) {
|
||||
out.push_str("- `");
|
||||
out.push_str(&value.replace('`', "\\`"));
|
||||
out.push_str("`\n");
|
||||
}
|
||||
|
||||
fn render_indented_code_bullet(out: &mut String, value: &str) {
|
||||
out.push_str(" - `");
|
||||
out.push_str(&value.replace('`', "\\`"));
|
||||
out.push_str("`\n");
|
||||
}
|
||||
|
||||
impl PublicApi {
|
||||
fn is_empty(&self) -> bool {
|
||||
self.functions.is_empty() && self.structs.is_empty() && self.enums.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
fn public_api_from_program(program: &Program, exports: &[String]) -> PublicApi {
|
||||
let export_names = exports.iter().cloned().collect::<BTreeSet<_>>();
|
||||
let aliases = alias_targets(program);
|
||||
|
||||
let mut functions = program
|
||||
.functions
|
||||
.iter()
|
||||
.filter(|function| export_names.contains(&function.name))
|
||||
.map(|function| DocFunction {
|
||||
name: function.name.clone(),
|
||||
signature: function_signature(
|
||||
&function.name,
|
||||
function
|
||||
.params
|
||||
.iter()
|
||||
.map(|param| (param.name.as_str(), ¶m.ty)),
|
||||
&function.return_type,
|
||||
&aliases,
|
||||
),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
functions.sort_by(|left, right| left.name.cmp(&right.name));
|
||||
|
||||
let mut structs = program
|
||||
.structs
|
||||
.iter()
|
||||
.filter(|struct_decl| export_names.contains(&struct_decl.name))
|
||||
.map(|struct_decl| DocStruct {
|
||||
name: struct_decl.name.clone(),
|
||||
fields: struct_decl
|
||||
.fields
|
||||
.iter()
|
||||
.map(|field| {
|
||||
format!(
|
||||
"{}: {}",
|
||||
field.name,
|
||||
display_public_type(&field.ty, &aliases)
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
structs.sort_by(|left, right| left.name.cmp(&right.name));
|
||||
|
||||
let mut enums = program
|
||||
.enums
|
||||
.iter()
|
||||
.filter(|enum_decl| export_names.contains(&enum_decl.name))
|
||||
.map(|enum_decl| DocEnum {
|
||||
name: enum_decl.name.clone(),
|
||||
variants: enum_decl
|
||||
.variants
|
||||
.iter()
|
||||
.map(|variant| match &variant.payload_ty {
|
||||
Some(payload_ty) => {
|
||||
format!(
|
||||
"{}({})",
|
||||
variant.name,
|
||||
display_public_type(payload_ty, &aliases)
|
||||
)
|
||||
}
|
||||
None => variant.name.clone(),
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
enums.sort_by(|left, right| left.name.cmp(&right.name));
|
||||
|
||||
PublicApi {
|
||||
functions,
|
||||
structs,
|
||||
enums,
|
||||
}
|
||||
}
|
||||
|
||||
fn function_signature<'a>(
|
||||
name: &str,
|
||||
params: impl Iterator<Item = (&'a str, &'a Type)>,
|
||||
return_type: &Type,
|
||||
aliases: &BTreeMap<String, Type>,
|
||||
) -> String {
|
||||
let params = params
|
||||
.map(|(name, ty)| format!("{}: {}", name, display_public_type(ty, aliases)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
format!(
|
||||
"fn {}({}) -> {}",
|
||||
name,
|
||||
params,
|
||||
display_public_type(return_type, aliases)
|
||||
)
|
||||
}
|
||||
|
||||
fn alias_targets(program: &Program) -> BTreeMap<String, Type> {
|
||||
let raw = program
|
||||
.type_aliases
|
||||
.iter()
|
||||
.map(|alias| (alias.name.clone(), alias.target.clone()))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
raw.keys()
|
||||
.map(|name| {
|
||||
let mut visiting = BTreeSet::new();
|
||||
(
|
||||
name.clone(),
|
||||
resolve_alias_type(&Type::Named(name.clone()), &raw, &mut visiting),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn display_public_type(ty: &Type, aliases: &BTreeMap<String, Type>) -> String {
|
||||
let mut visiting = BTreeSet::new();
|
||||
resolve_alias_type(ty, aliases, &mut visiting).to_string()
|
||||
}
|
||||
|
||||
fn resolve_alias_type(
|
||||
ty: &Type,
|
||||
aliases: &BTreeMap<String, Type>,
|
||||
visiting: &mut BTreeSet<String>,
|
||||
) -> Type {
|
||||
match ty {
|
||||
Type::Named(name) => {
|
||||
let Some(target) = aliases.get(name) else {
|
||||
return ty.clone();
|
||||
};
|
||||
if !visiting.insert(name.clone()) {
|
||||
return ty.clone();
|
||||
}
|
||||
let resolved = resolve_alias_type(target, aliases, visiting);
|
||||
visiting.remove(name);
|
||||
resolved
|
||||
}
|
||||
Type::Ptr(inner) => Type::Ptr(Box::new(resolve_alias_type(inner, aliases, visiting))),
|
||||
Type::Array(inner, len) => {
|
||||
Type::Array(Box::new(resolve_alias_type(inner, aliases, visiting)), *len)
|
||||
}
|
||||
Type::Vec(inner) => Type::Vec(Box::new(resolve_alias_type(inner, aliases, visiting))),
|
||||
Type::Slice(inner) => Type::Slice(Box::new(resolve_alias_type(inner, aliases, visiting))),
|
||||
Type::Option(inner) => Type::Option(Box::new(resolve_alias_type(inner, aliases, visiting))),
|
||||
Type::Result(ok, err) => Type::Result(
|
||||
Box::new(resolve_alias_type(ok, aliases, visiting)),
|
||||
Box::new(resolve_alias_type(err, aliases, visiting)),
|
||||
),
|
||||
Type::I32
|
||||
| Type::I64
|
||||
| Type::U32
|
||||
| Type::U64
|
||||
| Type::F64
|
||||
| Type::Bool
|
||||
| Type::Unit
|
||||
| Type::String => ty.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn list(expr: &SExpr) -> Option<&[SExpr]> {
|
||||
match &expr.kind {
|
||||
SExprKind::List(items) => Some(items),
|
||||
|
||||
@ -52,6 +52,16 @@ pub fn run_tests(
|
||||
test_runner::run(file, &checked, filter)
|
||||
}
|
||||
|
||||
pub fn list_tests(
|
||||
file: &str,
|
||||
source: &str,
|
||||
filter: Option<&str>,
|
||||
) -> Result<test_runner::TestRunSuccess, test_runner::TestRunFailure> {
|
||||
let checked =
|
||||
checked_program(file, source).map_err(test_runner::TestRunFailure::before_execution)?;
|
||||
Ok(test_runner::list(&checked, filter))
|
||||
}
|
||||
|
||||
fn checked_program(file: &str, source: &str) -> Result<check::CheckedProgram, Vec<Diagnostic>> {
|
||||
let tokens = lexer::lex(file, source)?;
|
||||
let forms = sexpr::parse(file, &tokens)?;
|
||||
|
||||
@ -2,6 +2,11 @@ use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::{
|
||||
diag::Diagnostic,
|
||||
reserved::{
|
||||
is_unsupported_generic_standard_library_call, unsupported_generic_function,
|
||||
unsupported_generic_standard_library_call, unsupported_generic_type_alias,
|
||||
unsupported_reserved_type_diagnostic,
|
||||
},
|
||||
sexpr::{Atom, SExpr, SExprKind},
|
||||
std_runtime,
|
||||
token::Span,
|
||||
@ -19,6 +24,7 @@ pub fn format(file: &str, source: &str, forms: &[SExpr]) -> Result<String, Vec<D
|
||||
function_names: collect_function_names(forms),
|
||||
struct_names: collect_struct_names(forms),
|
||||
enum_names: collect_enum_names(forms),
|
||||
type_alias_names: collect_type_alias_names(forms),
|
||||
errors: comment_errors
|
||||
.into_iter()
|
||||
.map(|comment| unsupported_non_full_line_comment(file, comment.span))
|
||||
@ -42,6 +48,7 @@ struct Formatter<'a> {
|
||||
function_names: HashSet<String>,
|
||||
struct_names: HashSet<String>,
|
||||
enum_names: HashSet<String>,
|
||||
type_alias_names: HashSet<String>,
|
||||
errors: Vec<Diagnostic>,
|
||||
}
|
||||
|
||||
@ -64,6 +71,7 @@ impl Formatter<'_> {
|
||||
Some("module") => self.write_module(form),
|
||||
Some("import") => self.write_import(form),
|
||||
Some("import_c") => self.write_c_import(form),
|
||||
Some("type") => self.write_type_alias(form),
|
||||
Some("enum") => self.write_enum(form),
|
||||
Some("struct") => self.write_struct(form),
|
||||
Some("fn") => self.write_function(form),
|
||||
@ -251,6 +259,67 @@ impl Formatter<'_> {
|
||||
);
|
||||
}
|
||||
|
||||
fn write_type_alias(&mut self, form: &SExpr) {
|
||||
let Some(items) = expect_list(form) else {
|
||||
self.errors.push(
|
||||
Diagnostic::new(
|
||||
self.file,
|
||||
"MalformedTypeAlias",
|
||||
"type alias form must be a list",
|
||||
)
|
||||
.with_span(form.span),
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
if matches!(items.get(2).and_then(list_head), Some("type_params")) {
|
||||
self.errors.push(unsupported_generic_type_alias(
|
||||
self.file,
|
||||
items.get(2).map_or(form.span, |item| item.span),
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
if items.len() != 3 {
|
||||
self.errors.push(
|
||||
Diagnostic::new(
|
||||
self.file,
|
||||
"MalformedTypeAlias",
|
||||
"type alias form must be `(type Alias TargetType)`",
|
||||
)
|
||||
.with_span(form.span),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(name) = expect_ident(&items[1]) else {
|
||||
self.errors.push(
|
||||
Diagnostic::new(
|
||||
self.file,
|
||||
"InvalidTypeAliasName",
|
||||
"type alias name must be an identifier",
|
||||
)
|
||||
.with_span(items[1].span),
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(target) = self.render_alias_target_type(&items[2]) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.reject_comments_before(
|
||||
form.span.end,
|
||||
"formatter does not support comments inside type alias forms",
|
||||
);
|
||||
|
||||
self.output.push_str("(type ");
|
||||
self.output.push_str(name);
|
||||
self.output.push(' ');
|
||||
self.output.push_str(&target);
|
||||
self.output.push(')');
|
||||
}
|
||||
|
||||
fn write_struct(&mut self, form: &SExpr) {
|
||||
let Some(items) = expect_list(form) else {
|
||||
self.errors.push(
|
||||
@ -512,17 +581,17 @@ impl Formatter<'_> {
|
||||
} else if is_ident(&variant_items[1], "string") {
|
||||
"string".to_string()
|
||||
} else if let Some(name) = expect_ident(&variant_items[1]) {
|
||||
if self.struct_names.contains(name) {
|
||||
if self.struct_names.contains(name) || self.type_alias_names.contains(name) {
|
||||
name.to_string()
|
||||
} else {
|
||||
self.errors.push(
|
||||
Diagnostic::new(
|
||||
self.file,
|
||||
"UnsupportedFormatterForm",
|
||||
"formatter supports only unary direct i32, i64, f64, bool, string, and known non-recursive struct enum payload variants",
|
||||
"formatter supports only unary direct i32, i64, f64, bool, string, known type aliases, and known non-recursive struct enum payload variants",
|
||||
)
|
||||
.with_span(variant_items[1].span)
|
||||
.expected("i32, i64, f64, bool, string, or known non-recursive struct type"),
|
||||
.expected("i32, i64, f64, bool, string, known type alias, or known non-recursive struct type"),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
@ -531,10 +600,10 @@ impl Formatter<'_> {
|
||||
Diagnostic::new(
|
||||
self.file,
|
||||
"UnsupportedFormatterForm",
|
||||
"formatter supports only unary direct i32, i64, f64, bool, string, and known non-recursive struct enum payload variants",
|
||||
"formatter supports only unary direct i32, i64, f64, bool, string, known type aliases, and known non-recursive struct enum payload variants",
|
||||
)
|
||||
.with_span(variant_items[1].span)
|
||||
.expected("i32, i64, f64, bool, string, or known non-recursive struct type"),
|
||||
.expected("i32, i64, f64, bool, string, known type alias, or known non-recursive struct type"),
|
||||
);
|
||||
continue;
|
||||
};
|
||||
@ -601,6 +670,12 @@ impl Formatter<'_> {
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(items.get(2).and_then(list_head), Some("type_params")) {
|
||||
self.errors
|
||||
.push(unsupported_generic_function(self.file, items[2].span));
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(name) = expect_ident(&items[1]) else {
|
||||
self.errors.push(
|
||||
Diagnostic::new(
|
||||
@ -847,8 +922,13 @@ impl Formatter<'_> {
|
||||
} else if is_ident(&pair[1], "string") {
|
||||
"string".to_string()
|
||||
} else if let Some(name) = expect_ident(&pair[1]) {
|
||||
if self.struct_names.contains(name) || self.enum_names.contains(name) {
|
||||
if self.struct_names.contains(name)
|
||||
|| self.enum_names.contains(name)
|
||||
|| self.type_alias_names.contains(name)
|
||||
{
|
||||
name.to_string()
|
||||
} else if self.push_reserved_type_error(&pair[1]) {
|
||||
continue;
|
||||
} else {
|
||||
self.errors.push(
|
||||
Diagnostic::new(
|
||||
@ -861,14 +941,20 @@ impl Formatter<'_> {
|
||||
continue;
|
||||
}
|
||||
} else if let Some(items) = expect_list(&pair[1]) {
|
||||
if let Some(text) = render_option_result_type(items) {
|
||||
if let Some(text) = render_option_result_type(items, &self.type_alias_names) {
|
||||
text
|
||||
} else if let Some(text) =
|
||||
render_supported_array_type(items, &self.struct_names, &self.enum_names)
|
||||
} else if let Some(text) = render_supported_array_type(
|
||||
items,
|
||||
&self.struct_names,
|
||||
&self.enum_names,
|
||||
&self.type_alias_names,
|
||||
) {
|
||||
text
|
||||
} else if let Some(text) = render_supported_vec_type(items, &self.type_alias_names)
|
||||
{
|
||||
text
|
||||
} else if let Some(text) = render_supported_vec_type(items) {
|
||||
text
|
||||
} else if self.push_reserved_type_error(&pair[1]) {
|
||||
continue;
|
||||
} else {
|
||||
self.errors.push(
|
||||
Diagnostic::new(
|
||||
@ -946,11 +1032,18 @@ impl Formatter<'_> {
|
||||
}
|
||||
|
||||
if let Some(name) = expect_ident(form) {
|
||||
if self.struct_names.contains(name) || self.enum_names.contains(name) {
|
||||
if self.struct_names.contains(name)
|
||||
|| self.enum_names.contains(name)
|
||||
|| self.type_alias_names.contains(name)
|
||||
{
|
||||
return Some(name.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if self.push_reserved_type_error(form) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some(items) = expect_list(form) else {
|
||||
self.errors.push(
|
||||
Diagnostic::new(
|
||||
@ -963,19 +1056,27 @@ impl Formatter<'_> {
|
||||
return None;
|
||||
};
|
||||
|
||||
if let Some(text) = render_option_result_type(items) {
|
||||
if let Some(text) = render_option_result_type(items, &self.type_alias_names) {
|
||||
return Some(text);
|
||||
}
|
||||
|
||||
if let Some(text) = render_supported_array_type(items, &self.struct_names, &self.enum_names)
|
||||
{
|
||||
if let Some(text) = render_supported_array_type(
|
||||
items,
|
||||
&self.struct_names,
|
||||
&self.enum_names,
|
||||
&self.type_alias_names,
|
||||
) {
|
||||
return Some(text);
|
||||
}
|
||||
|
||||
if let Some(text) = render_supported_vec_type(items) {
|
||||
if let Some(text) = render_supported_vec_type(items, &self.type_alias_names) {
|
||||
return Some(text);
|
||||
}
|
||||
|
||||
if self.push_reserved_type_error(form) {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.errors.push(
|
||||
Diagnostic::new(
|
||||
self.file,
|
||||
@ -987,6 +1088,66 @@ impl Formatter<'_> {
|
||||
None
|
||||
}
|
||||
|
||||
fn render_alias_target_type(&mut self, form: &SExpr) -> Option<String> {
|
||||
if let Some(text) = render_direct_type_atom(
|
||||
form,
|
||||
&self.struct_names,
|
||||
&self.enum_names,
|
||||
&self.type_alias_names,
|
||||
) {
|
||||
return Some(text);
|
||||
}
|
||||
|
||||
if self.push_reserved_type_error(form) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some(items) = expect_list(form) else {
|
||||
self.errors.push(
|
||||
Diagnostic::new(
|
||||
self.file,
|
||||
"InvalidTypeAliasTarget",
|
||||
"type alias target type is invalid",
|
||||
)
|
||||
.with_span(form.span)
|
||||
.expected("concrete type"),
|
||||
);
|
||||
return None;
|
||||
};
|
||||
|
||||
if let Some(text) = render_option_result_type(items, &self.type_alias_names) {
|
||||
return Some(text);
|
||||
}
|
||||
|
||||
if let Some(text) = render_supported_array_type(
|
||||
items,
|
||||
&self.struct_names,
|
||||
&self.enum_names,
|
||||
&self.type_alias_names,
|
||||
) {
|
||||
return Some(text);
|
||||
}
|
||||
|
||||
if let Some(text) = render_supported_vec_type(items, &self.type_alias_names) {
|
||||
return Some(text);
|
||||
}
|
||||
|
||||
if self.push_reserved_type_error(form) {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.errors.push(
|
||||
Diagnostic::new(
|
||||
self.file,
|
||||
"InvalidTypeAliasTarget",
|
||||
"type alias target type is invalid",
|
||||
)
|
||||
.with_span(form.span)
|
||||
.expected("concrete type"),
|
||||
);
|
||||
None
|
||||
}
|
||||
|
||||
fn render_c_import_return_type(&mut self, form: &SExpr) -> Option<String> {
|
||||
if is_ident(form, "i32") {
|
||||
return Some("i32".to_string());
|
||||
@ -1159,6 +1320,14 @@ impl Formatter<'_> {
|
||||
Some(format!("({})", name))
|
||||
}
|
||||
}
|
||||
other if is_unsupported_generic_standard_library_call(other) => {
|
||||
self.errors.push(unsupported_generic_standard_library_call(
|
||||
self.file,
|
||||
items[0].span,
|
||||
other,
|
||||
));
|
||||
None
|
||||
}
|
||||
other if std_runtime::is_standard_path(other) => {
|
||||
self.errors
|
||||
.push(std_runtime::unsupported_standard_library_call(
|
||||
@ -1318,6 +1487,16 @@ impl Formatter<'_> {
|
||||
is_array: false,
|
||||
});
|
||||
}
|
||||
if self.type_alias_names.contains(name) {
|
||||
return Some(RenderedType {
|
||||
text: name.to_string(),
|
||||
is_array: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if self.push_reserved_type_error(form) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some(items) = expect_list(form) else {
|
||||
@ -1332,20 +1511,24 @@ impl Formatter<'_> {
|
||||
return None;
|
||||
};
|
||||
|
||||
if let Some(text) = render_option_result_type(items) {
|
||||
if let Some(text) = render_option_result_type(items, &self.type_alias_names) {
|
||||
return Some(RenderedType {
|
||||
text,
|
||||
is_array: false,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(text) = render_supported_vec_type(items) {
|
||||
if let Some(text) = render_supported_vec_type(items, &self.type_alias_names) {
|
||||
return Some(RenderedType {
|
||||
text,
|
||||
is_array: false,
|
||||
});
|
||||
}
|
||||
|
||||
if self.push_reserved_type_error(form) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if items.len() != 3 || !is_ident(&items[0], "array") {
|
||||
self.errors.push(
|
||||
Diagnostic::new(
|
||||
@ -1362,7 +1545,11 @@ impl Formatter<'_> {
|
||||
&items[1],
|
||||
&self.struct_names,
|
||||
&self.enum_names,
|
||||
&self.type_alias_names,
|
||||
) else {
|
||||
if self.push_reserved_type_error(&items[1]) {
|
||||
return None;
|
||||
}
|
||||
self.errors.push(
|
||||
Diagnostic::new(
|
||||
self.file,
|
||||
@ -1422,11 +1609,18 @@ impl Formatter<'_> {
|
||||
}
|
||||
|
||||
if let Some(name) = expect_ident(form) {
|
||||
if self.struct_names.contains(name) || self.enum_names.contains(name) {
|
||||
if self.struct_names.contains(name)
|
||||
|| self.enum_names.contains(name)
|
||||
|| self.type_alias_names.contains(name)
|
||||
{
|
||||
return Some(name.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if self.push_reserved_type_error(form) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some(items) = expect_list(form) else {
|
||||
self.errors.push(
|
||||
Diagnostic::new(
|
||||
@ -1439,19 +1633,27 @@ impl Formatter<'_> {
|
||||
return None;
|
||||
};
|
||||
|
||||
if let Some(text) = render_option_result_type(items) {
|
||||
if let Some(text) = render_option_result_type(items, &self.type_alias_names) {
|
||||
return Some(text);
|
||||
}
|
||||
|
||||
if let Some(text) = render_supported_array_type(items, &self.struct_names, &self.enum_names)
|
||||
{
|
||||
if let Some(text) = render_supported_array_type(
|
||||
items,
|
||||
&self.struct_names,
|
||||
&self.enum_names,
|
||||
&self.type_alias_names,
|
||||
) {
|
||||
return Some(text);
|
||||
}
|
||||
|
||||
if let Some(text) = render_supported_vec_type(items) {
|
||||
if let Some(text) = render_supported_vec_type(items, &self.type_alias_names) {
|
||||
return Some(text);
|
||||
}
|
||||
|
||||
if self.push_reserved_type_error(form) {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.errors.push(
|
||||
Diagnostic::new(
|
||||
self.file,
|
||||
@ -2051,6 +2253,7 @@ impl Formatter<'_> {
|
||||
&items[1],
|
||||
&self.struct_names,
|
||||
&self.enum_names,
|
||||
&self.type_alias_names,
|
||||
) else {
|
||||
self.errors.push(
|
||||
Diagnostic::new(
|
||||
@ -2113,21 +2316,10 @@ impl Formatter<'_> {
|
||||
return None;
|
||||
}
|
||||
|
||||
let payload_type = if is_ident(&items[1], "i32") {
|
||||
"i32"
|
||||
} else if is_ident(&items[1], "i64") {
|
||||
"i64"
|
||||
} else if is_ident(&items[1], "u32") {
|
||||
"u32"
|
||||
} else if is_ident(&items[1], "u64") {
|
||||
"u64"
|
||||
} else if is_ident(&items[1], "f64") {
|
||||
"f64"
|
||||
} else if is_ident(&items[1], "bool") {
|
||||
"bool"
|
||||
} else if is_ident(&items[1], "string") {
|
||||
"string"
|
||||
} else {
|
||||
let Some(payload_type) = render_payload_type_atom(&items[1], &self.type_alias_names) else {
|
||||
if self.push_reserved_type_error(&items[1]) {
|
||||
return None;
|
||||
}
|
||||
self.errors.push(
|
||||
Diagnostic::new(
|
||||
self.file,
|
||||
@ -2169,21 +2361,24 @@ impl Formatter<'_> {
|
||||
return None;
|
||||
}
|
||||
|
||||
let result_type = if is_ident(&items[1], "i32") && is_ident(&items[2], "i32") {
|
||||
"i32 i32"
|
||||
} else if is_ident(&items[1], "i64") && is_ident(&items[2], "i32") {
|
||||
"i64 i32"
|
||||
} else if is_ident(&items[1], "u32") && is_ident(&items[2], "i32") {
|
||||
"u32 i32"
|
||||
} else if is_ident(&items[1], "u64") && is_ident(&items[2], "i32") {
|
||||
"u64 i32"
|
||||
} else if is_ident(&items[1], "f64") && is_ident(&items[2], "i32") {
|
||||
"f64 i32"
|
||||
} else if is_ident(&items[1], "bool") && is_ident(&items[2], "i32") {
|
||||
"bool i32"
|
||||
} else if is_ident(&items[1], "string") && is_ident(&items[2], "i32") {
|
||||
"string i32"
|
||||
} else {
|
||||
let Some(ok_ty) = render_payload_type_atom(&items[1], &self.type_alias_names) else {
|
||||
if self.push_reserved_type_error(&items[1]) {
|
||||
return None;
|
||||
}
|
||||
self.errors.push(
|
||||
Diagnostic::new(
|
||||
self.file,
|
||||
"UnsupportedFormatterForm",
|
||||
"formatter supports only `(result i32 i32)`, `(result i64 i32)`, `(result u32 i32)`, `(result u64 i32)`, `(result f64 i32)`, `(result bool i32)`, and `(result string i32)` constructors",
|
||||
)
|
||||
.with_span(span),
|
||||
);
|
||||
return None;
|
||||
};
|
||||
let Some(err_ty) = render_result_err_type_atom(&items[2], &self.type_alias_names) else {
|
||||
if self.push_reserved_type_error(&items[2]) {
|
||||
return None;
|
||||
}
|
||||
self.errors.push(
|
||||
Diagnostic::new(
|
||||
self.file,
|
||||
@ -2196,7 +2391,7 @@ impl Formatter<'_> {
|
||||
};
|
||||
|
||||
let value = self.render_expr(&items[3], env)?;
|
||||
Some(format!("({} {} {})", name, result_type, value))
|
||||
Some(format!("({} {} {} {})", name, ok_ty, err_ty, value))
|
||||
}
|
||||
|
||||
fn render_call(
|
||||
@ -2234,6 +2429,15 @@ impl Formatter<'_> {
|
||||
Some(output)
|
||||
}
|
||||
|
||||
fn push_reserved_type_error(&mut self, form: &SExpr) -> bool {
|
||||
if let Some(diagnostic) = unsupported_reserved_type_diagnostic(self.file, form) {
|
||||
self.errors.push(diagnostic);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn write_indented_rendered(&mut self, indent: &str, rendered: &str) {
|
||||
push_indented(&mut self.output, indent, rendered);
|
||||
}
|
||||
@ -2355,6 +2559,19 @@ fn collect_enum_names(forms: &[SExpr]) -> HashSet<String> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn collect_type_alias_names(forms: &[SExpr]) -> HashSet<String> {
|
||||
forms
|
||||
.iter()
|
||||
.filter_map(|form| {
|
||||
let items = expect_list(form)?;
|
||||
if !is_ident(items.first()?, "type") {
|
||||
return None;
|
||||
}
|
||||
expect_ident(items.get(1)?).map(str::to_string)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn qualified_enum_name(name: &str) -> Option<(&str, &str)> {
|
||||
let (enum_name, variant) = name.split_once('.')?;
|
||||
if enum_name.is_empty() || variant.is_empty() || variant.contains('.') {
|
||||
@ -2467,89 +2684,19 @@ fn list_head(form: &SExpr) -> Option<&str> {
|
||||
expect_ident(first)
|
||||
}
|
||||
|
||||
fn render_option_result_type(items: &[SExpr]) -> Option<String> {
|
||||
if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "i32") {
|
||||
return Some("(option i32)".to_string());
|
||||
fn render_option_result_type(
|
||||
items: &[SExpr],
|
||||
type_alias_names: &HashSet<String>,
|
||||
) -> Option<String> {
|
||||
if items.len() == 2 && is_ident(&items[0], "option") {
|
||||
let payload = render_payload_type_atom(&items[1], type_alias_names)?;
|
||||
return Some(format!("(option {})", payload));
|
||||
}
|
||||
|
||||
if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "i64") {
|
||||
return Some("(option i64)".to_string());
|
||||
}
|
||||
|
||||
if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "u32") {
|
||||
return Some("(option u32)".to_string());
|
||||
}
|
||||
|
||||
if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "u64") {
|
||||
return Some("(option u64)".to_string());
|
||||
}
|
||||
|
||||
if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "f64") {
|
||||
return Some("(option f64)".to_string());
|
||||
}
|
||||
|
||||
if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "bool") {
|
||||
return Some("(option bool)".to_string());
|
||||
}
|
||||
|
||||
if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "string") {
|
||||
return Some("(option string)".to_string());
|
||||
}
|
||||
|
||||
if items.len() == 3
|
||||
&& is_ident(&items[0], "result")
|
||||
&& is_ident(&items[1], "i32")
|
||||
&& is_ident(&items[2], "i32")
|
||||
{
|
||||
return Some("(result i32 i32)".to_string());
|
||||
}
|
||||
|
||||
if items.len() == 3
|
||||
&& is_ident(&items[0], "result")
|
||||
&& is_ident(&items[1], "i64")
|
||||
&& is_ident(&items[2], "i32")
|
||||
{
|
||||
return Some("(result i64 i32)".to_string());
|
||||
}
|
||||
|
||||
if items.len() == 3
|
||||
&& is_ident(&items[0], "result")
|
||||
&& is_ident(&items[1], "u32")
|
||||
&& is_ident(&items[2], "i32")
|
||||
{
|
||||
return Some("(result u32 i32)".to_string());
|
||||
}
|
||||
|
||||
if items.len() == 3
|
||||
&& is_ident(&items[0], "result")
|
||||
&& is_ident(&items[1], "u64")
|
||||
&& is_ident(&items[2], "i32")
|
||||
{
|
||||
return Some("(result u64 i32)".to_string());
|
||||
}
|
||||
|
||||
if items.len() == 3
|
||||
&& is_ident(&items[0], "result")
|
||||
&& is_ident(&items[1], "f64")
|
||||
&& is_ident(&items[2], "i32")
|
||||
{
|
||||
return Some("(result f64 i32)".to_string());
|
||||
}
|
||||
|
||||
if items.len() == 3
|
||||
&& is_ident(&items[0], "result")
|
||||
&& is_ident(&items[1], "bool")
|
||||
&& is_ident(&items[2], "i32")
|
||||
{
|
||||
return Some("(result bool i32)".to_string());
|
||||
}
|
||||
|
||||
if items.len() == 3
|
||||
&& is_ident(&items[0], "result")
|
||||
&& is_ident(&items[1], "string")
|
||||
&& is_ident(&items[2], "i32")
|
||||
{
|
||||
return Some("(result string i32)".to_string());
|
||||
if items.len() == 3 && is_ident(&items[0], "result") {
|
||||
let ok = render_payload_type_atom(&items[1], type_alias_names)?;
|
||||
let err = render_result_err_type_atom(&items[2], type_alias_names)?;
|
||||
return Some(format!("(result {} {})", ok, err));
|
||||
}
|
||||
|
||||
None
|
||||
@ -2559,42 +2706,27 @@ fn render_supported_array_constructor_type(
|
||||
form: &SExpr,
|
||||
struct_names: &HashSet<String>,
|
||||
enum_names: &HashSet<String>,
|
||||
type_alias_names: &HashSet<String>,
|
||||
) -> Option<String> {
|
||||
if is_ident(form, "i32") {
|
||||
Some("i32".to_string())
|
||||
} else if is_ident(form, "i64") {
|
||||
Some("i64".to_string())
|
||||
} else if is_ident(form, "u32") {
|
||||
Some("u32".to_string())
|
||||
} else if is_ident(form, "u64") {
|
||||
Some("u64".to_string())
|
||||
} else if is_ident(form, "f64") {
|
||||
Some("f64".to_string())
|
||||
} else if is_ident(form, "bool") {
|
||||
Some("bool".to_string())
|
||||
} else if is_ident(form, "string") {
|
||||
Some("string".to_string())
|
||||
} else if let Some(name) = expect_ident(form) {
|
||||
if struct_names.contains(name) || enum_names.contains(name) {
|
||||
Some(name.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
render_direct_type_atom(form, struct_names, enum_names, type_alias_names)
|
||||
}
|
||||
|
||||
fn render_supported_array_type(
|
||||
items: &[SExpr],
|
||||
struct_names: &HashSet<String>,
|
||||
enum_names: &HashSet<String>,
|
||||
type_alias_names: &HashSet<String>,
|
||||
) -> Option<String> {
|
||||
if items.len() != 3 || !is_ident(&items[0], "array") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let elem_ty = render_supported_array_constructor_type(&items[1], struct_names, enum_names)?;
|
||||
let elem_ty = render_supported_array_constructor_type(
|
||||
&items[1],
|
||||
struct_names,
|
||||
enum_names,
|
||||
type_alias_names,
|
||||
)?;
|
||||
let len = expect_int(&items[2])?;
|
||||
if len <= 0 {
|
||||
return None;
|
||||
@ -2603,7 +2735,10 @@ fn render_supported_array_type(
|
||||
Some(format!("(array {} {})", elem_ty, len))
|
||||
}
|
||||
|
||||
fn render_supported_vec_type(items: &[SExpr]) -> Option<String> {
|
||||
fn render_supported_vec_type(
|
||||
items: &[SExpr],
|
||||
type_alias_names: &HashSet<String>,
|
||||
) -> Option<String> {
|
||||
if items.len() == 2 && is_ident(&items[0], "vec") {
|
||||
if is_ident(&items[1], "i32") {
|
||||
return Some("(vec i32)".to_string());
|
||||
@ -2620,11 +2755,66 @@ fn render_supported_vec_type(items: &[SExpr]) -> Option<String> {
|
||||
if is_ident(&items[1], "string") {
|
||||
return Some("(vec string)".to_string());
|
||||
}
|
||||
if let Some(name) = expect_ident(&items[1]) {
|
||||
if type_alias_names.contains(name) {
|
||||
return Some(format!("(vec {})", name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn render_payload_type_atom(form: &SExpr, type_alias_names: &HashSet<String>) -> Option<String> {
|
||||
if is_ident(form, "i32") {
|
||||
Some("i32".to_string())
|
||||
} else if is_ident(form, "i64") {
|
||||
Some("i64".to_string())
|
||||
} else if is_ident(form, "u32") {
|
||||
Some("u32".to_string())
|
||||
} else if is_ident(form, "u64") {
|
||||
Some("u64".to_string())
|
||||
} else if is_ident(form, "f64") {
|
||||
Some("f64".to_string())
|
||||
} else if is_ident(form, "bool") {
|
||||
Some("bool".to_string())
|
||||
} else if is_ident(form, "string") {
|
||||
Some("string".to_string())
|
||||
} else if let Some(name) = expect_ident(form) {
|
||||
type_alias_names.contains(name).then(|| name.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn render_result_err_type_atom(form: &SExpr, type_alias_names: &HashSet<String>) -> Option<String> {
|
||||
if is_ident(form, "i32") {
|
||||
Some("i32".to_string())
|
||||
} else if let Some(name) = expect_ident(form) {
|
||||
type_alias_names.contains(name).then(|| name.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn render_direct_type_atom(
|
||||
form: &SExpr,
|
||||
struct_names: &HashSet<String>,
|
||||
enum_names: &HashSet<String>,
|
||||
type_alias_names: &HashSet<String>,
|
||||
) -> Option<String> {
|
||||
if let Some(text) = render_payload_type_atom(form, type_alias_names) {
|
||||
return Some(text);
|
||||
}
|
||||
|
||||
let name = expect_ident(form)?;
|
||||
if struct_names.contains(name) || enum_names.contains(name) {
|
||||
Some(name.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_list(form: &SExpr) -> Option<&[SExpr]> {
|
||||
match &form.kind {
|
||||
SExprKind::List(items) => Some(items),
|
||||
|
||||
@ -26,12 +26,24 @@ pub fn emit(_file: &str, program: &CheckedProgram) -> Result<String, Vec<Diagnos
|
||||
out.push_str("declare void @print_bool(i1)\n\n");
|
||||
out.push_str("declare i32 @string_len(ptr)\n\n");
|
||||
out.push_str("declare ptr @__glagol_string_concat(ptr, ptr)\n\n");
|
||||
out.push_str("declare i64 @__glagol_string_byte_at_result(ptr, i32)\n\n");
|
||||
out.push_str("declare ptr @__glagol_string_slice_result(ptr, i32, i32)\n\n");
|
||||
out.push_str("declare i1 @__glagol_string_starts_with(ptr, ptr)\n\n");
|
||||
out.push_str("declare i1 @__glagol_string_ends_with(ptr, ptr)\n\n");
|
||||
out.push_str("declare i64 @__glagol_string_parse_i32_result(ptr)\n\n");
|
||||
out.push_str("declare i32 @__glagol_string_parse_i64_result(ptr, ptr)\n\n");
|
||||
out.push_str("declare i64 @__glagol_string_parse_u32_result(ptr)\n\n");
|
||||
out.push_str("declare i32 @__glagol_string_parse_u64_result(ptr, ptr)\n\n");
|
||||
out.push_str("declare i32 @__glagol_string_parse_f64_result(ptr, ptr)\n\n");
|
||||
out.push_str("declare i32 @__glagol_string_parse_bool_result(ptr, ptr)\n\n");
|
||||
out.push_str("declare ptr @__glagol_json_quote_string(ptr)\n\n");
|
||||
out.push_str("declare ptr @__glagol_json_parse_string_value_result(ptr)\n\n");
|
||||
out.push_str("declare i32 @__glagol_json_parse_bool_value_result(ptr, ptr)\n\n");
|
||||
out.push_str("declare i64 @__glagol_json_parse_i32_value_result(ptr)\n\n");
|
||||
out.push_str("declare i64 @__glagol_json_parse_u32_value_result(ptr)\n\n");
|
||||
out.push_str("declare i32 @__glagol_json_parse_i64_value_result(ptr, ptr)\n\n");
|
||||
out.push_str("declare i32 @__glagol_json_parse_u64_value_result(ptr, ptr)\n\n");
|
||||
out.push_str("declare i32 @__glagol_json_parse_f64_value_result(ptr, ptr)\n\n");
|
||||
out.push_str("declare ptr @__glagol_num_i32_to_string(i32)\n\n");
|
||||
out.push_str("declare ptr @__glagol_num_i64_to_string(i64)\n\n");
|
||||
out.push_str("declare ptr @__glagol_num_u32_to_string(i32)\n\n");
|
||||
@ -49,6 +61,21 @@ pub fn emit(_file: &str, program: &CheckedProgram) -> Result<String, Vec<Diagnos
|
||||
out.push_str("declare ptr @__glagol_fs_read_text_result(ptr)\n\n");
|
||||
out.push_str("declare i32 @__glagol_fs_write_text(ptr, ptr)\n\n");
|
||||
out.push_str("declare i32 @__glagol_fs_write_text_result(ptr, ptr)\n\n");
|
||||
out.push_str("declare i1 @__glagol_fs_exists(ptr)\n\n");
|
||||
out.push_str("declare i1 @__glagol_fs_is_file(ptr)\n\n");
|
||||
out.push_str("declare i1 @__glagol_fs_is_dir(ptr)\n\n");
|
||||
out.push_str("declare i32 @__glagol_fs_remove_file_result(ptr)\n\n");
|
||||
out.push_str("declare i32 @__glagol_fs_create_dir_result(ptr)\n\n");
|
||||
out.push_str("declare i64 @__glagol_fs_open_text_read_result(ptr)\n\n");
|
||||
out.push_str("declare ptr @__glagol_fs_read_open_text_result(i32)\n\n");
|
||||
out.push_str("declare i32 @__glagol_fs_close_result(i32)\n\n");
|
||||
out.push_str("declare i64 @__glagol_net_tcp_connect_loopback_result(i32)\n\n");
|
||||
out.push_str("declare i64 @__glagol_net_tcp_listen_loopback_result(i32)\n\n");
|
||||
out.push_str("declare i64 @__glagol_net_tcp_bound_port_result(i32)\n\n");
|
||||
out.push_str("declare i64 @__glagol_net_tcp_accept_result(i32)\n\n");
|
||||
out.push_str("declare ptr @__glagol_net_tcp_read_all_result(i32)\n\n");
|
||||
out.push_str("declare i32 @__glagol_net_tcp_write_text_result(i32, ptr)\n\n");
|
||||
out.push_str("declare i32 @__glagol_net_tcp_close_result(i32)\n\n");
|
||||
out.push_str("declare i1 @__glagol_string_eq(ptr, ptr)\n\n");
|
||||
out.push_str("declare ptr @__glagol_vec_i32_empty()\n\n");
|
||||
out.push_str("declare ptr @__glagol_vec_i32_append(ptr, i32)\n\n");
|
||||
@ -1652,36 +1679,64 @@ impl FunctionGen<'_> {
|
||||
"__glagol_process_arg_result"
|
||||
| "__glagol_env_get_result"
|
||||
| "__glagol_fs_read_text_result"
|
||||
| "__glagol_fs_read_open_text_result"
|
||||
| "__glagol_net_tcp_read_all_result"
|
||||
| "__glagol_io_read_stdin_result"
|
||||
| "__glagol_string_slice_result"
|
||||
| "__glagol_json_parse_string_value_result"
|
||||
) {
|
||||
return self.emit_string_result_host_call(expr, callee, &arg_values);
|
||||
}
|
||||
|
||||
if callee == "__glagol_fs_write_text_result" {
|
||||
if callee == "__glagol_fs_write_text_result"
|
||||
|| callee == "__glagol_fs_remove_file_result"
|
||||
|| callee == "__glagol_fs_create_dir_result"
|
||||
|| callee == "__glagol_fs_close_result"
|
||||
|| callee == "__glagol_net_tcp_write_text_result"
|
||||
|| callee == "__glagol_net_tcp_close_result"
|
||||
{
|
||||
return self.emit_i32_result_status_call(expr, callee, &arg_values);
|
||||
}
|
||||
|
||||
if callee == "__glagol_string_parse_i32_result" {
|
||||
if callee == "__glagol_fs_open_text_read_result"
|
||||
|| callee == "__glagol_net_tcp_connect_loopback_result"
|
||||
|| callee == "__glagol_net_tcp_listen_loopback_result"
|
||||
|| callee == "__glagol_net_tcp_bound_port_result"
|
||||
|| callee == "__glagol_net_tcp_accept_result"
|
||||
|| callee == "__glagol_string_parse_i32_result"
|
||||
|| callee == "__glagol_json_parse_i32_value_result"
|
||||
|| callee == "__glagol_string_byte_at_result"
|
||||
{
|
||||
return self.emit_i32_result_encoded_i64_call(expr, callee, &arg_values);
|
||||
}
|
||||
|
||||
if callee == "__glagol_string_parse_u32_result" {
|
||||
if callee == "__glagol_string_parse_u32_result"
|
||||
|| callee == "__glagol_json_parse_u32_value_result"
|
||||
{
|
||||
return self.emit_i32_result_encoded_i64_call(expr, callee, &arg_values);
|
||||
}
|
||||
|
||||
if callee == "__glagol_string_parse_i64_result" {
|
||||
if callee == "__glagol_string_parse_i64_result"
|
||||
|| callee == "__glagol_json_parse_i64_value_result"
|
||||
{
|
||||
return self.emit_i64_result_out_param_call(expr, callee, &arg_values);
|
||||
}
|
||||
|
||||
if callee == "__glagol_string_parse_u64_result" {
|
||||
if callee == "__glagol_string_parse_u64_result"
|
||||
|| callee == "__glagol_json_parse_u64_value_result"
|
||||
{
|
||||
return self.emit_i64_result_out_param_call(expr, callee, &arg_values);
|
||||
}
|
||||
|
||||
if callee == "__glagol_string_parse_f64_result" {
|
||||
if callee == "__glagol_string_parse_f64_result"
|
||||
|| callee == "__glagol_json_parse_f64_value_result"
|
||||
{
|
||||
return self.emit_f64_result_out_param_call(expr, callee, &arg_values);
|
||||
}
|
||||
|
||||
if callee == "__glagol_string_parse_bool_result" {
|
||||
if callee == "__glagol_string_parse_bool_result"
|
||||
|| callee == "__glagol_json_parse_bool_value_result"
|
||||
{
|
||||
return self.emit_bool_result_out_param_call(expr, callee, &arg_values);
|
||||
}
|
||||
|
||||
|
||||
@ -5,9 +5,14 @@ use crate::{
|
||||
ast::{
|
||||
BinaryOp, CImportDecl, EnumDecl, EnumVariantDecl, Expr, ExprKind, Function, MatchArm,
|
||||
MatchPattern, MatchPatternKind, Param, Program, StructDecl, StructField, StructInitField,
|
||||
Test,
|
||||
Test, TypeAliasDecl,
|
||||
},
|
||||
diag::Diagnostic,
|
||||
reserved::{
|
||||
is_unsupported_generic_standard_library_call, unsupported_generic_function,
|
||||
unsupported_generic_standard_library_call, unsupported_generic_type_alias,
|
||||
unsupported_reserved_type_diagnostic,
|
||||
},
|
||||
sexpr::{Atom, SExpr, SExprKind},
|
||||
token::Span,
|
||||
types::Type,
|
||||
@ -23,6 +28,8 @@ pub fn lower_program_with_imported_names(
|
||||
imported_names: &[String],
|
||||
) -> Result<Program, Vec<Diagnostic>> {
|
||||
let mut module = None;
|
||||
let mut type_aliases = Vec::new();
|
||||
let mut alias_names = HashMap::new();
|
||||
let mut enums = Vec::new();
|
||||
let mut enum_names = imported_names.iter().cloned().collect::<HashSet<_>>();
|
||||
let mut structs = Vec::new();
|
||||
@ -49,10 +56,67 @@ pub fn lower_program_with_imported_names(
|
||||
}
|
||||
Err(mut errs) => errors.append(&mut errs),
|
||||
},
|
||||
Some("type") => match lower_type_alias(file, form) {
|
||||
Ok(alias) => match alias_names.entry(alias.name.clone()) {
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(alias.name_span);
|
||||
type_aliases.push(alias);
|
||||
}
|
||||
Entry::Occupied(entry) => errors.push(
|
||||
Diagnostic::new(
|
||||
file,
|
||||
"DuplicateTypeAlias",
|
||||
format!("duplicate type alias `{}`", alias.name),
|
||||
)
|
||||
.with_span(alias.name_span)
|
||||
.related("original type alias", *entry.get()),
|
||||
),
|
||||
},
|
||||
Err(mut errs) => errors.append(&mut errs),
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
for alias in &type_aliases {
|
||||
if is_reserved_type_name(&alias.name) {
|
||||
errors.push(
|
||||
Diagnostic::new(
|
||||
file,
|
||||
"TypeAliasNameConflict",
|
||||
format!(
|
||||
"type alias `{}` conflicts with a built-in type name",
|
||||
alias.name
|
||||
),
|
||||
)
|
||||
.with_span(alias.name_span)
|
||||
.hint("choose an alias name distinct from built-in type names"),
|
||||
);
|
||||
}
|
||||
if let Some(struct_decl) = structs.iter().find(|decl| decl.name == alias.name) {
|
||||
errors.push(
|
||||
Diagnostic::new(
|
||||
file,
|
||||
"TypeAliasNameConflict",
|
||||
format!("type alias `{}` conflicts with a struct", alias.name),
|
||||
)
|
||||
.with_span(alias.name_span)
|
||||
.related("struct declaration", struct_decl.name_span),
|
||||
);
|
||||
}
|
||||
if let Some(enum_decl) = enums.iter().find(|decl| decl.name == alias.name) {
|
||||
errors.push(
|
||||
Diagnostic::new(
|
||||
file,
|
||||
"TypeAliasNameConflict",
|
||||
format!("type alias `{}` conflicts with an enum", alias.name),
|
||||
)
|
||||
.with_span(alias.name_span)
|
||||
.related("enum declaration", enum_decl.name_span),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for form in forms {
|
||||
match list_head(form) {
|
||||
Some("module") => match lower_module(file, form) {
|
||||
@ -61,6 +125,7 @@ pub fn lower_program_with_imported_names(
|
||||
},
|
||||
Some("enum") => {}
|
||||
Some("struct") => {}
|
||||
Some("type") => {}
|
||||
Some("import_c") => match lower_c_import(file, form) {
|
||||
Ok(import) => c_imports.push(import),
|
||||
Err(mut errs) => errors.append(&mut errs),
|
||||
@ -106,6 +171,7 @@ pub fn lower_program_with_imported_names(
|
||||
if errors.is_empty() {
|
||||
Ok(Program {
|
||||
module: module.unwrap_or_else(|| "main".to_string()),
|
||||
type_aliases,
|
||||
enums,
|
||||
structs,
|
||||
c_imports,
|
||||
@ -124,6 +190,14 @@ pub fn print_program(program: &Program) -> String {
|
||||
output.push_str(&program.module);
|
||||
output.push('\n');
|
||||
|
||||
for alias in &program.type_aliases {
|
||||
output.push_str(" type ");
|
||||
output.push_str(&alias.name);
|
||||
output.push_str(" = ");
|
||||
output.push_str(&alias.target.to_string());
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
for enum_decl in &program.enums {
|
||||
output.push_str(" enum ");
|
||||
output.push_str(&enum_decl.name);
|
||||
@ -543,6 +617,77 @@ fn lower_module(file: &str, form: &SExpr) -> Result<String, Diagnostic> {
|
||||
})
|
||||
}
|
||||
|
||||
fn lower_type_alias(file: &str, form: &SExpr) -> Result<TypeAliasDecl, Vec<Diagnostic>> {
|
||||
let mut errors = Vec::new();
|
||||
let Some(items) = expect_list(form) else {
|
||||
return Err(vec![Diagnostic::new(
|
||||
file,
|
||||
"MalformedTypeAlias",
|
||||
"type alias form must be a list",
|
||||
)
|
||||
.with_span(form.span)]);
|
||||
};
|
||||
|
||||
if matches!(items.get(2).and_then(list_head), Some("type_params")) {
|
||||
return Err(vec![unsupported_generic_type_alias(
|
||||
file,
|
||||
items.get(2).map_or(form.span, |item| item.span),
|
||||
)]);
|
||||
}
|
||||
|
||||
if items.len() != 3 {
|
||||
return Err(vec![Diagnostic::new(
|
||||
file,
|
||||
"MalformedTypeAlias",
|
||||
"type alias form must be `(type Alias TargetType)`",
|
||||
)
|
||||
.with_span(form.span)
|
||||
.expected("(type Alias TargetType)")]);
|
||||
}
|
||||
|
||||
let name = match expect_ident(&items[1]) {
|
||||
Some(name) => name.to_string(),
|
||||
None => {
|
||||
errors.push(
|
||||
Diagnostic::new(
|
||||
file,
|
||||
"InvalidTypeAliasName",
|
||||
"type alias name must be an identifier",
|
||||
)
|
||||
.with_span(items[1].span),
|
||||
);
|
||||
"<error>".to_string()
|
||||
}
|
||||
};
|
||||
|
||||
let target = match lower_type(&items[2]) {
|
||||
Some(ty) => ty,
|
||||
None => {
|
||||
errors.push(invalid_type_diagnostic(
|
||||
file,
|
||||
&items[2],
|
||||
"InvalidTypeAliasTarget",
|
||||
"type alias target type is invalid",
|
||||
Some("concrete type"),
|
||||
None,
|
||||
));
|
||||
Type::Unit
|
||||
}
|
||||
};
|
||||
|
||||
if errors.is_empty() {
|
||||
Ok(TypeAliasDecl {
|
||||
name,
|
||||
name_span: items[1].span,
|
||||
target,
|
||||
target_span: items[2].span,
|
||||
span: form.span,
|
||||
})
|
||||
} else {
|
||||
Err(errors)
|
||||
}
|
||||
}
|
||||
|
||||
fn lower_struct(file: &str, form: &SExpr) -> Result<StructDecl, Vec<Diagnostic>> {
|
||||
let mut errors = Vec::new();
|
||||
let Some(items) = expect_list(form) else {
|
||||
@ -617,10 +762,14 @@ fn lower_struct(file: &str, form: &SExpr) -> Result<StructDecl, Vec<Diagnostic>>
|
||||
};
|
||||
|
||||
let Some(ty) = lower_type(&pair[1]) else {
|
||||
errors.push(
|
||||
Diagnostic::new(file, "InvalidStructFieldType", "invalid struct field type")
|
||||
.with_span(pair[1].span),
|
||||
);
|
||||
errors.push(invalid_type_diagnostic(
|
||||
file,
|
||||
&pair[1],
|
||||
"InvalidStructFieldType",
|
||||
"invalid struct field type",
|
||||
None,
|
||||
None,
|
||||
));
|
||||
continue;
|
||||
};
|
||||
|
||||
@ -724,15 +873,14 @@ fn lower_enum(file: &str, form: &SExpr) -> Result<EnumDecl, Vec<Diagnostic>> {
|
||||
};
|
||||
|
||||
let Some(payload_ty) = lower_type(&variant_items[1]) else {
|
||||
errors.push(
|
||||
Diagnostic::new(
|
||||
file,
|
||||
"InvalidEnumVariant",
|
||||
"enum variant payload type is invalid",
|
||||
)
|
||||
.with_span(variant_items[1].span)
|
||||
.expected("i32"),
|
||||
);
|
||||
errors.push(invalid_type_diagnostic(
|
||||
file,
|
||||
&variant_items[1],
|
||||
"InvalidEnumVariant",
|
||||
"enum variant payload type is invalid",
|
||||
Some("i32"),
|
||||
None,
|
||||
));
|
||||
continue;
|
||||
};
|
||||
|
||||
@ -784,6 +932,10 @@ fn lower_function(
|
||||
.hint("expected `(fn name ((arg Type) ...) -> ReturnType body...)`")]);
|
||||
}
|
||||
|
||||
if matches!(items.get(2).and_then(list_head), Some("type_params")) {
|
||||
return Err(vec![unsupported_generic_function(file, items[2].span)]);
|
||||
}
|
||||
|
||||
let name = match expect_ident(&items[1]) {
|
||||
Some(name) => name.to_string(),
|
||||
None => {
|
||||
@ -821,10 +973,14 @@ fn lower_function(
|
||||
}
|
||||
Some(ty) => ty,
|
||||
None => {
|
||||
errors.push(
|
||||
Diagnostic::new(file, "InvalidReturnType", "invalid return type")
|
||||
.with_span(items[4].span),
|
||||
);
|
||||
errors.push(invalid_type_diagnostic(
|
||||
file,
|
||||
&items[4],
|
||||
"InvalidReturnType",
|
||||
"invalid return type",
|
||||
None,
|
||||
None,
|
||||
));
|
||||
Type::Unit
|
||||
}
|
||||
};
|
||||
@ -921,10 +1077,14 @@ fn lower_c_import(file: &str, form: &SExpr) -> Result<CImportDecl, Vec<Diagnosti
|
||||
let return_type = match lower_type(&items[4]) {
|
||||
Some(ty) => ty,
|
||||
None => {
|
||||
errors.push(
|
||||
Diagnostic::new(file, "MalformedCImport", "invalid C import return type")
|
||||
.with_span(items[4].span),
|
||||
);
|
||||
errors.push(invalid_type_diagnostic(
|
||||
file,
|
||||
&items[4],
|
||||
"MalformedCImport",
|
||||
"invalid C import return type",
|
||||
None,
|
||||
None,
|
||||
));
|
||||
Type::Unit
|
||||
}
|
||||
};
|
||||
@ -1030,10 +1190,14 @@ fn lower_c_import_params(file: &str, form: &SExpr) -> Result<Vec<Param>, Vec<Dia
|
||||
);
|
||||
}
|
||||
let Some(ty) = lower_type(&pair[1]) else {
|
||||
errors.push(
|
||||
Diagnostic::new(file, "MalformedCImport", "invalid C import parameter type")
|
||||
.with_span(pair[1].span),
|
||||
);
|
||||
errors.push(invalid_type_diagnostic(
|
||||
file,
|
||||
&pair[1],
|
||||
"MalformedCImport",
|
||||
"invalid C import parameter type",
|
||||
None,
|
||||
None,
|
||||
));
|
||||
continue;
|
||||
};
|
||||
params.push(Param {
|
||||
@ -1198,10 +1362,14 @@ fn lower_params(file: &str, form: &SExpr) -> Result<Vec<Param>, Vec<Diagnostic>>
|
||||
};
|
||||
|
||||
let Some(ty) = lower_type(&pair[1]) else {
|
||||
errors.push(
|
||||
Diagnostic::new(file, "InvalidParamType", "invalid parameter type")
|
||||
.with_span(pair[1].span),
|
||||
);
|
||||
errors.push(invalid_type_diagnostic(
|
||||
file,
|
||||
&pair[1],
|
||||
"InvalidParamType",
|
||||
"invalid parameter type",
|
||||
None,
|
||||
None,
|
||||
));
|
||||
continue;
|
||||
};
|
||||
|
||||
@ -1249,6 +1417,28 @@ fn unsupported_unit_return_signature(file: &str, span: crate::token::Span) -> Di
|
||||
.hint("`unit` is reserved for compiler/runtime unit-producing forms")
|
||||
}
|
||||
|
||||
fn is_reserved_type_name(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"i32"
|
||||
| "i64"
|
||||
| "u32"
|
||||
| "u64"
|
||||
| "f64"
|
||||
| "bool"
|
||||
| "unit"
|
||||
| "string"
|
||||
| "ptr"
|
||||
| "slice"
|
||||
| "option"
|
||||
| "result"
|
||||
| "array"
|
||||
| "vec"
|
||||
| "map"
|
||||
| "set"
|
||||
)
|
||||
}
|
||||
|
||||
fn lower_type(form: &SExpr) -> Option<Type> {
|
||||
match &form.kind {
|
||||
SExprKind::Atom(Atom::Ident(name)) => match name.as_str() {
|
||||
@ -1556,6 +1746,9 @@ fn lower_expr(
|
||||
span: form.span,
|
||||
})
|
||||
}
|
||||
name if is_unsupported_generic_standard_library_call(name) => Err(
|
||||
unsupported_generic_standard_library_call(file, items[0].span, name),
|
||||
),
|
||||
name => {
|
||||
let mut args = Vec::new();
|
||||
for item in &items[1..] {
|
||||
@ -1930,13 +2123,14 @@ fn lower_array_init(
|
||||
}
|
||||
|
||||
let Some(elem_ty) = lower_type(&items[1]) else {
|
||||
return Err(Diagnostic::new(
|
||||
return Err(invalid_type_diagnostic(
|
||||
file,
|
||||
&items[1],
|
||||
"InvalidArrayElementType",
|
||||
"array constructor element type is invalid",
|
||||
)
|
||||
.with_span(items[1].span)
|
||||
.hint("fixed arrays use direct scalar `i32`, `i64`, `f64`, `bool`, `string`, known enum, or known non-recursive struct elements"));
|
||||
None,
|
||||
Some("fixed arrays use direct scalar `i32`, `i64`, `f64`, `bool`, `string`, known enum, or known non-recursive struct elements"),
|
||||
));
|
||||
};
|
||||
|
||||
let mut elements = Vec::new();
|
||||
@ -1972,13 +2166,14 @@ fn lower_option_some(
|
||||
}
|
||||
|
||||
let Some(payload_ty) = lower_type(&items[1]) else {
|
||||
return Err(Diagnostic::new(
|
||||
return Err(invalid_type_diagnostic(
|
||||
file,
|
||||
&items[1],
|
||||
"InvalidOptionPayloadType",
|
||||
"option constructor payload type is invalid",
|
||||
)
|
||||
.with_span(items[1].span)
|
||||
.hint("first-pass options use `i32` payloads"));
|
||||
None,
|
||||
Some("first-pass options use `i32` payloads"),
|
||||
));
|
||||
};
|
||||
|
||||
Ok(Expr {
|
||||
@ -2003,13 +2198,14 @@ fn lower_option_none(file: &str, form: &SExpr, items: &[SExpr]) -> Result<Expr,
|
||||
}
|
||||
|
||||
let Some(payload_ty) = lower_type(&items[1]) else {
|
||||
return Err(Diagnostic::new(
|
||||
return Err(invalid_type_diagnostic(
|
||||
file,
|
||||
&items[1],
|
||||
"InvalidOptionPayloadType",
|
||||
"option constructor payload type is invalid",
|
||||
)
|
||||
.with_span(items[1].span)
|
||||
.hint("first-pass options use `i32` payloads"));
|
||||
None,
|
||||
Some("first-pass options use `i32` payloads"),
|
||||
));
|
||||
};
|
||||
|
||||
Ok(Expr {
|
||||
@ -2039,22 +2235,24 @@ fn lower_result_ok(
|
||||
}
|
||||
|
||||
let Some(ok_ty) = lower_type(&items[1]) else {
|
||||
return Err(Diagnostic::new(
|
||||
return Err(invalid_type_diagnostic(
|
||||
file,
|
||||
&items[1],
|
||||
"InvalidResultPayloadType",
|
||||
"result constructor ok type is invalid",
|
||||
)
|
||||
.with_span(items[1].span)
|
||||
.hint("first-pass results use `i32` payloads"));
|
||||
None,
|
||||
Some("first-pass results use `i32` payloads"),
|
||||
));
|
||||
};
|
||||
let Some(err_ty) = lower_type(&items[2]) else {
|
||||
return Err(Diagnostic::new(
|
||||
return Err(invalid_type_diagnostic(
|
||||
file,
|
||||
&items[2],
|
||||
"InvalidResultPayloadType",
|
||||
"result constructor err type is invalid",
|
||||
)
|
||||
.with_span(items[2].span)
|
||||
.hint("first-pass results use `i32` payloads"));
|
||||
None,
|
||||
Some("first-pass results use `i32` payloads"),
|
||||
));
|
||||
};
|
||||
|
||||
Ok(Expr {
|
||||
@ -2087,22 +2285,24 @@ fn lower_result_err(
|
||||
}
|
||||
|
||||
let Some(ok_ty) = lower_type(&items[1]) else {
|
||||
return Err(Diagnostic::new(
|
||||
return Err(invalid_type_diagnostic(
|
||||
file,
|
||||
&items[1],
|
||||
"InvalidResultPayloadType",
|
||||
"result constructor ok type is invalid",
|
||||
)
|
||||
.with_span(items[1].span)
|
||||
.hint("first-pass results use `i32` payloads"));
|
||||
None,
|
||||
Some("first-pass results use `i32` payloads"),
|
||||
));
|
||||
};
|
||||
let Some(err_ty) = lower_type(&items[2]) else {
|
||||
return Err(Diagnostic::new(
|
||||
return Err(invalid_type_diagnostic(
|
||||
file,
|
||||
&items[2],
|
||||
"InvalidResultPayloadType",
|
||||
"result constructor err type is invalid",
|
||||
)
|
||||
.with_span(items[2].span)
|
||||
.hint("first-pass results use `i32` payloads"));
|
||||
None,
|
||||
Some("first-pass results use `i32` payloads"),
|
||||
));
|
||||
};
|
||||
|
||||
Ok(Expr {
|
||||
@ -2224,10 +2424,14 @@ fn lower_local(
|
||||
};
|
||||
|
||||
let Some(ty) = lower_type(&items[2]) else {
|
||||
return Err(
|
||||
Diagnostic::new(file, "InvalidLocalType", "invalid local type")
|
||||
.with_span(items[2].span),
|
||||
);
|
||||
return Err(invalid_type_diagnostic(
|
||||
file,
|
||||
&items[2],
|
||||
"InvalidLocalType",
|
||||
"invalid local type",
|
||||
None,
|
||||
None,
|
||||
));
|
||||
};
|
||||
|
||||
Ok(Expr {
|
||||
@ -2314,6 +2518,29 @@ fn lower_while(
|
||||
})
|
||||
}
|
||||
|
||||
fn invalid_type_diagnostic(
|
||||
file: &str,
|
||||
form: &SExpr,
|
||||
fallback_code: &'static str,
|
||||
fallback_message: impl Into<String>,
|
||||
fallback_expected: Option<&str>,
|
||||
fallback_hint: Option<&str>,
|
||||
) -> Diagnostic {
|
||||
if let Some(diagnostic) = unsupported_reserved_type_diagnostic(file, form) {
|
||||
return diagnostic;
|
||||
}
|
||||
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(file, fallback_code, fallback_message).with_span(form.span);
|
||||
if let Some(expected) = fallback_expected {
|
||||
diagnostic = diagnostic.expected(expected);
|
||||
}
|
||||
if let Some(hint) = fallback_hint {
|
||||
diagnostic = diagnostic.hint(hint);
|
||||
}
|
||||
diagnostic
|
||||
}
|
||||
|
||||
fn list_head(form: &SExpr) -> Option<&str> {
|
||||
let items = expect_list(form)?;
|
||||
let first = items.first()?;
|
||||
|
||||
@ -8,21 +8,27 @@ mod lexer;
|
||||
mod llvm;
|
||||
mod lower;
|
||||
mod project;
|
||||
mod reserved;
|
||||
mod scaffold;
|
||||
mod sexpr;
|
||||
mod std_runtime;
|
||||
mod symbols;
|
||||
mod test_runner;
|
||||
mod token;
|
||||
mod types;
|
||||
mod unsafe_ops;
|
||||
|
||||
use std::{
|
||||
env, fs, io,
|
||||
env, fs,
|
||||
io::{self, Write},
|
||||
panic::{self, AssertUnwindSafe},
|
||||
path::{Path, PathBuf},
|
||||
process::{self, Command as ProcessCommand},
|
||||
thread,
|
||||
};
|
||||
|
||||
const TEST_RUNNER_THREAD_STACK_SIZE: usize = 16 * 1024 * 1024;
|
||||
|
||||
fn main() {
|
||||
let raw_args = env::args().collect::<Vec<_>>();
|
||||
let command_line = raw_args.join(" ");
|
||||
@ -48,9 +54,29 @@ fn run_invocation_guarded(invocation: Invocation) -> ! {
|
||||
panic::set_hook(Box::new(|_| {}));
|
||||
|
||||
let run_invocation = invocation.clone();
|
||||
let result = panic::catch_unwind(AssertUnwindSafe(move || {
|
||||
run_invocation_inner(run_invocation);
|
||||
}));
|
||||
let result = if invocation.mode == Mode::RunTests {
|
||||
let thread_invocation = run_invocation.clone();
|
||||
match thread::Builder::new()
|
||||
.name("glagol-test-runner".to_string())
|
||||
.stack_size(TEST_RUNNER_THREAD_STACK_SIZE)
|
||||
.spawn(move || {
|
||||
panic::catch_unwind(AssertUnwindSafe(move || {
|
||||
run_invocation_inner(thread_invocation);
|
||||
}))
|
||||
}) {
|
||||
Ok(handle) => match handle.join() {
|
||||
Ok(result) => result,
|
||||
Err(payload) => Err(payload),
|
||||
},
|
||||
Err(_) => panic::catch_unwind(AssertUnwindSafe(move || {
|
||||
run_invocation_inner(run_invocation);
|
||||
})),
|
||||
}
|
||||
} else {
|
||||
panic::catch_unwind(AssertUnwindSafe(move || {
|
||||
run_invocation_inner(run_invocation);
|
||||
}))
|
||||
};
|
||||
|
||||
panic::set_hook(previous_hook);
|
||||
|
||||
@ -87,21 +113,32 @@ fn run_invocation_inner(invocation: Invocation) -> ! {
|
||||
if invocation.mode == Mode::New {
|
||||
run_new(invocation);
|
||||
}
|
||||
if invocation.mode == Mode::Clean {
|
||||
run_clean(invocation);
|
||||
}
|
||||
if invocation.mode == Mode::Doc {
|
||||
run_doc(invocation);
|
||||
}
|
||||
if invocation.mode == Mode::Symbols && project::is_project_input(&invocation.path) {
|
||||
run_project_symbols(invocation);
|
||||
}
|
||||
if invocation.mode == Mode::Format && invocation.fmt_action != FmtAction::Stdout {
|
||||
run_fmt_action(invocation);
|
||||
}
|
||||
|
||||
let project_capable_mode = matches!(invocation.mode, Mode::Check | Mode::Build)
|
||||
|| (invocation.mode == Mode::RunTests && invocation.manifest_mode_name == "test");
|
||||
let project_capable_mode = matches!(
|
||||
invocation.mode,
|
||||
Mode::Check | Mode::Build | Mode::Run | Mode::Symbols
|
||||
) || (invocation.mode == Mode::RunTests
|
||||
&& invocation.manifest_mode_name == "test");
|
||||
if project_capable_mode && project::is_project_input(&invocation.path) {
|
||||
match invocation.mode {
|
||||
Mode::Check => run_project_check(invocation),
|
||||
Mode::RunTests => run_project_test(invocation),
|
||||
Mode::Build => run_project_build(invocation),
|
||||
_ => unreachable!("project mode is selected only for check/test/build"),
|
||||
Mode::Run => run_project_run(invocation),
|
||||
Mode::Symbols => run_project_symbols(invocation),
|
||||
_ => unreachable!("project mode is selected only for check/test/build/run/symbols"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,6 +161,7 @@ fn run_invocation_inner(invocation: Invocation) -> ! {
|
||||
|
||||
match invocation.mode {
|
||||
Mode::Build => run_build(invocation, &source),
|
||||
Mode::Run => run_run(invocation, &source),
|
||||
mode => run_text_mode(invocation, mode, &source),
|
||||
}
|
||||
}
|
||||
@ -146,7 +184,13 @@ fn run_project_check(invocation: Invocation) -> ! {
|
||||
}
|
||||
|
||||
fn run_project_test(invocation: Invocation) -> ! {
|
||||
match project::run_tests(&invocation.path, invocation.test_filter.as_deref()) {
|
||||
let result = if invocation.test_list {
|
||||
project::list_tests(&invocation.path, invocation.test_filter.as_deref())
|
||||
} else {
|
||||
project::run_tests(&invocation.path, invocation.test_filter.as_deref())
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(success) => {
|
||||
let output = success.output;
|
||||
let primary_output = if let Some(output_path) = invocation.output_path.as_deref() {
|
||||
@ -197,6 +241,33 @@ fn run_project_build(invocation: Invocation) -> ! {
|
||||
run_build_from_llvm(invocation, output.text, Some(output.artifact), Vec::new());
|
||||
}
|
||||
|
||||
fn run_project_run(invocation: Invocation) -> ! {
|
||||
let output = match project::compile_to_llvm(&invocation.path) {
|
||||
Ok(output) => output,
|
||||
Err(failure) => exit_project_failure(invocation, failure),
|
||||
};
|
||||
run_native_from_llvm(invocation, output.text, Some(output.artifact), Vec::new());
|
||||
}
|
||||
|
||||
fn run_project_symbols(invocation: Invocation) -> ! {
|
||||
let loaded = match project::load_project_sources_for_tools(&invocation.path) {
|
||||
Ok(loaded) => loaded,
|
||||
Err(failure) => exit_tool_failure(invocation, failure),
|
||||
};
|
||||
let output = match symbols::render_project(&loaded) {
|
||||
Ok(output) => output,
|
||||
Err(diagnostics) => exit_tool_failure(
|
||||
invocation.clone(),
|
||||
project::ToolFailure {
|
||||
diagnostics,
|
||||
sources: loaded.sources.clone(),
|
||||
artifact: Some(loaded.artifact.clone()),
|
||||
},
|
||||
),
|
||||
};
|
||||
finish_symbols_output(invocation, output, Some(&loaded.artifact));
|
||||
}
|
||||
|
||||
fn exit_project_failure(invocation: Invocation, failure: project::ProjectTestFailure) -> ! {
|
||||
let rendered = render_source_diagnostics_multi(
|
||||
&failure.diagnostics,
|
||||
@ -219,11 +290,52 @@ fn exit_project_failure(invocation: Invocation, failure: project::ProjectTestFai
|
||||
process::exit(ExitCode::SourceFailure.code());
|
||||
}
|
||||
|
||||
fn exit_tool_failure(invocation: Invocation, failure: project::ToolFailure) -> ! {
|
||||
let rendered = render_source_diagnostics_multi(
|
||||
&failure.diagnostics,
|
||||
&failure.sources,
|
||||
invocation.diagnostics,
|
||||
);
|
||||
eprint!("{}", rendered.stderr);
|
||||
write_manifest_if_requested_with_project(
|
||||
&invocation,
|
||||
false,
|
||||
PrimaryOutput::Diagnostics {
|
||||
text: &rendered.machine_text,
|
||||
},
|
||||
None,
|
||||
None,
|
||||
failure.artifact.as_ref(),
|
||||
);
|
||||
process::exit(ExitCode::SourceFailure.code());
|
||||
}
|
||||
|
||||
fn run_text_mode(invocation: Invocation, mode: Mode, source: &str) -> ! {
|
||||
if mode == Mode::RunTests {
|
||||
run_test_mode(invocation, source);
|
||||
}
|
||||
|
||||
if mode == Mode::Symbols {
|
||||
match symbols::render_file(&invocation.path, source) {
|
||||
Ok(output) => finish_symbols_output(invocation, output, None),
|
||||
Err(diagnostics) => {
|
||||
let rendered =
|
||||
render_source_diagnostics(&diagnostics, source, invocation.diagnostics);
|
||||
eprint!("{}", rendered.stderr);
|
||||
write_manifest_if_requested(
|
||||
&invocation,
|
||||
false,
|
||||
PrimaryOutput::Diagnostics {
|
||||
text: &rendered.machine_text,
|
||||
},
|
||||
None,
|
||||
None,
|
||||
);
|
||||
process::exit(ExitCode::SourceFailure.code());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let foreign_imports = c_imports_for_manifest(&invocation.path, source);
|
||||
let result = match mode {
|
||||
Mode::EmitLlvm => driver::compile_to_llvm(&invocation.path, source),
|
||||
@ -233,9 +345,12 @@ fn run_text_mode(invocation: Invocation, mode: Mode, source: &str) -> ! {
|
||||
Mode::InspectLoweringSurface => driver::inspect_lowering_surface(&invocation.path, source),
|
||||
Mode::InspectLoweringChecked => driver::inspect_lowering_checked(&invocation.path, source),
|
||||
Mode::CheckTests => driver::check_tests(&invocation.path, source),
|
||||
Mode::Symbols => unreachable!("symbols mode is handled separately"),
|
||||
Mode::RunTests => unreachable!("test mode is handled separately"),
|
||||
Mode::Build => unreachable!("build is handled separately"),
|
||||
Mode::Run => unreachable!("run is handled separately"),
|
||||
Mode::New => unreachable!("new is handled separately"),
|
||||
Mode::Clean => unreachable!("clean is handled separately"),
|
||||
Mode::Doc => unreachable!("doc is handled separately"),
|
||||
};
|
||||
|
||||
@ -299,8 +414,54 @@ fn run_text_mode(invocation: Invocation, mode: Mode, source: &str) -> ! {
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_symbols_output(
|
||||
invocation: Invocation,
|
||||
output: String,
|
||||
project_artifact: Option<&project::ProjectArtifact>,
|
||||
) -> ! {
|
||||
let primary_output = if let Some(output_path) = invocation.output_path.as_deref() {
|
||||
if let Err(err) = fs::write(output_path, &output) {
|
||||
let message = format!("cannot write `{}`: {}", output_path, err);
|
||||
emit_message_diagnostic(
|
||||
&message,
|
||||
"OutputWriteFailed",
|
||||
ExitCode::ArtifactFailure,
|
||||
&invocation,
|
||||
PrimaryOutput::Diagnostics {
|
||||
text: message.as_str(),
|
||||
},
|
||||
None,
|
||||
);
|
||||
}
|
||||
PrimaryOutput::Path {
|
||||
kind: Mode::Symbols.output_kind(),
|
||||
path: output_path,
|
||||
}
|
||||
} else {
|
||||
print!("{}", output);
|
||||
PrimaryOutput::Stdout {
|
||||
kind: Mode::Symbols.output_kind(),
|
||||
text: &output,
|
||||
}
|
||||
};
|
||||
|
||||
write_manifest_if_requested_with_project(
|
||||
&invocation,
|
||||
true,
|
||||
primary_output,
|
||||
None,
|
||||
None,
|
||||
project_artifact,
|
||||
);
|
||||
process::exit(0);
|
||||
}
|
||||
|
||||
fn run_new(invocation: Invocation) -> ! {
|
||||
match scaffold::create_project(&invocation.path, invocation.project_name.as_deref()) {
|
||||
match scaffold::create_project(
|
||||
&invocation.path,
|
||||
invocation.project_name.as_deref(),
|
||||
invocation.project_template,
|
||||
) {
|
||||
Ok(()) => {
|
||||
write_manifest_if_requested(&invocation, true, PrimaryOutput::NoOutput, None, None);
|
||||
process::exit(0);
|
||||
@ -322,6 +483,28 @@ fn run_new(invocation: Invocation) -> ! {
|
||||
}
|
||||
}
|
||||
|
||||
fn run_clean(invocation: Invocation) -> ! {
|
||||
let build_dir = generated_build_dir(&invocation.path);
|
||||
if build_dir.exists() {
|
||||
if let Err(err) = fs::remove_dir_all(&build_dir) {
|
||||
let message = format!("cannot remove `{}`: {}", build_dir.display(), err);
|
||||
emit_message_diagnostic(
|
||||
&message,
|
||||
"CleanFailed",
|
||||
ExitCode::ArtifactFailure,
|
||||
&invocation,
|
||||
PrimaryOutput::Diagnostics {
|
||||
text: message.as_str(),
|
||||
},
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
write_manifest_if_requested(&invocation, true, PrimaryOutput::NoOutput, None, None);
|
||||
process::exit(0);
|
||||
}
|
||||
|
||||
fn run_doc(invocation: Invocation) -> ! {
|
||||
let Some(output_dir) = invocation.output_path.as_deref() else {
|
||||
emit_message_diagnostic(
|
||||
@ -502,7 +685,13 @@ fn finish_formatted_source(
|
||||
|
||||
fn run_test_mode(invocation: Invocation, source: &str) -> ! {
|
||||
let foreign_imports = c_imports_for_manifest(&invocation.path, source);
|
||||
match driver::run_tests(&invocation.path, source, invocation.test_filter.as_deref()) {
|
||||
let result = if invocation.test_list {
|
||||
driver::list_tests(&invocation.path, source, invocation.test_filter.as_deref())
|
||||
} else {
|
||||
driver::run_tests(&invocation.path, source, invocation.test_filter.as_deref())
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(result) => {
|
||||
let output = result.output;
|
||||
let primary_output = if let Some(output_path) = invocation.output_path.as_deref() {
|
||||
@ -589,6 +778,31 @@ fn run_build(invocation: Invocation, source: &str) -> ! {
|
||||
run_build_from_llvm(invocation, llvm_ir, None, foreign_imports);
|
||||
}
|
||||
|
||||
fn run_run(invocation: Invocation, source: &str) -> ! {
|
||||
let foreign_imports = c_imports_for_manifest(&invocation.path, source);
|
||||
let llvm_ir = match driver::compile_to_llvm(&invocation.path, source) {
|
||||
Ok(output) => output,
|
||||
Err(diagnostics) => {
|
||||
let rendered = render_source_diagnostics(&diagnostics, source, invocation.diagnostics);
|
||||
eprint!("{}", rendered.stderr);
|
||||
write_manifest_if_requested_with_foreign_imports(
|
||||
&invocation,
|
||||
false,
|
||||
PrimaryOutput::Diagnostics {
|
||||
text: &rendered.machine_text,
|
||||
},
|
||||
None,
|
||||
None,
|
||||
&foreign_imports,
|
||||
None,
|
||||
);
|
||||
process::exit(ExitCode::SourceFailure.code());
|
||||
}
|
||||
};
|
||||
|
||||
run_native_from_llvm(invocation, llvm_ir, None, foreign_imports);
|
||||
}
|
||||
|
||||
fn run_build_from_llvm(
|
||||
invocation: Invocation,
|
||||
llvm_ir: String,
|
||||
@ -607,18 +821,125 @@ fn run_build_from_llvm(
|
||||
None,
|
||||
);
|
||||
};
|
||||
let output_path = PathBuf::from(output_path);
|
||||
let native = build_native_executable_or_exit(&invocation, llvm_ir, &output_path);
|
||||
let output_display = native.output_path.display().to_string();
|
||||
write_manifest_if_requested_with_foreign_imports(
|
||||
&invocation,
|
||||
true,
|
||||
PrimaryOutput::Path {
|
||||
kind: "native-executable",
|
||||
path: &output_display,
|
||||
},
|
||||
None,
|
||||
Some(BuildInfo {
|
||||
clang: &native.clang,
|
||||
runtime: &native.runtime,
|
||||
c_inputs: &invocation.link_c_paths,
|
||||
}),
|
||||
&foreign_imports,
|
||||
project_artifact.as_ref(),
|
||||
);
|
||||
process::exit(0);
|
||||
}
|
||||
|
||||
if let Some(parent) = Path::new(output_path).parent() {
|
||||
fn run_native_from_llvm(
|
||||
invocation: Invocation,
|
||||
llvm_ir: String,
|
||||
project_artifact: Option<project::ProjectArtifact>,
|
||||
foreign_imports: Vec<project::ProjectArtifactCImport>,
|
||||
) -> ! {
|
||||
let output_path = run_output_path(&invocation, project_artifact.as_ref());
|
||||
if invocation.output_path.is_none() {
|
||||
if let Some(parent) = output_path.parent() {
|
||||
if let Err(err) = fs::create_dir_all(parent) {
|
||||
let message = format!("cannot create `{}`: {}", parent.display(), err);
|
||||
emit_message_diagnostic(
|
||||
&message,
|
||||
"OutputWriteFailed",
|
||||
ExitCode::ArtifactFailure,
|
||||
&invocation,
|
||||
PrimaryOutput::Diagnostics {
|
||||
text: message.as_str(),
|
||||
},
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let native = build_native_executable_or_exit(&invocation, llvm_ir, &output_path);
|
||||
let run_output = match ProcessCommand::new(&native.output_path)
|
||||
.args(&invocation.run_args)
|
||||
.output()
|
||||
{
|
||||
Ok(output) => output,
|
||||
Err(err) => {
|
||||
let message = format!("cannot run `{}`: {}", native.output_path.display(), err);
|
||||
emit_message_diagnostic(
|
||||
&message,
|
||||
"RunFailed",
|
||||
ExitCode::ArtifactFailure,
|
||||
&invocation,
|
||||
PrimaryOutput::Diagnostics {
|
||||
text: message.as_str(),
|
||||
},
|
||||
None,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let _ = io::stdout().write_all(&run_output.stdout);
|
||||
let _ = io::stderr().write_all(&run_output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&run_output.stdout).to_string();
|
||||
let stderr = String::from_utf8_lossy(&run_output.stderr).to_string();
|
||||
let exit_status = run_output.status.code();
|
||||
write_manifest_if_requested_with_run_report(
|
||||
&invocation,
|
||||
run_output.status.success(),
|
||||
PrimaryOutput::Stdout {
|
||||
kind: Mode::Run.output_kind(),
|
||||
text: &stdout,
|
||||
},
|
||||
Some(BuildInfo {
|
||||
clang: &native.clang,
|
||||
runtime: &native.runtime,
|
||||
c_inputs: &invocation.link_c_paths,
|
||||
}),
|
||||
&foreign_imports,
|
||||
project_artifact.as_ref(),
|
||||
RunReport {
|
||||
exit_status,
|
||||
stdout: &stdout,
|
||||
stderr: &stderr,
|
||||
args: &invocation.run_args,
|
||||
},
|
||||
);
|
||||
process::exit(exit_status.unwrap_or(1));
|
||||
}
|
||||
|
||||
struct NativeBuild {
|
||||
output_path: PathBuf,
|
||||
clang: String,
|
||||
runtime: PathBuf,
|
||||
}
|
||||
|
||||
fn build_native_executable_or_exit(
|
||||
invocation: &Invocation,
|
||||
llvm_ir: String,
|
||||
output_path: &Path,
|
||||
) -> NativeBuild {
|
||||
if let Some(parent) = output_path.parent() {
|
||||
if !parent.as_os_str().is_empty() && !parent.is_dir() {
|
||||
let message = format!(
|
||||
"cannot write `{}`: parent directory does not exist",
|
||||
output_path
|
||||
output_path.display()
|
||||
);
|
||||
emit_message_diagnostic(
|
||||
&message,
|
||||
"OutputWriteFailed",
|
||||
ExitCode::ArtifactFailure,
|
||||
&invocation,
|
||||
invocation,
|
||||
PrimaryOutput::Diagnostics {
|
||||
text: message.as_str(),
|
||||
},
|
||||
@ -634,7 +955,7 @@ fn run_build_from_llvm(
|
||||
&message,
|
||||
"ToolchainUnavailable",
|
||||
ExitCode::Toolchain,
|
||||
&invocation,
|
||||
invocation,
|
||||
PrimaryOutput::Diagnostics {
|
||||
text: message.as_str(),
|
||||
},
|
||||
@ -650,7 +971,7 @@ fn run_build_from_llvm(
|
||||
&message,
|
||||
"InputReadFailed",
|
||||
ExitCode::SourceFailure,
|
||||
&invocation,
|
||||
invocation,
|
||||
PrimaryOutput::Diagnostics {
|
||||
text: message.as_str(),
|
||||
},
|
||||
@ -659,12 +980,11 @@ fn run_build_from_llvm(
|
||||
}
|
||||
}
|
||||
|
||||
let output = Path::new(output_path);
|
||||
let output_dir = output
|
||||
let output_dir = output_path
|
||||
.parent()
|
||||
.filter(|parent| !parent.as_os_str().is_empty())
|
||||
.unwrap_or_else(|| Path::new("."));
|
||||
let stem = output
|
||||
let stem = output_path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("glagol-output");
|
||||
@ -682,7 +1002,7 @@ fn run_build_from_llvm(
|
||||
&message,
|
||||
"OutputWriteFailed",
|
||||
ExitCode::ArtifactFailure,
|
||||
&invocation,
|
||||
invocation,
|
||||
PrimaryOutput::Diagnostics {
|
||||
text: message.as_str(),
|
||||
},
|
||||
@ -707,7 +1027,7 @@ fn run_build_from_llvm(
|
||||
&message,
|
||||
"ToolchainUnavailable",
|
||||
ExitCode::Toolchain,
|
||||
&invocation,
|
||||
invocation,
|
||||
PrimaryOutput::Diagnostics {
|
||||
text: message.as_str(),
|
||||
},
|
||||
@ -721,7 +1041,7 @@ fn run_build_from_llvm(
|
||||
&message,
|
||||
"ToolchainUnavailable",
|
||||
ExitCode::Toolchain,
|
||||
&invocation,
|
||||
invocation,
|
||||
PrimaryOutput::Diagnostics {
|
||||
text: message.as_str(),
|
||||
},
|
||||
@ -747,7 +1067,7 @@ fn run_build_from_llvm(
|
||||
&message,
|
||||
"ToolchainUnavailable",
|
||||
ExitCode::Toolchain,
|
||||
&invocation,
|
||||
invocation,
|
||||
PrimaryOutput::Diagnostics {
|
||||
text: message.as_str(),
|
||||
},
|
||||
@ -758,12 +1078,12 @@ fn run_build_from_llvm(
|
||||
if let Err(err) = fs::rename(&temp_binary, output_path) {
|
||||
let _ = fs::remove_file(&temp_llvm);
|
||||
let _ = fs::remove_file(&temp_binary);
|
||||
let message = format!("cannot write `{}`: {}", output_path, err);
|
||||
let message = format!("cannot write `{}`: {}", output_path.display(), err);
|
||||
emit_message_diagnostic(
|
||||
&message,
|
||||
"OutputWriteFailed",
|
||||
ExitCode::ArtifactFailure,
|
||||
&invocation,
|
||||
invocation,
|
||||
PrimaryOutput::Diagnostics {
|
||||
text: message.as_str(),
|
||||
},
|
||||
@ -772,23 +1092,11 @@ fn run_build_from_llvm(
|
||||
}
|
||||
|
||||
let _ = fs::remove_file(&temp_llvm);
|
||||
write_manifest_if_requested_with_foreign_imports(
|
||||
&invocation,
|
||||
true,
|
||||
PrimaryOutput::Path {
|
||||
kind: "native-executable",
|
||||
path: output_path,
|
||||
},
|
||||
None,
|
||||
Some(BuildInfo {
|
||||
clang: &clang,
|
||||
runtime: &runtime,
|
||||
c_inputs: &invocation.link_c_paths,
|
||||
}),
|
||||
&foreign_imports,
|
||||
project_artifact.as_ref(),
|
||||
);
|
||||
process::exit(0);
|
||||
NativeBuild {
|
||||
output_path: output_path.to_path_buf(),
|
||||
clang,
|
||||
runtime,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -809,8 +1117,11 @@ struct Invocation {
|
||||
command_line: String,
|
||||
fmt_action: FmtAction,
|
||||
project_name: Option<String>,
|
||||
project_template: scaffold::ProjectTemplate,
|
||||
link_c_paths: Vec<String>,
|
||||
test_filter: Option<String>,
|
||||
test_list: bool,
|
||||
run_args: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
@ -843,8 +1154,11 @@ fn parse_args(raw_args: &[String]) -> Result<Args, ParseError> {
|
||||
let mut diagnostics = DiagnosticFormat::TextAndSexpr;
|
||||
let mut fmt_action = FmtAction::Stdout;
|
||||
let mut project_name = None;
|
||||
let mut project_template = scaffold::ProjectTemplate::Binary;
|
||||
let mut link_c_paths = Vec::new();
|
||||
let mut test_filter = None;
|
||||
let mut test_list = false;
|
||||
let mut run_args = Vec::new();
|
||||
let mut no_color = false;
|
||||
let command_line = raw_args.join(" ");
|
||||
let mut iter = raw_args
|
||||
@ -855,6 +1169,11 @@ fn parse_args(raw_args: &[String]) -> Result<Args, ParseError> {
|
||||
.into_iter();
|
||||
|
||||
while let Some(arg) = iter.next() {
|
||||
if arg == "--" {
|
||||
run_args.extend(iter);
|
||||
break;
|
||||
}
|
||||
|
||||
match arg.as_str() {
|
||||
"-h" | "--help" => return Ok(Args::Help),
|
||||
"--version" => return Ok(Args::Version),
|
||||
@ -987,6 +1306,26 @@ fn parse_args(raw_args: &[String]) -> Result<Args, ParseError> {
|
||||
command_line: command_line.clone(),
|
||||
})?);
|
||||
}
|
||||
"--template" => {
|
||||
let value = iter.next().ok_or_else(|| ParseError {
|
||||
message: "`--template` requires one of: binary, library, workspace".to_string(),
|
||||
manifest_path: manifest_path.clone(),
|
||||
diagnostics,
|
||||
command_line: command_line.clone(),
|
||||
})?;
|
||||
let Some(parsed) = scaffold::ProjectTemplate::parse(&value) else {
|
||||
return parse_error(
|
||||
format!(
|
||||
"unsupported project template `{}`; expected binary, library, or workspace",
|
||||
value
|
||||
),
|
||||
manifest_path,
|
||||
diagnostics,
|
||||
command_line,
|
||||
);
|
||||
};
|
||||
project_template = parsed;
|
||||
}
|
||||
"--manifest" => {
|
||||
if manifest_path.is_some() {
|
||||
return parse_error(
|
||||
@ -1027,14 +1366,30 @@ fn parse_args(raw_args: &[String]) -> Result<Args, ParseError> {
|
||||
command_line: command_line.clone(),
|
||||
})?);
|
||||
}
|
||||
"check" | "fmt" | "test" | "build" | "new" | "doc" if path.is_none() => {
|
||||
"--list" => {
|
||||
if test_list {
|
||||
return parse_error(
|
||||
"`--list` was provided more than once",
|
||||
manifest_path,
|
||||
diagnostics,
|
||||
command_line,
|
||||
);
|
||||
}
|
||||
test_list = true;
|
||||
}
|
||||
"check" | "fmt" | "test" | "build" | "run" | "clean" | "new" | "doc" | "symbols"
|
||||
if path.is_none() =>
|
||||
{
|
||||
let next = match arg.as_str() {
|
||||
"check" => Mode::Check,
|
||||
"fmt" => Mode::Format,
|
||||
"test" => Mode::RunTests,
|
||||
"build" => Mode::Build,
|
||||
"run" => Mode::Run,
|
||||
"clean" => Mode::Clean,
|
||||
"new" => Mode::New,
|
||||
"doc" => Mode::Doc,
|
||||
"symbols" => Mode::Symbols,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
set_mode(
|
||||
@ -1114,9 +1469,18 @@ fn parse_args(raw_args: &[String]) -> Result<Args, ParseError> {
|
||||
);
|
||||
}
|
||||
|
||||
if !link_c_paths.is_empty() && mode != Mode::Build {
|
||||
if project_template != scaffold::ProjectTemplate::Binary && mode != Mode::New {
|
||||
return parse_error(
|
||||
"`--link-c` is only supported with `build`",
|
||||
"`--template` is only supported with `new`",
|
||||
manifest_path,
|
||||
diagnostics,
|
||||
command_line,
|
||||
);
|
||||
}
|
||||
|
||||
if !link_c_paths.is_empty() && !matches!(mode, Mode::Build | Mode::Run) {
|
||||
return parse_error(
|
||||
"`--link-c` is only supported with `build` and `run`",
|
||||
manifest_path,
|
||||
diagnostics,
|
||||
command_line,
|
||||
@ -1132,6 +1496,33 @@ fn parse_args(raw_args: &[String]) -> Result<Args, ParseError> {
|
||||
);
|
||||
}
|
||||
|
||||
if test_list && mode != Mode::RunTests {
|
||||
return parse_error(
|
||||
"`--list` is only supported with `test` and `--run-tests`",
|
||||
manifest_path,
|
||||
diagnostics,
|
||||
command_line,
|
||||
);
|
||||
}
|
||||
|
||||
if !run_args.is_empty() && mode != Mode::Run {
|
||||
return parse_error(
|
||||
"`--` program arguments are only supported with `run`",
|
||||
manifest_path,
|
||||
diagnostics,
|
||||
command_line,
|
||||
);
|
||||
}
|
||||
|
||||
if mode == Mode::Clean && output_path.is_some() {
|
||||
return parse_error(
|
||||
"`clean` does not support `-o`",
|
||||
manifest_path,
|
||||
diagnostics,
|
||||
command_line,
|
||||
);
|
||||
}
|
||||
|
||||
if mode == Mode::Doc && output_path.is_none() {
|
||||
return parse_error(
|
||||
"`doc` requires `-o <dir>`",
|
||||
@ -1162,8 +1553,11 @@ fn parse_args(raw_args: &[String]) -> Result<Args, ParseError> {
|
||||
command_line,
|
||||
fmt_action,
|
||||
project_name,
|
||||
project_template,
|
||||
link_c_paths,
|
||||
test_filter,
|
||||
test_list,
|
||||
run_args,
|
||||
}))
|
||||
}
|
||||
|
||||
@ -1233,8 +1627,11 @@ fn exit_parse_error(err: ParseError, command_line: &str) -> ! {
|
||||
},
|
||||
fmt_action: FmtAction::Stdout,
|
||||
project_name: None,
|
||||
project_template: scaffold::ProjectTemplate::Binary,
|
||||
link_c_paths: Vec::new(),
|
||||
test_filter: None,
|
||||
test_list: false,
|
||||
run_args: Vec::new(),
|
||||
};
|
||||
write_manifest_or_exit(
|
||||
manifest_path,
|
||||
@ -1246,6 +1643,7 @@ fn exit_parse_error(err: ParseError, command_line: &str) -> ! {
|
||||
PrimaryOutput::Diagnostics { text: &stderr },
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
&[],
|
||||
None,
|
||||
err.diagnostics,
|
||||
@ -1267,8 +1665,11 @@ enum Mode {
|
||||
CheckTests,
|
||||
RunTests,
|
||||
Build,
|
||||
Run,
|
||||
Clean,
|
||||
New,
|
||||
Doc,
|
||||
Symbols,
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
@ -1283,8 +1684,11 @@ impl Mode {
|
||||
Self::CheckTests => "check-tests",
|
||||
Self::RunTests => "test",
|
||||
Self::Build => "build",
|
||||
Self::Run => "run",
|
||||
Self::Clean => "clean",
|
||||
Self::New => "new",
|
||||
Self::Doc => "doc",
|
||||
Self::Symbols => "symbols",
|
||||
}
|
||||
}
|
||||
|
||||
@ -1297,8 +1701,11 @@ impl Mode {
|
||||
Self::InspectLoweringSurface | Self::InspectLoweringChecked => "lowering-inspector",
|
||||
Self::CheckTests | Self::RunTests => "stdout",
|
||||
Self::Build => "native-executable",
|
||||
Self::Run => "program-stdout",
|
||||
Self::Clean => "no-output",
|
||||
Self::New => "no-output",
|
||||
Self::Doc => "documentation",
|
||||
Self::Symbols => "symbols",
|
||||
}
|
||||
}
|
||||
|
||||
@ -1490,6 +1897,13 @@ struct TestSummary {
|
||||
filter: Option<String>,
|
||||
}
|
||||
|
||||
struct RunReport<'a> {
|
||||
exit_status: Option<i32>,
|
||||
stdout: &'a str,
|
||||
stderr: &'a str,
|
||||
args: &'a [String],
|
||||
}
|
||||
|
||||
fn test_summary_from_report(report: test_runner::TestReport) -> TestSummary {
|
||||
TestSummary {
|
||||
total_discovered: report.total_discovered,
|
||||
@ -1592,6 +2006,34 @@ fn write_manifest_if_requested_with_foreign_imports(
|
||||
success,
|
||||
primary_output,
|
||||
test_summary,
|
||||
None,
|
||||
build_info,
|
||||
foreign_imports,
|
||||
project_info,
|
||||
invocation.diagnostics,
|
||||
);
|
||||
write_manifest_or_exit(manifest_path, &manifest);
|
||||
}
|
||||
}
|
||||
|
||||
fn write_manifest_if_requested_with_run_report(
|
||||
invocation: &Invocation,
|
||||
success: bool,
|
||||
primary_output: PrimaryOutput<'_>,
|
||||
build_info: Option<BuildInfo<'_>>,
|
||||
foreign_imports: &[project::ProjectArtifactCImport],
|
||||
project_info: Option<&project::ProjectArtifact>,
|
||||
run_report: RunReport<'_>,
|
||||
) {
|
||||
if let Some(manifest_path) = invocation.manifest_path.as_deref() {
|
||||
let manifest = render_manifest(
|
||||
Some(&invocation.path),
|
||||
&invocation.command_line,
|
||||
Some(invocation.manifest_mode_name.as_str()),
|
||||
success,
|
||||
primary_output,
|
||||
None,
|
||||
Some(run_report),
|
||||
build_info,
|
||||
foreign_imports,
|
||||
project_info,
|
||||
@ -1615,6 +2057,7 @@ fn render_manifest(
|
||||
success: bool,
|
||||
primary_output: PrimaryOutput<'_>,
|
||||
test_summary: Option<TestSummary>,
|
||||
run_report: Option<RunReport<'_>>,
|
||||
build_info: Option<BuildInfo<'_>>,
|
||||
foreign_imports: &[project::ProjectArtifactCImport],
|
||||
project_info: Option<&project::ProjectArtifact>,
|
||||
@ -1638,7 +2081,10 @@ fn render_manifest(
|
||||
" (success {})\n",
|
||||
if success { "true" } else { "false" }
|
||||
));
|
||||
out.push_str(" (diagnostics-schema-version 1)\n");
|
||||
out.push_str(&format!(
|
||||
" (diagnostics-schema-version {})\n",
|
||||
diag::DIAGNOSTIC_SCHEMA_VERSION
|
||||
));
|
||||
out.push_str(&format!(
|
||||
" (diagnostics-encoding {})\n",
|
||||
match diagnostics {
|
||||
@ -1713,6 +2159,34 @@ fn render_manifest(
|
||||
out.push_str(" )");
|
||||
}
|
||||
|
||||
if let Some(report) = run_report {
|
||||
out.push('\n');
|
||||
out.push_str(" (run-report\n");
|
||||
match report.exit_status {
|
||||
Some(status) => out.push_str(&format!(" (exit-status {})\n", status)),
|
||||
None => out.push_str(" (exit-status null)\n"),
|
||||
}
|
||||
out.push_str(&format!(
|
||||
" (stdout {})\n",
|
||||
diag::render_string(report.stdout)
|
||||
));
|
||||
out.push_str(&format!(
|
||||
" (stderr {})\n",
|
||||
diag::render_string(report.stderr)
|
||||
));
|
||||
out.push_str(" (args");
|
||||
if report.args.is_empty() {
|
||||
out.push_str(")\n");
|
||||
} else {
|
||||
out.push('\n');
|
||||
for arg in report.args {
|
||||
out.push_str(&format!(" (arg {})\n", diag::render_string(arg)));
|
||||
}
|
||||
out.push_str(" )\n");
|
||||
}
|
||||
out.push_str(" )");
|
||||
}
|
||||
|
||||
if let Some(build) = build_info {
|
||||
out.push('\n');
|
||||
out.push_str(" (hosted-build\n");
|
||||
@ -1998,9 +2472,108 @@ fn parse_test_count(output: &str, suffix: &str) -> Option<usize> {
|
||||
}
|
||||
|
||||
fn runtime_path() -> PathBuf {
|
||||
runtime_path_candidates()
|
||||
.into_iter()
|
||||
.find(|path| path.is_file())
|
||||
.unwrap_or_else(checkout_runtime_path)
|
||||
}
|
||||
|
||||
fn runtime_path_candidates() -> Vec<PathBuf> {
|
||||
let mut candidates = Vec::new();
|
||||
if let Some(path) = env::var_os("SLOVO_RUNTIME_C") {
|
||||
candidates.push(PathBuf::from(path));
|
||||
}
|
||||
if let Some(path) = env::var_os("GLAGOL_RUNTIME_C") {
|
||||
candidates.push(PathBuf::from(path));
|
||||
}
|
||||
if let Ok(exe) = env::current_exe() {
|
||||
if let Some(bin_dir) = exe.parent() {
|
||||
candidates.push(bin_dir.join("runtime/runtime.c"));
|
||||
candidates.push(bin_dir.join("../runtime/runtime.c"));
|
||||
candidates.push(bin_dir.join("../share/slovo/runtime/runtime.c"));
|
||||
}
|
||||
}
|
||||
candidates.push(checkout_runtime_path());
|
||||
candidates
|
||||
}
|
||||
|
||||
fn checkout_runtime_path() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../runtime/runtime.c")
|
||||
}
|
||||
|
||||
fn run_output_path(
|
||||
invocation: &Invocation,
|
||||
project_artifact: Option<&project::ProjectArtifact>,
|
||||
) -> PathBuf {
|
||||
if let Some(output_path) = invocation.output_path.as_deref() {
|
||||
return PathBuf::from(output_path);
|
||||
}
|
||||
|
||||
let stem = project_artifact
|
||||
.map(|artifact| artifact.project_name.as_str())
|
||||
.or_else(|| {
|
||||
Path::new(&invocation.path)
|
||||
.file_stem()
|
||||
.and_then(|stem| stem.to_str())
|
||||
})
|
||||
.map(sanitize_output_stem)
|
||||
.filter(|stem| !stem.is_empty())
|
||||
.unwrap_or_else(|| "slovo-program".to_string());
|
||||
generated_build_dir(&invocation.path).join(format!("{}{}", stem, env::consts::EXE_SUFFIX))
|
||||
}
|
||||
|
||||
fn generated_build_dir(input: &str) -> PathBuf {
|
||||
generated_build_root(input).join(".slovo").join("build")
|
||||
}
|
||||
|
||||
fn generated_build_root(input: &str) -> PathBuf {
|
||||
let path = Path::new(input);
|
||||
if project::is_project_input(input) {
|
||||
if path.is_dir() {
|
||||
return path.to_path_buf();
|
||||
}
|
||||
return path
|
||||
.parent()
|
||||
.filter(|parent| !parent.as_os_str().is_empty())
|
||||
.unwrap_or_else(|| Path::new("."))
|
||||
.to_path_buf();
|
||||
}
|
||||
path.parent()
|
||||
.filter(|parent| !parent.as_os_str().is_empty())
|
||||
.unwrap_or_else(|| Path::new("."))
|
||||
.to_path_buf()
|
||||
}
|
||||
|
||||
fn sanitize_output_stem(value: &str) -> String {
|
||||
let mut out = String::new();
|
||||
let mut previous_dash = false;
|
||||
for ch in value.chars().flat_map(char::to_lowercase) {
|
||||
let mapped = if ch.is_ascii_lowercase() || ch.is_ascii_digit() {
|
||||
Some(ch)
|
||||
} else if ch == '-' || ch == '_' || ch == '.' || ch.is_whitespace() {
|
||||
Some('-')
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let Some(ch) = mapped else {
|
||||
continue;
|
||||
};
|
||||
if ch == '-' {
|
||||
if !out.is_empty() && !previous_dash {
|
||||
out.push('-');
|
||||
previous_dash = true;
|
||||
}
|
||||
} else {
|
||||
out.push(ch);
|
||||
previous_dash = false;
|
||||
}
|
||||
}
|
||||
while out.ends_with('-') {
|
||||
out.pop();
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn c_imports_for_manifest(file: &str, source: &str) -> Vec<project::ProjectArtifactCImport> {
|
||||
let Ok(tokens) = lexer::lex(file, source) else {
|
||||
return Vec::new();
|
||||
@ -2102,6 +2675,6 @@ fn normalized_output_path(path: &str) -> Option<PathBuf> {
|
||||
|
||||
fn print_usage() {
|
||||
eprintln!(
|
||||
"usage: glagol [check|fmt|test|build] [--json-diagnostics] [--no-color] [--manifest <path>] [--link-c <path>] [-o <path>] [--filter <substring>] <file.slo|project>\n glagol fmt [--check|--write] <file.slo|project>\n glagol new <project-dir> [--name <name>]\n glagol doc <file.slo|project> -o <dir>\n glagol [--emit=llvm|--format|--print-tree|--inspect-lowering=surface|--inspect-lowering=checked|--check-tests|--run-tests] [--json-diagnostics] [--no-color] [-o <path>] [--manifest <path>] [--filter <substring>] <file.slo>\n glagol --version"
|
||||
"usage: glagol [check|fmt|test|build|run|clean|symbols] [--json-diagnostics] [--no-color] [--manifest <path>] [--link-c <path>] [-o <path>] [--filter <substring>] [--list] <file.slo|project> [-- <program-args>...]\n glagol fmt [--check|--write] <file.slo|project>\n glagol new <project-dir> [--name <name>] [--template binary|library|workspace]\n glagol doc <file.slo|project> -o <dir>\n glagol symbols <file.slo|project|workspace>\n glagol [--emit=llvm|--format|--print-tree|--inspect-lowering=surface|--inspect-lowering=checked|--check-tests|--run-tests] [--json-diagnostics] [--no-color] [-o <path>] [--manifest <path>] [--filter <substring>] [--list] <file.slo>\n glagol --version"
|
||||
);
|
||||
}
|
||||
|
||||
@ -130,6 +130,7 @@ struct WorkspaceManifest {
|
||||
source: String,
|
||||
root: PathBuf,
|
||||
members: Vec<String>,
|
||||
default_package: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -176,6 +177,7 @@ struct ModuleUnit {
|
||||
local_functions: HashMap<String, DeclInfo>,
|
||||
local_structs: HashMap<String, DeclInfo>,
|
||||
local_enums: HashMap<String, DeclInfo>,
|
||||
local_aliases: HashMap<String, DeclInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -203,6 +205,7 @@ enum DeclKind {
|
||||
CImport,
|
||||
Struct,
|
||||
Enum,
|
||||
TypeAlias,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -449,6 +452,26 @@ pub fn run_tests(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list_tests(
|
||||
input: &str,
|
||||
filter: Option<&str>,
|
||||
) -> Result<ProjectTestSuccess, ProjectTestFailure> {
|
||||
let checked = load_checked_project(input, false).map_err(|failure| ProjectTestFailure {
|
||||
diagnostics: failure.diagnostics,
|
||||
report: None,
|
||||
sources: failure.sources,
|
||||
artifact: failure.artifact,
|
||||
})?;
|
||||
|
||||
let success = test_runner::list(&checked.program, filter);
|
||||
Ok(ProjectTestSuccess {
|
||||
output: success.output,
|
||||
report: success.report,
|
||||
sources: checked.sources,
|
||||
artifact: checked.artifact,
|
||||
})
|
||||
}
|
||||
|
||||
struct CheckedProject {
|
||||
program: CheckedProgram,
|
||||
sources: Vec<SourceFile>,
|
||||
@ -1101,7 +1124,16 @@ fn parse_manifest(path: PathBuf, source: String) -> Result<Manifest, Vec<Diagnos
|
||||
};
|
||||
|
||||
match key {
|
||||
"name" => set_manifest_key(&file, &mut errors, &mut name, parsed, line.span, "name"),
|
||||
"name" => set_manifest_key(
|
||||
&file,
|
||||
&mut errors,
|
||||
&mut name,
|
||||
parsed,
|
||||
line.span,
|
||||
"name",
|
||||
"ProjectManifestInvalid",
|
||||
"project",
|
||||
),
|
||||
"source_root" => set_manifest_key(
|
||||
&file,
|
||||
&mut errors,
|
||||
@ -1109,8 +1141,19 @@ fn parse_manifest(path: PathBuf, source: String) -> Result<Manifest, Vec<Diagnos
|
||||
parsed,
|
||||
line.span,
|
||||
"source_root",
|
||||
"ProjectManifestInvalid",
|
||||
"project",
|
||||
),
|
||||
"entry" => set_manifest_key(
|
||||
&file,
|
||||
&mut errors,
|
||||
&mut entry,
|
||||
parsed,
|
||||
line.span,
|
||||
"entry",
|
||||
"ProjectManifestInvalid",
|
||||
"project",
|
||||
),
|
||||
"entry" => set_manifest_key(&file, &mut errors, &mut entry, parsed, line.span, "entry"),
|
||||
_ => errors.push(
|
||||
Diagnostic::new(
|
||||
&file,
|
||||
@ -1194,6 +1237,7 @@ fn parse_workspace_manifest(
|
||||
let mut in_workspace = false;
|
||||
let mut saw_workspace = false;
|
||||
let mut members = None::<Vec<String>>;
|
||||
let mut default_package = None::<String>;
|
||||
|
||||
for line in manifest_lines(&source) {
|
||||
let trimmed = line.text.trim();
|
||||
@ -1264,6 +1308,28 @@ fn parse_workspace_manifest(
|
||||
.with_span(line.span),
|
||||
),
|
||||
},
|
||||
"default_package" => match parse_manifest_string(value.trim()) {
|
||||
Some(parsed) => {
|
||||
if default_package.replace(parsed).is_some() {
|
||||
errors.push(
|
||||
Diagnostic::new(
|
||||
&file,
|
||||
"WorkspaceManifestInvalid",
|
||||
"duplicate workspace manifest key `default_package`",
|
||||
)
|
||||
.with_span(line.span),
|
||||
);
|
||||
}
|
||||
}
|
||||
None => errors.push(
|
||||
Diagnostic::new(
|
||||
&file,
|
||||
"WorkspaceManifestInvalid",
|
||||
"workspace default_package must be a string",
|
||||
)
|
||||
.with_span(line.span),
|
||||
),
|
||||
},
|
||||
other => errors.push(
|
||||
Diagnostic::new(
|
||||
&file,
|
||||
@ -1292,8 +1358,22 @@ fn parse_workspace_manifest(
|
||||
Vec::new()
|
||||
});
|
||||
|
||||
for member in &members {
|
||||
if normalize_workspace_member(member).is_none() {
|
||||
let mut normalized_members = Vec::new();
|
||||
let mut seen_members = BTreeMap::<String, String>::new();
|
||||
for member in members {
|
||||
if let Some(normalized) = normalize_workspace_member(&member) {
|
||||
if let Some(original) = seen_members.insert(normalized.clone(), member.clone()) {
|
||||
errors.push(Diagnostic::new(
|
||||
&file,
|
||||
"DuplicateWorkspaceMember",
|
||||
format!(
|
||||
"workspace member path `{}` duplicates `{}` after normalization",
|
||||
member, original
|
||||
),
|
||||
));
|
||||
}
|
||||
normalized_members.push(normalized);
|
||||
} else {
|
||||
errors.push(Diagnostic::new(
|
||||
&file,
|
||||
"WorkspaceMemberPathEscape",
|
||||
@ -1304,12 +1384,19 @@ fn parse_workspace_manifest(
|
||||
));
|
||||
}
|
||||
}
|
||||
members = members
|
||||
.into_iter()
|
||||
.filter_map(|member| normalize_workspace_member(&member))
|
||||
.collect();
|
||||
members = normalized_members;
|
||||
members.sort();
|
||||
|
||||
if let Some(default_package) = &default_package {
|
||||
if !is_project_name(default_package) {
|
||||
errors.push(Diagnostic::new(
|
||||
&file,
|
||||
"WorkspaceManifestInvalid",
|
||||
"workspace default_package must be a package name",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if !errors.is_empty() {
|
||||
return Err(errors);
|
||||
}
|
||||
@ -1325,6 +1412,7 @@ fn parse_workspace_manifest(
|
||||
source,
|
||||
root,
|
||||
members,
|
||||
default_package,
|
||||
})
|
||||
}
|
||||
|
||||
@ -1344,6 +1432,7 @@ fn parse_package_manifest(
|
||||
let mut source_root = None::<String>;
|
||||
let mut entry = None::<String>;
|
||||
let mut dependencies = Vec::new();
|
||||
let mut dependency_keys = BTreeSet::<String>::new();
|
||||
|
||||
for line in manifest_lines(&source) {
|
||||
let trimmed = line.text.trim();
|
||||
@ -1421,6 +1510,8 @@ fn parse_package_manifest(
|
||||
parsed,
|
||||
line.span,
|
||||
"name",
|
||||
"PackageManifestInvalid",
|
||||
"package",
|
||||
),
|
||||
"version" => set_manifest_key(
|
||||
&file,
|
||||
@ -1429,6 +1520,8 @@ fn parse_package_manifest(
|
||||
parsed,
|
||||
line.span,
|
||||
"version",
|
||||
"PackageManifestInvalid",
|
||||
"package",
|
||||
),
|
||||
"source_root" => set_manifest_key(
|
||||
&file,
|
||||
@ -1437,6 +1530,8 @@ fn parse_package_manifest(
|
||||
parsed,
|
||||
line.span,
|
||||
"source_root",
|
||||
"PackageManifestInvalid",
|
||||
"package",
|
||||
),
|
||||
"entry" => set_manifest_key(
|
||||
&file,
|
||||
@ -1445,6 +1540,8 @@ fn parse_package_manifest(
|
||||
parsed,
|
||||
line.span,
|
||||
"entry",
|
||||
"PackageManifestInvalid",
|
||||
"package",
|
||||
),
|
||||
other => errors.push(
|
||||
Diagnostic::new(
|
||||
@ -1456,21 +1553,54 @@ fn parse_package_manifest(
|
||||
),
|
||||
}
|
||||
}
|
||||
"dependencies" => match parse_dependency_path(value) {
|
||||
Some(path) => dependencies.push(PackageDependency {
|
||||
key: key.to_string(),
|
||||
path,
|
||||
span: line.span,
|
||||
}),
|
||||
None => errors.push(
|
||||
Diagnostic::new(
|
||||
&file,
|
||||
"UnsupportedDependency",
|
||||
"workspace dependencies must use `{ path = \"...\" }` local path records only",
|
||||
)
|
||||
.with_span(line.span),
|
||||
),
|
||||
},
|
||||
"dependencies" => {
|
||||
let valid_key = if is_project_name(key) {
|
||||
true
|
||||
} else {
|
||||
errors.push(
|
||||
Diagnostic::new(
|
||||
&file,
|
||||
"InvalidPackageDependencyName",
|
||||
format!(
|
||||
"package dependency name `{}` must start with `a-z` and contain only `a-z`, `0-9`, and `-`",
|
||||
key
|
||||
),
|
||||
)
|
||||
.with_span(line.span),
|
||||
);
|
||||
false
|
||||
};
|
||||
let unique_key = if dependency_keys.insert(key.to_string()) {
|
||||
true
|
||||
} else {
|
||||
errors.push(
|
||||
Diagnostic::new(
|
||||
&file,
|
||||
"DuplicatePackageDependencyName",
|
||||
format!("duplicate package dependency name `{}`", key),
|
||||
)
|
||||
.with_span(line.span),
|
||||
);
|
||||
false
|
||||
};
|
||||
|
||||
match parse_dependency_path(value) {
|
||||
Some(path) if valid_key && unique_key => dependencies.push(PackageDependency {
|
||||
key: key.to_string(),
|
||||
path,
|
||||
span: line.span,
|
||||
}),
|
||||
Some(_) => {}
|
||||
None => errors.push(
|
||||
Diagnostic::new(
|
||||
&file,
|
||||
"UnsupportedDependency",
|
||||
"workspace dependencies must use `{ path = \"...\" }` local path records only",
|
||||
)
|
||||
.with_span(line.span),
|
||||
),
|
||||
}
|
||||
}
|
||||
"" | "project" => errors.push(
|
||||
Diagnostic::new(
|
||||
&file,
|
||||
@ -1626,13 +1756,15 @@ fn set_manifest_key(
|
||||
value: String,
|
||||
span: Span,
|
||||
key: &str,
|
||||
code: &'static str,
|
||||
manifest_kind: &str,
|
||||
) {
|
||||
if slot.replace(value).is_some() {
|
||||
errors.push(
|
||||
Diagnostic::new(
|
||||
file,
|
||||
"ProjectManifestInvalid",
|
||||
format!("duplicate project manifest key `{}`", key),
|
||||
code,
|
||||
format!("duplicate {} manifest key `{}`", manifest_kind, key),
|
||||
)
|
||||
.with_span(span),
|
||||
);
|
||||
@ -2258,6 +2390,7 @@ fn parse_module(path: &Path, file: String, source: String) -> Result<ModuleUnit,
|
||||
errors.append(&mut errs);
|
||||
Program {
|
||||
module: name.clone(),
|
||||
type_aliases: Vec::new(),
|
||||
enums: Vec::new(),
|
||||
structs: Vec::new(),
|
||||
c_imports: Vec::new(),
|
||||
@ -2267,7 +2400,7 @@ fn parse_module(path: &Path, file: String, source: String) -> Result<ModuleUnit,
|
||||
}
|
||||
};
|
||||
|
||||
let (local_functions, local_structs, local_enums, mut duplicate_errors) =
|
||||
let (local_functions, local_structs, local_enums, local_aliases, mut duplicate_errors) =
|
||||
local_declarations(&file, &program);
|
||||
errors.append(&mut duplicate_errors);
|
||||
|
||||
@ -2281,6 +2414,7 @@ fn parse_module(path: &Path, file: String, source: String) -> Result<ModuleUnit,
|
||||
local_functions,
|
||||
local_structs,
|
||||
local_enums,
|
||||
local_aliases,
|
||||
})
|
||||
} else {
|
||||
Err(errors)
|
||||
@ -2575,6 +2709,44 @@ fn external_enum_from_module(module: &ModuleUnit, name: &str) -> Option<External
|
||||
})
|
||||
}
|
||||
|
||||
fn external_enum_from_checked_module(
|
||||
module: &ModuleUnit,
|
||||
checked: &CheckedProgram,
|
||||
name: &str,
|
||||
) -> Option<ExternalEnum> {
|
||||
let source_enum = module
|
||||
.program
|
||||
.enums
|
||||
.iter()
|
||||
.find(|enum_decl| enum_decl.name == name)?;
|
||||
let checked_enum = checked
|
||||
.enums
|
||||
.iter()
|
||||
.find(|enum_decl| enum_decl.name == name)?;
|
||||
|
||||
Some(ExternalEnum {
|
||||
name: name.to_string(),
|
||||
span: source_enum.name_span,
|
||||
variants: checked_enum
|
||||
.variants
|
||||
.iter()
|
||||
.map(|checked_variant| {
|
||||
let source_variant = source_enum
|
||||
.variants
|
||||
.iter()
|
||||
.find(|variant| variant.name == checked_variant.name);
|
||||
ExternalEnumVariant {
|
||||
name: checked_variant.name.clone(),
|
||||
name_span: source_variant
|
||||
.map_or(source_enum.name_span, |variant| variant.name_span),
|
||||
payload_ty: checked_variant.payload_ty.clone(),
|
||||
payload_ty_span: source_variant.and_then(|variant| variant.payload_ty_span),
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
fn local_declarations(
|
||||
file: &str,
|
||||
program: &Program,
|
||||
@ -2582,14 +2754,44 @@ fn local_declarations(
|
||||
HashMap<String, DeclInfo>,
|
||||
HashMap<String, DeclInfo>,
|
||||
HashMap<String, DeclInfo>,
|
||||
HashMap<String, DeclInfo>,
|
||||
Vec<Diagnostic>,
|
||||
) {
|
||||
let mut all = HashMap::<String, DeclInfo>::new();
|
||||
let mut functions = HashMap::new();
|
||||
let mut structs = HashMap::new();
|
||||
let mut enums = HashMap::new();
|
||||
let mut aliases = HashMap::new();
|
||||
let mut errors = Vec::new();
|
||||
|
||||
for alias in &program.type_aliases {
|
||||
let decl = DeclInfo {
|
||||
span: alias.name_span,
|
||||
kind: DeclKind::TypeAlias,
|
||||
};
|
||||
if let Some(original) = all.insert(alias.name.clone(), decl.clone()) {
|
||||
if original.kind == DeclKind::CImport {
|
||||
errors.push(
|
||||
Diagnostic::new(
|
||||
file,
|
||||
"DuplicateTopLevelName",
|
||||
format!("duplicate top-level name `{}`", alias.name),
|
||||
)
|
||||
.with_span(alias.name_span)
|
||||
.related("original declaration", original.span),
|
||||
);
|
||||
} else {
|
||||
errors.push(duplicate_name(
|
||||
file,
|
||||
&alias.name,
|
||||
alias.name_span,
|
||||
original.span,
|
||||
));
|
||||
}
|
||||
}
|
||||
aliases.insert(alias.name.clone(), decl);
|
||||
}
|
||||
|
||||
for function in &program.functions {
|
||||
let decl = DeclInfo {
|
||||
span: function.span,
|
||||
@ -2721,7 +2923,7 @@ fn local_declarations(
|
||||
enums.insert(enum_decl.name.clone(), decl);
|
||||
}
|
||||
|
||||
(functions, structs, enums, errors)
|
||||
(functions, structs, enums, aliases, errors)
|
||||
}
|
||||
|
||||
fn validate_workspace_packages(
|
||||
@ -2750,6 +2952,18 @@ fn validate_workspace_packages(
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(default_package) = &workspace.default_package {
|
||||
if !by_name.contains_key(default_package) {
|
||||
diagnostics.push(Diagnostic::new(
|
||||
&workspace.path.display().to_string(),
|
||||
"WorkspaceDefaultPackageMissing",
|
||||
format!(
|
||||
"workspace default_package `{}` is not a workspace package",
|
||||
default_package
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let mut resolved_dependencies = vec![Vec::<usize>::new(); packages.len()];
|
||||
for (package_index, package) in packages.iter().enumerate() {
|
||||
@ -2970,9 +3184,20 @@ fn resolve_and_check_workspace(
|
||||
let mut external_enums = Vec::new();
|
||||
for (name, binding) in imports {
|
||||
let provider = &packages[binding.provider_package].modules[binding.provider_module];
|
||||
let checked_provider =
|
||||
checked_by_module.get(&(binding.provider_package, provider.name.clone()));
|
||||
match binding.kind {
|
||||
DeclKind::Function => {
|
||||
if let Some(function) =
|
||||
if let Some(function) = checked_provider
|
||||
.and_then(|program| program.functions.iter().find(|f| f.name == *name))
|
||||
{
|
||||
external_functions.push(ExternalFunction {
|
||||
name: name.clone(),
|
||||
params: function.params.iter().map(|(_, ty)| ty.clone()).collect(),
|
||||
return_type: function.return_type.clone(),
|
||||
foreign: false,
|
||||
});
|
||||
} else if let Some(function) =
|
||||
provider.program.functions.iter().find(|f| f.name == *name)
|
||||
{
|
||||
external_functions.push(ExternalFunction {
|
||||
@ -2984,7 +3209,16 @@ fn resolve_and_check_workspace(
|
||||
}
|
||||
}
|
||||
DeclKind::CImport => {
|
||||
if let Some(import) =
|
||||
if let Some(import) = checked_provider
|
||||
.and_then(|program| program.c_imports.iter().find(|f| f.name == *name))
|
||||
{
|
||||
external_functions.push(ExternalFunction {
|
||||
name: name.clone(),
|
||||
params: import.params.iter().map(|(_, ty)| ty.clone()).collect(),
|
||||
return_type: import.return_type.clone(),
|
||||
foreign: true,
|
||||
});
|
||||
} else if let Some(import) =
|
||||
provider.program.c_imports.iter().find(|f| f.name == *name)
|
||||
{
|
||||
external_functions.push(ExternalFunction {
|
||||
@ -2996,7 +3230,15 @@ fn resolve_and_check_workspace(
|
||||
}
|
||||
}
|
||||
DeclKind::Struct => {
|
||||
if let Some(struct_decl) =
|
||||
if let Some(struct_decl) = checked_provider
|
||||
.and_then(|program| program.structs.iter().find(|s| s.name == *name))
|
||||
{
|
||||
external_structs.push(ExternalStruct {
|
||||
name: name.clone(),
|
||||
fields: struct_decl.fields.clone(),
|
||||
span: struct_decl.span,
|
||||
});
|
||||
} else if let Some(struct_decl) =
|
||||
provider.program.structs.iter().find(|s| s.name == *name)
|
||||
{
|
||||
external_structs.push(ExternalStruct {
|
||||
@ -3011,10 +3253,16 @@ fn resolve_and_check_workspace(
|
||||
}
|
||||
}
|
||||
DeclKind::Enum => {
|
||||
if let Some(enum_decl) = external_enum_from_module(provider, name) {
|
||||
if let Some(enum_decl) = checked_provider
|
||||
.and_then(|program| {
|
||||
external_enum_from_checked_module(provider, program, name)
|
||||
})
|
||||
.or_else(|| external_enum_from_module(provider, name))
|
||||
{
|
||||
external_enums.push(enum_decl);
|
||||
}
|
||||
}
|
||||
DeclKind::TypeAlias => {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3088,9 +3336,9 @@ fn resolve_and_check_workspace(
|
||||
|
||||
let mut selected_build_entry_package = None;
|
||||
if require_entry_wrapper {
|
||||
selected_build_entry_package = select_workspace_build_entry(&packages, &module_maps);
|
||||
match selected_build_entry_package {
|
||||
Some(package_index) => {
|
||||
match select_workspace_build_entry(&workspace, &packages, &module_maps) {
|
||||
Ok(package_index) => {
|
||||
selected_build_entry_package = Some(package_index);
|
||||
add_workspace_entry_wrapper(
|
||||
&packages,
|
||||
package_index,
|
||||
@ -3099,11 +3347,7 @@ fn resolve_and_check_workspace(
|
||||
&mut diagnostics,
|
||||
);
|
||||
}
|
||||
None => diagnostics.push(Diagnostic::new(
|
||||
&workspace.path.display().to_string(),
|
||||
"WorkspaceBuildAmbiguousEntryPackage",
|
||||
"workspace build requires exactly one package with its entry module",
|
||||
)),
|
||||
Err(diagnostic) => diagnostics.push(diagnostic),
|
||||
}
|
||||
if !diagnostics.is_empty() {
|
||||
return Err(ProjectLoadFailure {
|
||||
@ -3189,9 +3433,19 @@ fn resolve_and_check(
|
||||
let mut external_enums = Vec::new();
|
||||
for (name, binding) in imports {
|
||||
let provider = &modules[by_name[&binding.provider]];
|
||||
let checked_provider = checked_by_module.get(&binding.provider);
|
||||
match binding.kind {
|
||||
DeclKind::Function => {
|
||||
if let Some(function) =
|
||||
if let Some(function) = checked_provider
|
||||
.and_then(|program| program.functions.iter().find(|f| f.name == *name))
|
||||
{
|
||||
external_functions.push(ExternalFunction {
|
||||
name: name.clone(),
|
||||
params: function.params.iter().map(|(_, ty)| ty.clone()).collect(),
|
||||
return_type: function.return_type.clone(),
|
||||
foreign: false,
|
||||
});
|
||||
} else if let Some(function) =
|
||||
provider.program.functions.iter().find(|f| f.name == *name)
|
||||
{
|
||||
external_functions.push(ExternalFunction {
|
||||
@ -3203,7 +3457,16 @@ fn resolve_and_check(
|
||||
}
|
||||
}
|
||||
DeclKind::CImport => {
|
||||
if let Some(import) =
|
||||
if let Some(import) = checked_provider
|
||||
.and_then(|program| program.c_imports.iter().find(|f| f.name == *name))
|
||||
{
|
||||
external_functions.push(ExternalFunction {
|
||||
name: name.clone(),
|
||||
params: import.params.iter().map(|(_, ty)| ty.clone()).collect(),
|
||||
return_type: import.return_type.clone(),
|
||||
foreign: true,
|
||||
});
|
||||
} else if let Some(import) =
|
||||
provider.program.c_imports.iter().find(|f| f.name == *name)
|
||||
{
|
||||
external_functions.push(ExternalFunction {
|
||||
@ -3215,7 +3478,15 @@ fn resolve_and_check(
|
||||
}
|
||||
}
|
||||
DeclKind::Struct => {
|
||||
if let Some(struct_decl) =
|
||||
if let Some(struct_decl) = checked_provider
|
||||
.and_then(|program| program.structs.iter().find(|s| s.name == *name))
|
||||
{
|
||||
external_structs.push(ExternalStruct {
|
||||
name: name.clone(),
|
||||
fields: struct_decl.fields.clone(),
|
||||
span: struct_decl.span,
|
||||
});
|
||||
} else if let Some(struct_decl) =
|
||||
provider.program.structs.iter().find(|s| s.name == *name)
|
||||
{
|
||||
external_structs.push(ExternalStruct {
|
||||
@ -3230,10 +3501,16 @@ fn resolve_and_check(
|
||||
}
|
||||
}
|
||||
DeclKind::Enum => {
|
||||
if let Some(enum_decl) = external_enum_from_module(provider, name) {
|
||||
if let Some(enum_decl) = checked_provider
|
||||
.and_then(|program| {
|
||||
external_enum_from_checked_module(provider, program, name)
|
||||
})
|
||||
.or_else(|| external_enum_from_module(provider, name))
|
||||
{
|
||||
external_enums.push(enum_decl);
|
||||
}
|
||||
}
|
||||
DeclKind::TypeAlias => {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3327,6 +3604,22 @@ fn build_export_maps(
|
||||
Some(kind) => {
|
||||
exports.insert(export.name.clone(), kind);
|
||||
}
|
||||
None if module.local_aliases.contains_key(&export.name) => diagnostics.push(
|
||||
Diagnostic::new(
|
||||
&module.file,
|
||||
"Visibility",
|
||||
format!(
|
||||
"type alias `{}` is module-local and cannot be exported",
|
||||
export.name
|
||||
),
|
||||
)
|
||||
.with_span(export.span)
|
||||
.related(
|
||||
"type alias declaration",
|
||||
module.local_aliases[&export.name].span,
|
||||
)
|
||||
.hint("export functions, structs, or enums; imported signatures see concrete target types"),
|
||||
),
|
||||
None => diagnostics.push(
|
||||
Diagnostic::new(
|
||||
&module.file,
|
||||
@ -3374,6 +3667,7 @@ fn resolve_imports(
|
||||
.get(&name.name)
|
||||
.or_else(|| module.local_structs.get(&name.name))
|
||||
.or_else(|| module.local_enums.get(&name.name))
|
||||
.or_else(|| module.local_aliases.get(&name.name))
|
||||
{
|
||||
diagnostics.push(duplicate_name(
|
||||
&module.file,
|
||||
@ -3416,7 +3710,8 @@ fn resolve_imports(
|
||||
.local_functions
|
||||
.get(&name.name)
|
||||
.or_else(|| provider.local_structs.get(&name.name))
|
||||
.or_else(|| provider.local_enums.get(&name.name));
|
||||
.or_else(|| provider.local_enums.get(&name.name))
|
||||
.or_else(|| provider.local_aliases.get(&name.name));
|
||||
let exported = export_maps[provider_index].get(&name.name).copied();
|
||||
match (provider_local, exported) {
|
||||
(_, Some(kind)) => {
|
||||
@ -3429,6 +3724,23 @@ fn resolve_imports(
|
||||
},
|
||||
);
|
||||
}
|
||||
(Some(decl), None) if decl.kind == DeclKind::TypeAlias => diagnostics.push(
|
||||
Diagnostic::new(
|
||||
&module.file,
|
||||
"Visibility",
|
||||
format!(
|
||||
"type alias `{}` is module-local and cannot be imported from module `{}`",
|
||||
name.name, provider.name
|
||||
),
|
||||
)
|
||||
.with_span(name.span)
|
||||
.related_in_file(
|
||||
provider.file.clone(),
|
||||
"type alias declaration",
|
||||
decl.span,
|
||||
)
|
||||
.hint("import functions, structs, or enums; function signatures expose the alias target type"),
|
||||
),
|
||||
(Some(decl), None) => diagnostics.push(
|
||||
Diagnostic::new(
|
||||
&module.file,
|
||||
@ -3503,13 +3815,14 @@ fn resolve_workspace_imports(
|
||||
&packages[provider_package_index].modules[provider_module_index];
|
||||
for name in &import.names {
|
||||
if let Some(local) = module
|
||||
.local_functions
|
||||
.get(&name.name)
|
||||
.or_else(|| module.local_structs.get(&name.name))
|
||||
.or_else(|| module.local_enums.get(&name.name))
|
||||
{
|
||||
diagnostics.push(duplicate_name(
|
||||
&module.file,
|
||||
.local_functions
|
||||
.get(&name.name)
|
||||
.or_else(|| module.local_structs.get(&name.name))
|
||||
.or_else(|| module.local_enums.get(&name.name))
|
||||
.or_else(|| module.local_aliases.get(&name.name))
|
||||
{
|
||||
diagnostics.push(duplicate_name(
|
||||
&module.file,
|
||||
&name.name,
|
||||
name.span,
|
||||
local.span,
|
||||
@ -3554,7 +3867,8 @@ fn resolve_workspace_imports(
|
||||
.local_functions
|
||||
.get(&name.name)
|
||||
.or_else(|| provider.local_structs.get(&name.name))
|
||||
.or_else(|| provider.local_enums.get(&name.name));
|
||||
.or_else(|| provider.local_enums.get(&name.name))
|
||||
.or_else(|| provider.local_aliases.get(&name.name));
|
||||
let exported = export_maps[provider_package_index]
|
||||
[provider_module_index]
|
||||
.get(&name.name)
|
||||
@ -3571,6 +3885,25 @@ fn resolve_workspace_imports(
|
||||
},
|
||||
);
|
||||
}
|
||||
(Some(decl), None) if decl.kind == DeclKind::TypeAlias => {
|
||||
diagnostics.push(
|
||||
Diagnostic::new(
|
||||
&module.file,
|
||||
"Visibility",
|
||||
format!(
|
||||
"type alias `{}` is module-local and cannot be imported from module `{}`",
|
||||
name.name, import.module
|
||||
),
|
||||
)
|
||||
.with_span(name.span)
|
||||
.related_in_file(
|
||||
provider.file.clone(),
|
||||
"type alias declaration",
|
||||
decl.span,
|
||||
)
|
||||
.hint("import functions, structs, or enums; function signatures expose the alias target type"),
|
||||
);
|
||||
}
|
||||
(Some(decl), None) => diagnostics.push(
|
||||
Diagnostic::new(
|
||||
&module.file,
|
||||
@ -4156,8 +4489,11 @@ fn add_entry_wrapper(
|
||||
else {
|
||||
diagnostics.push(Diagnostic::new(
|
||||
&entry_module.file,
|
||||
"MissingImport",
|
||||
format!("entry module `{}` has no `main` function", manifest.entry),
|
||||
"ProjectEntryMainMissing",
|
||||
format!(
|
||||
"entry module `{}` has no `main` function; build/run require `(fn main () -> i32 ...)`",
|
||||
manifest.entry
|
||||
),
|
||||
));
|
||||
return;
|
||||
};
|
||||
@ -4166,8 +4502,12 @@ fn add_entry_wrapper(
|
||||
diagnostics.push(
|
||||
Diagnostic::new(
|
||||
&entry_module.file,
|
||||
"MissingImport",
|
||||
"project entry `main` must have no parameters and return `i32`",
|
||||
"ProjectEntryMainInvalidSignature",
|
||||
format!(
|
||||
"project entry `main` must have signature `(fn main () -> i32 ...)`; found {} parameter(s) and return `{}`",
|
||||
entry_function.params.len(),
|
||||
entry_function.return_type
|
||||
),
|
||||
)
|
||||
.with_span(entry_function.span),
|
||||
);
|
||||
@ -4193,9 +4533,38 @@ fn add_entry_wrapper(
|
||||
}
|
||||
|
||||
fn select_workspace_build_entry(
|
||||
workspace: &WorkspaceManifest,
|
||||
packages: &[PackageUnit],
|
||||
module_maps: &[HashMap<String, usize>],
|
||||
) -> Option<usize> {
|
||||
) -> Result<usize, Diagnostic> {
|
||||
if let Some(default_package) = &workspace.default_package {
|
||||
let Some(package_index) = packages
|
||||
.iter()
|
||||
.position(|package| package.manifest.name == *default_package)
|
||||
else {
|
||||
return Err(Diagnostic::new(
|
||||
&workspace.path.display().to_string(),
|
||||
"WorkspaceDefaultPackageMissing",
|
||||
format!(
|
||||
"workspace default_package `{}` is not a workspace package",
|
||||
default_package
|
||||
),
|
||||
));
|
||||
};
|
||||
let package = &packages[package_index];
|
||||
if module_maps[package_index].contains_key(&package.manifest.entry) {
|
||||
return Ok(package_index);
|
||||
}
|
||||
return Err(Diagnostic::new(
|
||||
&package.manifest.path.display().to_string(),
|
||||
"WorkspaceDefaultPackageEntryMissing",
|
||||
format!(
|
||||
"workspace default_package `{}` has no entry module `{}`; build/run require one selected package entry module",
|
||||
default_package, package.manifest.entry
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let candidates = packages
|
||||
.iter()
|
||||
.enumerate()
|
||||
@ -4203,9 +4572,13 @@ fn select_workspace_build_entry(
|
||||
.map(|(index, _)| index)
|
||||
.collect::<Vec<_>>();
|
||||
if candidates.len() == 1 {
|
||||
candidates.first().copied()
|
||||
Ok(candidates[0])
|
||||
} else {
|
||||
None
|
||||
Err(Diagnostic::new(
|
||||
&workspace.path.display().to_string(),
|
||||
"WorkspaceBuildAmbiguousEntryPackage",
|
||||
"workspace build requires exactly one package with its entry module or `[workspace] default_package = \"name\"`",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@ -4229,9 +4602,9 @@ fn add_workspace_entry_wrapper(
|
||||
else {
|
||||
diagnostics.push(Diagnostic::new(
|
||||
&entry_module.file,
|
||||
"MissingImport",
|
||||
"WorkspaceEntryMainMissing",
|
||||
format!(
|
||||
"entry module `{}` in package `{}` has no `main` function",
|
||||
"entry module `{}` in package `{}` has no `main` function; build/run require `(fn main () -> i32 ...)`",
|
||||
package.manifest.entry, package.manifest.name
|
||||
),
|
||||
));
|
||||
@ -4242,8 +4615,12 @@ fn add_workspace_entry_wrapper(
|
||||
diagnostics.push(
|
||||
Diagnostic::new(
|
||||
&entry_module.file,
|
||||
"MissingImport",
|
||||
"workspace entry `main` must have no parameters and return `i32`",
|
||||
"WorkspaceEntryMainInvalidSignature",
|
||||
format!(
|
||||
"workspace entry `main` must have signature `(fn main () -> i32 ...)`; found {} parameter(s) and return `{}`",
|
||||
entry_function.params.len(),
|
||||
entry_function.return_type
|
||||
),
|
||||
)
|
||||
.with_span(entry_function.span),
|
||||
);
|
||||
|
||||
163
compiler/src/reserved.rs
Normal file
163
compiler/src/reserved.rs
Normal file
@ -0,0 +1,163 @@
|
||||
use crate::{
|
||||
diag::Diagnostic,
|
||||
sexpr::{Atom, SExpr, SExprKind},
|
||||
token::Span,
|
||||
};
|
||||
|
||||
const CURRENT_BETA_UNSUPPORTED: &str = "reserved but not supported in the current beta";
|
||||
|
||||
pub(crate) fn unsupported_reserved_type_diagnostic(file: &str, form: &SExpr) -> Option<Diagnostic> {
|
||||
if let Some(name) = expect_ident(form) {
|
||||
if is_generic_type_parameter_name(name) {
|
||||
return Some(unsupported_generic_type_parameter(file, form.span, name));
|
||||
}
|
||||
}
|
||||
|
||||
let items = expect_list(form)?;
|
||||
let head = items.first().and_then(expect_ident)?;
|
||||
match head {
|
||||
"map" => Some(
|
||||
Diagnostic::new(
|
||||
file,
|
||||
"UnsupportedMapType",
|
||||
format!("`map` types are {}", CURRENT_BETA_UNSUPPORTED),
|
||||
)
|
||||
.with_span(form.span)
|
||||
.expected("supported concrete type")
|
||||
.found(render_type_form(form))
|
||||
.hint(
|
||||
"use current concrete arrays, vectors, option/result, structs, enums, or scalars",
|
||||
),
|
||||
),
|
||||
"set" => Some(
|
||||
Diagnostic::new(
|
||||
file,
|
||||
"UnsupportedSetType",
|
||||
format!("`set` types are {}", CURRENT_BETA_UNSUPPORTED),
|
||||
)
|
||||
.with_span(form.span)
|
||||
.expected("supported concrete type")
|
||||
.found(render_type_form(form))
|
||||
.hint(
|
||||
"use current concrete arrays, vectors, option/result, structs, enums, or scalars",
|
||||
),
|
||||
),
|
||||
"vec" if items.len() != 2 => Some(
|
||||
Diagnostic::new(
|
||||
file,
|
||||
"UnsupportedGenericTypeParameter",
|
||||
format!("generic vector syntax is {}", CURRENT_BETA_UNSUPPORTED),
|
||||
)
|
||||
.with_span(form.span)
|
||||
.expected("(vec i32), (vec i64), (vec f64), (vec bool), or (vec string)")
|
||||
.found(render_type_form(form))
|
||||
.hint("choose one current concrete vector family explicitly"),
|
||||
),
|
||||
_ => items
|
||||
.iter()
|
||||
.find_map(|item| unsupported_reserved_type_diagnostic(file, item)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn unsupported_generic_function(file: &str, span: Span) -> Diagnostic {
|
||||
Diagnostic::new(
|
||||
file,
|
||||
"UnsupportedGenericFunction",
|
||||
format!(
|
||||
"generic function declarations are {}",
|
||||
CURRENT_BETA_UNSUPPORTED
|
||||
),
|
||||
)
|
||||
.with_span(span)
|
||||
.expected("(fn name ((arg ConcreteType) ...) -> ConcreteType body...)")
|
||||
.found("(type_params ...)")
|
||||
.hint("write a concrete function for each currently supported type family")
|
||||
}
|
||||
|
||||
pub(crate) fn unsupported_generic_type_alias(file: &str, span: Span) -> Diagnostic {
|
||||
Diagnostic::new(
|
||||
file,
|
||||
"UnsupportedGenericTypeAlias",
|
||||
format!(
|
||||
"parameterized type aliases are {}",
|
||||
CURRENT_BETA_UNSUPPORTED
|
||||
),
|
||||
)
|
||||
.with_span(span)
|
||||
.expected("(type Alias ConcreteType)")
|
||||
.found("(type_params ...)")
|
||||
.hint("aliases remain transparent names for concrete supported target types")
|
||||
}
|
||||
|
||||
pub(crate) fn unsupported_generic_type_parameter(file: &str, span: Span, name: &str) -> Diagnostic {
|
||||
Diagnostic::new(
|
||||
file,
|
||||
"UnsupportedGenericTypeParameter",
|
||||
format!(
|
||||
"generic type parameter `{}` is {}",
|
||||
name, CURRENT_BETA_UNSUPPORTED
|
||||
),
|
||||
)
|
||||
.with_span(span)
|
||||
.expected("concrete supported type")
|
||||
.found(name.to_string())
|
||||
.hint("use a concrete promoted type such as `i32`, `string`, or `(vec i32)`")
|
||||
}
|
||||
|
||||
pub(crate) fn unsupported_generic_standard_library_call(
|
||||
file: &str,
|
||||
span: Span,
|
||||
name: &str,
|
||||
) -> Diagnostic {
|
||||
Diagnostic::new(
|
||||
file,
|
||||
"UnsupportedGenericStandardLibraryCall",
|
||||
format!(
|
||||
"generic standard-library call `{}` is {}",
|
||||
name, CURRENT_BETA_UNSUPPORTED
|
||||
),
|
||||
)
|
||||
.with_span(span)
|
||||
.expected("promoted concrete standard-library function")
|
||||
.found(name.to_string())
|
||||
.hint("use current concrete families such as `std.vec.i32.empty`")
|
||||
}
|
||||
|
||||
pub(crate) fn is_unsupported_generic_standard_library_call(name: &str) -> bool {
|
||||
matches!(name, "std.vec.empty" | "std.result.map")
|
||||
}
|
||||
|
||||
pub(crate) fn is_generic_type_parameter_name(name: &str) -> bool {
|
||||
name.len() == 1 && name.bytes().all(|byte| byte.is_ascii_uppercase())
|
||||
}
|
||||
|
||||
fn expect_list(expr: &SExpr) -> Option<&[SExpr]> {
|
||||
match &expr.kind {
|
||||
SExprKind::List(items) => Some(items),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_ident(expr: &SExpr) -> Option<&str> {
|
||||
match &expr.kind {
|
||||
SExprKind::Atom(Atom::Ident(name)) => Some(name),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_type_form(form: &SExpr) -> String {
|
||||
match &form.kind {
|
||||
SExprKind::Atom(Atom::Ident(name)) => name.clone(),
|
||||
SExprKind::Atom(Atom::Int(value)) => value.to_string(),
|
||||
SExprKind::Atom(Atom::I64(value)) => format!("{}i64", value),
|
||||
SExprKind::Atom(Atom::U32(value)) => format!("{}u32", value),
|
||||
SExprKind::Atom(Atom::U64(value)) => format!("{}u64", value),
|
||||
SExprKind::Atom(Atom::Float(value)) => value.to_string(),
|
||||
SExprKind::Atom(Atom::String(value)) => format!("{:?}", value),
|
||||
SExprKind::Atom(Atom::Arrow) => "->".to_string(),
|
||||
SExprKind::List(items) => {
|
||||
let parts = items.iter().map(render_type_form).collect::<Vec<_>>();
|
||||
format!("({})", parts.join(" "))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,29 @@ use std::{
|
||||
|
||||
use crate::diag::Diagnostic;
|
||||
|
||||
pub fn create_project(target: &str, explicit_name: Option<&str>) -> Result<(), Diagnostic> {
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum ProjectTemplate {
|
||||
Binary,
|
||||
Library,
|
||||
Workspace,
|
||||
}
|
||||
|
||||
impl ProjectTemplate {
|
||||
pub fn parse(value: &str) -> Option<Self> {
|
||||
match value {
|
||||
"binary" => Some(Self::Binary),
|
||||
"library" => Some(Self::Library),
|
||||
"workspace" => Some(Self::Workspace),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_project(
|
||||
target: &str,
|
||||
explicit_name: Option<&str>,
|
||||
template: ProjectTemplate,
|
||||
) -> Result<(), Diagnostic> {
|
||||
if target.trim().is_empty() {
|
||||
return Err(Diagnostic::new(
|
||||
target,
|
||||
@ -49,6 +71,14 @@ pub fn create_project(target: &str, explicit_name: Option<&str>) -> Result<(), D
|
||||
));
|
||||
}
|
||||
|
||||
match template {
|
||||
ProjectTemplate::Binary => create_binary_project(root, target, &name),
|
||||
ProjectTemplate::Library => create_library_project(root, target, &name),
|
||||
ProjectTemplate::Workspace => create_workspace_project(root, target),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_binary_project(root: &Path, target: &str, name: &str) -> Result<(), Diagnostic> {
|
||||
let src = root.join("src");
|
||||
create_dir_all_checked(&src, target)?;
|
||||
write_checked(
|
||||
@ -66,6 +96,56 @@ pub fn create_project(target: &str, explicit_name: Option<&str>) -> Result<(), D
|
||||
)
|
||||
}
|
||||
|
||||
fn create_library_project(root: &Path, target: &str, name: &str) -> Result<(), Diagnostic> {
|
||||
let src = root.join("src");
|
||||
create_dir_all_checked(&src, target)?;
|
||||
write_checked(
|
||||
&root.join("slovo.toml"),
|
||||
&format!(
|
||||
"[project]\nname = \"{}\"\nsource_root = \"src\"\nentry = \"lib\"\n",
|
||||
name
|
||||
),
|
||||
target,
|
||||
)?;
|
||||
write_checked(
|
||||
&src.join("lib.slo"),
|
||||
"(module lib (export answer double))\n\n(fn answer () -> i32\n 42)\n\n(fn double ((value i32)) -> i32\n (+ value value))\n\n(test \"answer is stable\"\n (= (answer) 42))\n\n(test \"double works\"\n (= (double 21) 42))\n",
|
||||
target,
|
||||
)
|
||||
}
|
||||
|
||||
fn create_workspace_project(root: &Path, target: &str) -> Result<(), Diagnostic> {
|
||||
let app_src = root.join("packages/app/src");
|
||||
let lib_src = root.join("packages/libutil/src");
|
||||
create_dir_all_checked(&app_src, target)?;
|
||||
create_dir_all_checked(&lib_src, target)?;
|
||||
write_checked(
|
||||
&root.join("slovo.toml"),
|
||||
"[workspace]\nmembers = [\"packages/app\", \"packages/libutil\"]\ndefault_package = \"app\"\n",
|
||||
target,
|
||||
)?;
|
||||
write_checked(
|
||||
&root.join("packages/libutil/slovo.toml"),
|
||||
"[package]\nname = \"libutil\"\nversion = \"0.1.0\"\n",
|
||||
target,
|
||||
)?;
|
||||
write_checked(
|
||||
&root.join("packages/libutil/src/libutil.slo"),
|
||||
"(module libutil (export answer label))\n\n(fn answer () -> i32\n 42)\n\n(fn label () -> string\n \"libutil\")\n\n(test \"answer is stable\"\n (= (answer) 42))\n",
|
||||
target,
|
||||
)?;
|
||||
write_checked(
|
||||
&root.join("packages/app/slovo.toml"),
|
||||
"[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nlibutil = { path = \"../libutil\" }\n",
|
||||
target,
|
||||
)?;
|
||||
write_checked(
|
||||
&root.join("packages/app/src/main.slo"),
|
||||
"(module main)\n\n(import libutil.libutil (answer label))\n\n(fn main () -> i32\n (if (= (answer) 42)\n 0\n 1))\n\n(test \"app uses libutil\"\n (if (= (label) \"libutil\")\n (= (answer) 42)\n false))\n",
|
||||
target,
|
||||
)
|
||||
}
|
||||
|
||||
fn has_entries(path: &Path) -> Result<bool, Diagnostic> {
|
||||
let mut entries = fs::read_dir(path).map_err(|err| io_diagnostic(path, err))?;
|
||||
match entries.next() {
|
||||
|
||||
@ -68,6 +68,10 @@ const F64_PARAM: &[RuntimeType] = &[RuntimeType::F64];
|
||||
const BOOL_PARAM: &[RuntimeType] = &[RuntimeType::Bool];
|
||||
const STRING_PARAM: &[RuntimeType] = &[RuntimeType::String];
|
||||
const STRING_STRING_PARAMS: &[RuntimeType] = &[RuntimeType::String, RuntimeType::String];
|
||||
const STRING_I32_PARAMS: &[RuntimeType] = &[RuntimeType::String, RuntimeType::I32];
|
||||
const STRING_I32_I32_PARAMS: &[RuntimeType] =
|
||||
&[RuntimeType::String, RuntimeType::I32, RuntimeType::I32];
|
||||
const I32_STRING_PARAMS: &[RuntimeType] = &[RuntimeType::I32, RuntimeType::String];
|
||||
const VEC_I32_PARAM: &[RuntimeType] = &[RuntimeType::VecI32];
|
||||
const VEC_I32_I32_PARAMS: &[RuntimeType] = &[RuntimeType::VecI32, RuntimeType::I32];
|
||||
const VEC_I64_PARAM: &[RuntimeType] = &[RuntimeType::VecI64];
|
||||
@ -175,6 +179,34 @@ pub const FUNCTIONS: &[RuntimeFunction] = &[
|
||||
return_type: RuntimeType::String,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.string.byte_at_result",
|
||||
runtime_symbol: "__glagol_string_byte_at_result",
|
||||
params: STRING_I32_PARAMS,
|
||||
return_type: RuntimeType::ResultI32I32,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.string.slice_result",
|
||||
runtime_symbol: "__glagol_string_slice_result",
|
||||
params: STRING_I32_I32_PARAMS,
|
||||
return_type: RuntimeType::ResultStringI32,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.string.starts_with",
|
||||
runtime_symbol: "__glagol_string_starts_with",
|
||||
params: STRING_STRING_PARAMS,
|
||||
return_type: RuntimeType::Bool,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.string.ends_with",
|
||||
runtime_symbol: "__glagol_string_ends_with",
|
||||
params: STRING_STRING_PARAMS,
|
||||
return_type: RuntimeType::Bool,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.string.parse_i32_result",
|
||||
runtime_symbol: "__glagol_string_parse_i32_result",
|
||||
@ -217,6 +249,62 @@ pub const FUNCTIONS: &[RuntimeFunction] = &[
|
||||
return_type: RuntimeType::ResultBoolI32,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.json.quote_string",
|
||||
runtime_symbol: "__glagol_json_quote_string",
|
||||
params: STRING_PARAM,
|
||||
return_type: RuntimeType::String,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.json.parse_string_value_result",
|
||||
runtime_symbol: "__glagol_json_parse_string_value_result",
|
||||
params: STRING_PARAM,
|
||||
return_type: RuntimeType::ResultStringI32,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.json.parse_bool_value_result",
|
||||
runtime_symbol: "__glagol_json_parse_bool_value_result",
|
||||
params: STRING_PARAM,
|
||||
return_type: RuntimeType::ResultBoolI32,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.json.parse_i32_value_result",
|
||||
runtime_symbol: "__glagol_json_parse_i32_value_result",
|
||||
params: STRING_PARAM,
|
||||
return_type: RuntimeType::ResultI32I32,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.json.parse_u32_value_result",
|
||||
runtime_symbol: "__glagol_json_parse_u32_value_result",
|
||||
params: STRING_PARAM,
|
||||
return_type: RuntimeType::ResultU32I32,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.json.parse_i64_value_result",
|
||||
runtime_symbol: "__glagol_json_parse_i64_value_result",
|
||||
params: STRING_PARAM,
|
||||
return_type: RuntimeType::ResultI64I32,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.json.parse_u64_value_result",
|
||||
runtime_symbol: "__glagol_json_parse_u64_value_result",
|
||||
params: STRING_PARAM,
|
||||
return_type: RuntimeType::ResultU64I32,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.json.parse_f64_value_result",
|
||||
runtime_symbol: "__glagol_json_parse_f64_value_result",
|
||||
params: STRING_PARAM,
|
||||
return_type: RuntimeType::ResultF64I32,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.io.eprint",
|
||||
runtime_symbol: "__glagol_io_eprint",
|
||||
@ -294,6 +382,111 @@ pub const FUNCTIONS: &[RuntimeFunction] = &[
|
||||
return_type: RuntimeType::ResultI32I32,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.fs.exists",
|
||||
runtime_symbol: "__glagol_fs_exists",
|
||||
params: STRING_PARAM,
|
||||
return_type: RuntimeType::Bool,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.fs.is_file",
|
||||
runtime_symbol: "__glagol_fs_is_file",
|
||||
params: STRING_PARAM,
|
||||
return_type: RuntimeType::Bool,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.fs.is_dir",
|
||||
runtime_symbol: "__glagol_fs_is_dir",
|
||||
params: STRING_PARAM,
|
||||
return_type: RuntimeType::Bool,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.fs.remove_file_result",
|
||||
runtime_symbol: "__glagol_fs_remove_file_result",
|
||||
params: STRING_PARAM,
|
||||
return_type: RuntimeType::ResultI32I32,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.fs.create_dir_result",
|
||||
runtime_symbol: "__glagol_fs_create_dir_result",
|
||||
params: STRING_PARAM,
|
||||
return_type: RuntimeType::ResultI32I32,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.fs.open_text_read_result",
|
||||
runtime_symbol: "__glagol_fs_open_text_read_result",
|
||||
params: STRING_PARAM,
|
||||
return_type: RuntimeType::ResultI32I32,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.fs.read_open_text_result",
|
||||
runtime_symbol: "__glagol_fs_read_open_text_result",
|
||||
params: I32_PARAM,
|
||||
return_type: RuntimeType::ResultStringI32,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.fs.close_result",
|
||||
runtime_symbol: "__glagol_fs_close_result",
|
||||
params: I32_PARAM,
|
||||
return_type: RuntimeType::ResultI32I32,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.net.tcp_connect_loopback_result",
|
||||
runtime_symbol: "__glagol_net_tcp_connect_loopback_result",
|
||||
params: I32_PARAM,
|
||||
return_type: RuntimeType::ResultI32I32,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.net.tcp_listen_loopback_result",
|
||||
runtime_symbol: "__glagol_net_tcp_listen_loopback_result",
|
||||
params: I32_PARAM,
|
||||
return_type: RuntimeType::ResultI32I32,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.net.tcp_bound_port_result",
|
||||
runtime_symbol: "__glagol_net_tcp_bound_port_result",
|
||||
params: I32_PARAM,
|
||||
return_type: RuntimeType::ResultI32I32,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.net.tcp_accept_result",
|
||||
runtime_symbol: "__glagol_net_tcp_accept_result",
|
||||
params: I32_PARAM,
|
||||
return_type: RuntimeType::ResultI32I32,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.net.tcp_read_all_result",
|
||||
runtime_symbol: "__glagol_net_tcp_read_all_result",
|
||||
params: I32_PARAM,
|
||||
return_type: RuntimeType::ResultStringI32,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.net.tcp_write_text_result",
|
||||
runtime_symbol: "__glagol_net_tcp_write_text_result",
|
||||
params: I32_STRING_PARAMS,
|
||||
return_type: RuntimeType::ResultI32I32,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.net.tcp_close_result",
|
||||
runtime_symbol: "__glagol_net_tcp_close_result",
|
||||
params: I32_PARAM,
|
||||
return_type: RuntimeType::ResultI32I32,
|
||||
promoted: true,
|
||||
},
|
||||
RuntimeFunction {
|
||||
source_name: "std.vec.i32.empty",
|
||||
runtime_symbol: "__glagol_vec_i32_empty",
|
||||
@ -601,6 +794,21 @@ const RESERVED_HELPER_SYMBOLS: &[&str] = &[
|
||||
"__glagol_fs_read_trap",
|
||||
"__glagol_fs_read_text_result",
|
||||
"__glagol_fs_write_text_result",
|
||||
"__glagol_fs_exists",
|
||||
"__glagol_fs_is_file",
|
||||
"__glagol_fs_is_dir",
|
||||
"__glagol_fs_remove_file_result",
|
||||
"__glagol_fs_create_dir_result",
|
||||
"__glagol_fs_open_text_read_result",
|
||||
"__glagol_fs_read_open_text_result",
|
||||
"__glagol_fs_close_result",
|
||||
"__glagol_net_tcp_connect_loopback_result",
|
||||
"__glagol_net_tcp_listen_loopback_result",
|
||||
"__glagol_net_tcp_bound_port_result",
|
||||
"__glagol_net_tcp_accept_result",
|
||||
"__glagol_net_tcp_read_all_result",
|
||||
"__glagol_net_tcp_write_text_result",
|
||||
"__glagol_net_tcp_close_result",
|
||||
"__glagol_vec_i32_eq",
|
||||
"__glagol_vec_i32_allocation_trap",
|
||||
"__glagol_vec_i32_index_trap",
|
||||
@ -619,6 +827,18 @@ const RESERVED_HELPER_SYMBOLS: &[&str] = &[
|
||||
"__glagol_string_parse_u64_result",
|
||||
"__glagol_string_parse_f64_result",
|
||||
"__glagol_string_parse_bool_result",
|
||||
"__glagol_string_byte_at_result",
|
||||
"__glagol_string_slice_result",
|
||||
"__glagol_string_starts_with",
|
||||
"__glagol_string_ends_with",
|
||||
"__glagol_json_quote_string",
|
||||
"__glagol_json_parse_string_value_result",
|
||||
"__glagol_json_parse_bool_value_result",
|
||||
"__glagol_json_parse_i32_value_result",
|
||||
"__glagol_json_parse_u32_value_result",
|
||||
"__glagol_json_parse_i64_value_result",
|
||||
"__glagol_json_parse_u64_value_result",
|
||||
"__glagol_json_parse_f64_value_result",
|
||||
"__glagol_num_u32_to_string",
|
||||
"__glagol_num_u64_to_string",
|
||||
"__glagol_num_f64_to_string",
|
||||
@ -660,7 +880,7 @@ pub fn unsupported_standard_library_call(file: &str, span: Span, source_name: &s
|
||||
format!("standard library call `{}` is not supported", source_name),
|
||||
)
|
||||
.with_span(span)
|
||||
.expected("std.io.print_i32, std.io.print_u32, std.io.print_i64, std.io.print_u64, std.io.print_f64, std.io.print_string, std.io.print_bool, std.io.eprint, std.io.read_stdin_result, std.string.len, std.string.concat, std.string.parse_i32_result, std.string.parse_u32_result, std.string.parse_i64_result, std.string.parse_u64_result, std.string.parse_f64_result, std.string.parse_bool_result, std.process.argc, std.process.arg, std.process.arg_result, std.env.get, std.env.get_result, std.fs.read_text, std.fs.read_text_result, std.fs.write_text, std.fs.write_text_result, std.vec.i32.empty, std.vec.i32.append, std.vec.i32.len, std.vec.i32.index, std.vec.i64.empty, std.vec.i64.append, std.vec.i64.len, std.vec.i64.index, std.vec.f64.empty, std.vec.f64.append, std.vec.f64.len, std.vec.f64.index, std.vec.bool.empty, std.vec.bool.append, std.vec.bool.len, std.vec.bool.index, std.vec.string.empty, std.vec.string.append, std.vec.string.len, std.vec.string.index, std.time.monotonic_ms, std.time.sleep_ms, std.random.i32, std.num.i32_to_i64, std.num.i32_to_f64, std.num.i64_to_f64, std.num.i64_to_i32_result, std.num.f64_to_i32_result, std.num.f64_to_i64_result, std.num.i32_to_string, std.num.u32_to_string, std.num.i64_to_string, std.num.u64_to_string, std.num.f64_to_string, std.result.is_ok, std.result.is_err, std.result.unwrap_ok, or std.result.unwrap_err")
|
||||
.expected("std.io.print_i32, std.io.print_u32, std.io.print_i64, std.io.print_u64, std.io.print_f64, std.io.print_string, std.io.print_bool, std.io.eprint, std.io.read_stdin_result, std.string.len, std.string.concat, std.string.byte_at_result, std.string.slice_result, std.string.starts_with, std.string.ends_with, std.string.parse_i32_result, std.string.parse_u32_result, std.string.parse_i64_result, std.string.parse_u64_result, std.string.parse_f64_result, std.string.parse_bool_result, std.json.quote_string, std.json.parse_string_value_result, std.json.parse_bool_value_result, std.json.parse_i32_value_result, std.json.parse_u32_value_result, std.json.parse_i64_value_result, std.json.parse_u64_value_result, std.json.parse_f64_value_result, std.process.argc, std.process.arg, std.process.arg_result, std.env.get, std.env.get_result, std.fs.read_text, std.fs.read_text_result, std.fs.write_text, std.fs.write_text_result, std.fs.exists, std.fs.is_file, std.fs.is_dir, std.fs.remove_file_result, std.fs.create_dir_result, std.fs.open_text_read_result, std.fs.read_open_text_result, std.fs.close_result, std.vec.i32.empty, std.vec.i32.append, std.vec.i32.len, std.vec.i32.index, std.vec.i64.empty, std.vec.i64.append, std.vec.i64.len, std.vec.i64.index, std.vec.f64.empty, std.vec.f64.append, std.vec.f64.len, std.vec.f64.index, std.vec.bool.empty, std.vec.bool.append, std.vec.bool.len, std.vec.bool.index, std.vec.string.empty, std.vec.string.append, std.vec.string.len, std.vec.string.index, std.time.monotonic_ms, std.time.sleep_ms, std.random.i32, std.num.i32_to_i64, std.num.i32_to_f64, std.num.i64_to_f64, std.num.i64_to_i32_result, std.num.f64_to_i32_result, std.num.f64_to_i64_result, std.num.i32_to_string, std.num.u32_to_string, std.num.i64_to_string, std.num.u64_to_string, std.num.f64_to_string, std.result.is_ok, std.result.is_err, std.result.unwrap_ok, or std.result.unwrap_err")
|
||||
.found(source_name)
|
||||
.hint("use a promoted standard-runtime name or a legacy intrinsic alias")
|
||||
}
|
||||
|
||||
667
compiler/src/symbols.rs
Normal file
667
compiler/src/symbols.rs
Normal file
@ -0,0 +1,667 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
diag, lexer,
|
||||
project::{ProjectArtifact, SourceFile, ToolProject},
|
||||
sexpr::{Atom, SExpr, SExprKind},
|
||||
token::Span,
|
||||
};
|
||||
|
||||
pub fn render_file(path: &str, source: &str) -> Result<String, Vec<diag::Diagnostic>> {
|
||||
let source = SourceFile {
|
||||
path: path.to_string(),
|
||||
source: source.to_string(),
|
||||
};
|
||||
render_sources(&[source], None)
|
||||
}
|
||||
|
||||
pub fn render_project(project: &ToolProject) -> Result<String, Vec<diag::Diagnostic>> {
|
||||
render_sources(&project.sources, Some(&project.artifact))
|
||||
}
|
||||
|
||||
fn render_sources(
|
||||
sources: &[SourceFile],
|
||||
artifact: Option<&ProjectArtifact>,
|
||||
) -> Result<String, Vec<diag::Diagnostic>> {
|
||||
let package_by_path = package_names_by_path(artifact);
|
||||
let mut modules = Vec::new();
|
||||
let mut diagnostics = Vec::new();
|
||||
|
||||
let mut sorted_sources = sources.iter().collect::<Vec<_>>();
|
||||
sorted_sources.sort_by(|left, right| left.path.cmp(&right.path));
|
||||
|
||||
for source in sorted_sources {
|
||||
match module_symbols(
|
||||
source,
|
||||
package_by_path.get(&source.path).map(String::as_str),
|
||||
) {
|
||||
Ok(module) => modules.push(module),
|
||||
Err(mut errs) => diagnostics.append(&mut errs),
|
||||
}
|
||||
}
|
||||
|
||||
if !diagnostics.is_empty() {
|
||||
return Err(diagnostics);
|
||||
}
|
||||
|
||||
let mut out = String::new();
|
||||
out.push_str("(symbols\n");
|
||||
out.push_str(" (schema slovo.symbols)\n");
|
||||
out.push_str(" (version \"1.0.0-beta.10\")\n");
|
||||
out.push_str(" (modules");
|
||||
if modules.is_empty() {
|
||||
out.push_str(")\n");
|
||||
} else {
|
||||
out.push('\n');
|
||||
for module in &modules {
|
||||
render_module(module, &mut out);
|
||||
}
|
||||
out.push_str(" )\n");
|
||||
}
|
||||
out.push_str(")\n");
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn package_names_by_path(artifact: Option<&ProjectArtifact>) -> HashMap<String, String> {
|
||||
let mut packages = HashMap::new();
|
||||
let Some(workspace) = artifact.and_then(|artifact| artifact.workspace.as_ref()) else {
|
||||
return packages;
|
||||
};
|
||||
for package in &workspace.packages {
|
||||
for module in &package.modules {
|
||||
packages.insert(module.path.clone(), package.name.clone());
|
||||
}
|
||||
}
|
||||
packages
|
||||
}
|
||||
|
||||
fn module_symbols(
|
||||
source: &SourceFile,
|
||||
package: Option<&str>,
|
||||
) -> Result<ModuleSymbols, Vec<diag::Diagnostic>> {
|
||||
let tokens = lexer::lex(&source.path, &source.source)?;
|
||||
let forms = crate::sexpr::parse(&source.path, &tokens)?;
|
||||
|
||||
let mut module = ModuleSymbols {
|
||||
name: "main".to_string(),
|
||||
path: source.path.clone(),
|
||||
package: package.map(str::to_string),
|
||||
module_span: None,
|
||||
exports: Vec::new(),
|
||||
imports: Vec::new(),
|
||||
type_aliases: Vec::new(),
|
||||
structs: Vec::new(),
|
||||
enums: Vec::new(),
|
||||
functions: Vec::new(),
|
||||
tests: Vec::new(),
|
||||
source: source.source.clone(),
|
||||
};
|
||||
|
||||
for form in &forms {
|
||||
match list_head(form) {
|
||||
Some("module") => extract_module(form, &mut module),
|
||||
Some("import") => extract_import(form, &mut module.imports),
|
||||
Some("type") => extract_type_alias(form, &mut module.type_aliases),
|
||||
Some("struct") => extract_struct(form, &mut module.structs),
|
||||
Some("enum") => extract_enum(form, &mut module.enums),
|
||||
Some("fn") => extract_function(form, &mut module.functions),
|
||||
Some("test") => extract_test(form, &mut module.tests),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(module)
|
||||
}
|
||||
|
||||
fn extract_module(form: &SExpr, module: &mut ModuleSymbols) {
|
||||
let Some(items) = expect_list(form) else {
|
||||
return;
|
||||
};
|
||||
if let Some(name) = items.get(1).and_then(expect_ident) {
|
||||
module.name = name.to_string();
|
||||
module.module_span = Some(form.span);
|
||||
}
|
||||
if let Some(export_items) = items.get(2).and_then(expect_list) {
|
||||
if matches!(export_items.first().and_then(expect_ident), Some("export")) {
|
||||
module
|
||||
.exports
|
||||
.extend(export_items[1..].iter().filter_map(named_symbol_from_ident));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_import(form: &SExpr, imports: &mut Vec<ImportSymbol>) {
|
||||
let Some(items) = expect_list(form) else {
|
||||
return;
|
||||
};
|
||||
let Some(module_name) = items.get(1).and_then(expect_ident) else {
|
||||
return;
|
||||
};
|
||||
let names = items
|
||||
.get(2)
|
||||
.and_then(expect_list)
|
||||
.map(|items| items.iter().filter_map(named_symbol_from_ident).collect())
|
||||
.unwrap_or_default();
|
||||
imports.push(ImportSymbol {
|
||||
module: module_name.to_string(),
|
||||
span: form.span,
|
||||
module_span: items[1].span,
|
||||
names,
|
||||
});
|
||||
}
|
||||
|
||||
fn extract_type_alias(form: &SExpr, aliases: &mut Vec<TypeAliasSymbol>) {
|
||||
let Some(items) = expect_list(form) else {
|
||||
return;
|
||||
};
|
||||
let Some(name) = items.get(1).and_then(named_symbol_from_ident) else {
|
||||
return;
|
||||
};
|
||||
aliases.push(TypeAliasSymbol {
|
||||
name,
|
||||
span: form.span,
|
||||
target_span: items.get(2).map(|item| item.span),
|
||||
});
|
||||
}
|
||||
|
||||
fn extract_struct(form: &SExpr, structs: &mut Vec<StructSymbol>) {
|
||||
let Some(items) = expect_list(form) else {
|
||||
return;
|
||||
};
|
||||
let Some(name) = items.get(1).and_then(named_symbol_from_ident) else {
|
||||
return;
|
||||
};
|
||||
let fields = items[2..]
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
let pair = expect_list(item)?;
|
||||
let field = pair.first().and_then(named_symbol_from_ident)?;
|
||||
Some(FieldSymbol {
|
||||
name: field,
|
||||
span: item.span,
|
||||
type_span: pair.get(1).map(|ty| ty.span),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
structs.push(StructSymbol {
|
||||
name,
|
||||
span: form.span,
|
||||
fields,
|
||||
});
|
||||
}
|
||||
|
||||
fn extract_enum(form: &SExpr, enums: &mut Vec<EnumSymbol>) {
|
||||
let Some(items) = expect_list(form) else {
|
||||
return;
|
||||
};
|
||||
let Some(name) = items.get(1).and_then(named_symbol_from_ident) else {
|
||||
return;
|
||||
};
|
||||
let variants = items[2..]
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
if let Some(name) = named_symbol_from_ident(item) {
|
||||
return Some(VariantSymbol {
|
||||
name,
|
||||
span: item.span,
|
||||
payload_span: None,
|
||||
});
|
||||
}
|
||||
let variant_items = expect_list(item)?;
|
||||
let name = variant_items.first().and_then(named_symbol_from_ident)?;
|
||||
Some(VariantSymbol {
|
||||
name,
|
||||
span: item.span,
|
||||
payload_span: variant_items.get(1).map(|payload| payload.span),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
enums.push(EnumSymbol {
|
||||
name,
|
||||
span: form.span,
|
||||
variants,
|
||||
});
|
||||
}
|
||||
|
||||
fn extract_function(form: &SExpr, functions: &mut Vec<FunctionSymbol>) {
|
||||
let Some(items) = expect_list(form) else {
|
||||
return;
|
||||
};
|
||||
let Some(name) = items.get(1).and_then(named_symbol_from_ident) else {
|
||||
return;
|
||||
};
|
||||
let params = items
|
||||
.get(2)
|
||||
.and_then(expect_list)
|
||||
.map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
let pair = expect_list(item)?;
|
||||
let name = pair.first().and_then(named_symbol_from_ident)?;
|
||||
Some(ParamSymbol {
|
||||
name,
|
||||
span: item.span,
|
||||
type_span: pair.get(1).map(|ty| ty.span),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
functions.push(FunctionSymbol {
|
||||
name,
|
||||
span: form.span,
|
||||
params,
|
||||
return_span: items.get(4).map(|item| item.span),
|
||||
});
|
||||
}
|
||||
|
||||
fn extract_test(form: &SExpr, tests: &mut Vec<TestSymbol>) {
|
||||
let Some(items) = expect_list(form) else {
|
||||
return;
|
||||
};
|
||||
let Some(name_expr) = items.get(1) else {
|
||||
return;
|
||||
};
|
||||
let Some(name) = expect_string(name_expr) else {
|
||||
return;
|
||||
};
|
||||
tests.push(TestSymbol {
|
||||
name: NamedSymbol {
|
||||
name: name.to_string(),
|
||||
span: name_expr.span,
|
||||
},
|
||||
span: form.span,
|
||||
});
|
||||
}
|
||||
|
||||
fn render_module(module: &ModuleSymbols, out: &mut String) {
|
||||
out.push_str(" (module\n");
|
||||
out.push_str(&format!(
|
||||
" (name {})\n",
|
||||
diag::render_string(&module.name)
|
||||
));
|
||||
if let Some(package) = module.package.as_deref() {
|
||||
out.push_str(&format!(
|
||||
" (package {})\n",
|
||||
diag::render_string(package)
|
||||
));
|
||||
}
|
||||
out.push_str(&format!(
|
||||
" (path {})\n",
|
||||
diag::render_string(&module.path)
|
||||
));
|
||||
if let Some(span) = module.module_span {
|
||||
render_span(" ", "module_span", span, &module.source, out);
|
||||
}
|
||||
render_named_symbols(
|
||||
"exports",
|
||||
"export",
|
||||
&module.exports,
|
||||
" ",
|
||||
&module.source,
|
||||
out,
|
||||
);
|
||||
render_imports(&module.imports, module, out);
|
||||
render_type_aliases(&module.type_aliases, module, out);
|
||||
render_structs(&module.structs, module, out);
|
||||
render_enums(&module.enums, module, out);
|
||||
render_functions(&module.functions, module, out);
|
||||
render_tests(&module.tests, module, out);
|
||||
out.push_str(" )\n");
|
||||
}
|
||||
|
||||
fn render_named_symbols(
|
||||
section: &str,
|
||||
item_name: &str,
|
||||
symbols: &[NamedSymbol],
|
||||
indent: &str,
|
||||
source: &str,
|
||||
out: &mut String,
|
||||
) {
|
||||
out.push_str(indent);
|
||||
out.push('(');
|
||||
out.push_str(section);
|
||||
if symbols.is_empty() {
|
||||
out.push_str(")\n");
|
||||
return;
|
||||
}
|
||||
out.push('\n');
|
||||
for symbol in symbols {
|
||||
out.push_str(indent);
|
||||
out.push_str(" (");
|
||||
out.push_str(item_name);
|
||||
out.push('\n');
|
||||
out.push_str(indent);
|
||||
out.push_str(" (name ");
|
||||
out.push_str(&diag::render_string(&symbol.name));
|
||||
out.push_str(")\n");
|
||||
let symbol_indent = format!("{} ", indent);
|
||||
render_span(&symbol_indent, "span", symbol.span, source, out);
|
||||
out.push_str(indent);
|
||||
out.push_str(" )\n");
|
||||
}
|
||||
out.push_str(indent);
|
||||
out.push_str(")\n");
|
||||
}
|
||||
|
||||
fn render_imports(imports: &[ImportSymbol], module: &ModuleSymbols, out: &mut String) {
|
||||
out.push_str(" (imports");
|
||||
if imports.is_empty() {
|
||||
out.push_str(")\n");
|
||||
return;
|
||||
}
|
||||
out.push('\n');
|
||||
for import in imports {
|
||||
out.push_str(" (import\n");
|
||||
out.push_str(&format!(
|
||||
" (module {})\n",
|
||||
diag::render_string(&import.module)
|
||||
));
|
||||
render_span(" ", "span", import.span, &module.source, out);
|
||||
render_span(
|
||||
" ",
|
||||
"module_span",
|
||||
import.module_span,
|
||||
&module.source,
|
||||
out,
|
||||
);
|
||||
render_named_symbols(
|
||||
"names",
|
||||
"name",
|
||||
&import.names,
|
||||
" ",
|
||||
&module.source,
|
||||
out,
|
||||
);
|
||||
out.push_str(" )\n");
|
||||
}
|
||||
out.push_str(" )\n");
|
||||
}
|
||||
|
||||
fn render_type_aliases(aliases: &[TypeAliasSymbol], module: &ModuleSymbols, out: &mut String) {
|
||||
out.push_str(" (type_aliases");
|
||||
if aliases.is_empty() {
|
||||
out.push_str(")\n");
|
||||
return;
|
||||
}
|
||||
out.push('\n');
|
||||
for alias in aliases {
|
||||
out.push_str(" (type_alias\n");
|
||||
render_decl_name(&alias.name, " ", &module.source, out);
|
||||
render_span(" ", "span", alias.span, &module.source, out);
|
||||
if let Some(span) = alias.target_span {
|
||||
render_span(" ", "target_span", span, &module.source, out);
|
||||
}
|
||||
out.push_str(" )\n");
|
||||
}
|
||||
out.push_str(" )\n");
|
||||
}
|
||||
|
||||
fn render_structs(structs: &[StructSymbol], module: &ModuleSymbols, out: &mut String) {
|
||||
out.push_str(" (structs");
|
||||
if structs.is_empty() {
|
||||
out.push_str(")\n");
|
||||
return;
|
||||
}
|
||||
out.push('\n');
|
||||
for struct_symbol in structs {
|
||||
out.push_str(" (struct\n");
|
||||
render_decl_name(&struct_symbol.name, " ", &module.source, out);
|
||||
render_span(
|
||||
" ",
|
||||
"span",
|
||||
struct_symbol.span,
|
||||
&module.source,
|
||||
out,
|
||||
);
|
||||
out.push_str(" (fields");
|
||||
if struct_symbol.fields.is_empty() {
|
||||
out.push_str(")\n");
|
||||
} else {
|
||||
out.push('\n');
|
||||
for field in &struct_symbol.fields {
|
||||
out.push_str(" (field\n");
|
||||
render_decl_name(&field.name, " ", &module.source, out);
|
||||
render_span(" ", "span", field.span, &module.source, out);
|
||||
if let Some(span) = field.type_span {
|
||||
render_span(" ", "type_span", span, &module.source, out);
|
||||
}
|
||||
out.push_str(" )\n");
|
||||
}
|
||||
out.push_str(" )\n");
|
||||
}
|
||||
out.push_str(" )\n");
|
||||
}
|
||||
out.push_str(" )\n");
|
||||
}
|
||||
|
||||
fn render_enums(enums: &[EnumSymbol], module: &ModuleSymbols, out: &mut String) {
|
||||
out.push_str(" (enums");
|
||||
if enums.is_empty() {
|
||||
out.push_str(")\n");
|
||||
return;
|
||||
}
|
||||
out.push('\n');
|
||||
for enum_symbol in enums {
|
||||
out.push_str(" (enum\n");
|
||||
render_decl_name(&enum_symbol.name, " ", &module.source, out);
|
||||
render_span(" ", "span", enum_symbol.span, &module.source, out);
|
||||
out.push_str(" (variants");
|
||||
if enum_symbol.variants.is_empty() {
|
||||
out.push_str(")\n");
|
||||
} else {
|
||||
out.push('\n');
|
||||
for variant in &enum_symbol.variants {
|
||||
out.push_str(" (variant\n");
|
||||
render_decl_name(&variant.name, " ", &module.source, out);
|
||||
render_span(" ", "span", variant.span, &module.source, out);
|
||||
if let Some(span) = variant.payload_span {
|
||||
render_span(" ", "payload_span", span, &module.source, out);
|
||||
}
|
||||
out.push_str(" )\n");
|
||||
}
|
||||
out.push_str(" )\n");
|
||||
}
|
||||
out.push_str(" )\n");
|
||||
}
|
||||
out.push_str(" )\n");
|
||||
}
|
||||
|
||||
fn render_functions(functions: &[FunctionSymbol], module: &ModuleSymbols, out: &mut String) {
|
||||
out.push_str(" (functions");
|
||||
if functions.is_empty() {
|
||||
out.push_str(")\n");
|
||||
return;
|
||||
}
|
||||
out.push('\n');
|
||||
for function in functions {
|
||||
out.push_str(" (function\n");
|
||||
render_decl_name(&function.name, " ", &module.source, out);
|
||||
render_span(" ", "span", function.span, &module.source, out);
|
||||
if let Some(span) = function.return_span {
|
||||
render_span(" ", "return_span", span, &module.source, out);
|
||||
}
|
||||
out.push_str(" (params");
|
||||
if function.params.is_empty() {
|
||||
out.push_str(")\n");
|
||||
} else {
|
||||
out.push('\n');
|
||||
for param in &function.params {
|
||||
out.push_str(" (param\n");
|
||||
render_decl_name(¶m.name, " ", &module.source, out);
|
||||
render_span(" ", "span", param.span, &module.source, out);
|
||||
if let Some(span) = param.type_span {
|
||||
render_span(" ", "type_span", span, &module.source, out);
|
||||
}
|
||||
out.push_str(" )\n");
|
||||
}
|
||||
out.push_str(" )\n");
|
||||
}
|
||||
out.push_str(" )\n");
|
||||
}
|
||||
out.push_str(" )\n");
|
||||
}
|
||||
|
||||
fn render_tests(tests: &[TestSymbol], module: &ModuleSymbols, out: &mut String) {
|
||||
out.push_str(" (tests");
|
||||
if tests.is_empty() {
|
||||
out.push_str(")\n");
|
||||
return;
|
||||
}
|
||||
out.push('\n');
|
||||
for test in tests {
|
||||
out.push_str(" (test\n");
|
||||
render_decl_name(&test.name, " ", &module.source, out);
|
||||
render_span(" ", "span", test.span, &module.source, out);
|
||||
out.push_str(" )\n");
|
||||
}
|
||||
out.push_str(" )\n");
|
||||
}
|
||||
|
||||
fn render_decl_name(symbol: &NamedSymbol, indent: &str, source: &str, out: &mut String) {
|
||||
out.push_str(indent);
|
||||
out.push_str(&format!("(name {})\n", diag::render_string(&symbol.name)));
|
||||
render_span(indent, "name_span", symbol.span, source, out);
|
||||
}
|
||||
|
||||
fn render_span(indent: &str, label: &str, span: Span, source: &str, out: &mut String) {
|
||||
let start = position(source, span.start);
|
||||
let end = position(source, span.end);
|
||||
out.push_str(indent);
|
||||
out.push('(');
|
||||
out.push_str(label);
|
||||
out.push_str(&format!(
|
||||
" (span (start {}) (end {})) (range (start (line {}) (column {})) (end (line {}) (column {}))))\n",
|
||||
span.start, span.end, start.line, start.column, end.line, end.column
|
||||
));
|
||||
}
|
||||
|
||||
fn position(source: &str, offset: usize) -> Position {
|
||||
let mut line = 1;
|
||||
let mut column = 1;
|
||||
for (index, byte) in source.as_bytes().iter().enumerate() {
|
||||
if index >= offset {
|
||||
break;
|
||||
}
|
||||
if *byte == b'\n' {
|
||||
line += 1;
|
||||
column = 1;
|
||||
} else {
|
||||
column += 1;
|
||||
}
|
||||
}
|
||||
Position { line, column }
|
||||
}
|
||||
|
||||
fn list_head(form: &SExpr) -> Option<&str> {
|
||||
expect_list(form)?.first().and_then(expect_ident)
|
||||
}
|
||||
|
||||
fn expect_list(form: &SExpr) -> Option<&[SExpr]> {
|
||||
match &form.kind {
|
||||
SExprKind::List(items) => Some(items),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_ident(form: &SExpr) -> Option<&str> {
|
||||
match &form.kind {
|
||||
SExprKind::Atom(Atom::Ident(name)) => Some(name),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_string(form: &SExpr) -> Option<&str> {
|
||||
match &form.kind {
|
||||
SExprKind::Atom(Atom::String(name)) => Some(name),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn named_symbol_from_ident(form: &SExpr) -> Option<NamedSymbol> {
|
||||
Some(NamedSymbol {
|
||||
name: expect_ident(form)?.to_string(),
|
||||
span: form.span,
|
||||
})
|
||||
}
|
||||
|
||||
struct ModuleSymbols {
|
||||
name: String,
|
||||
path: String,
|
||||
package: Option<String>,
|
||||
module_span: Option<Span>,
|
||||
exports: Vec<NamedSymbol>,
|
||||
imports: Vec<ImportSymbol>,
|
||||
type_aliases: Vec<TypeAliasSymbol>,
|
||||
structs: Vec<StructSymbol>,
|
||||
enums: Vec<EnumSymbol>,
|
||||
functions: Vec<FunctionSymbol>,
|
||||
tests: Vec<TestSymbol>,
|
||||
source: String,
|
||||
}
|
||||
|
||||
struct NamedSymbol {
|
||||
name: String,
|
||||
span: Span,
|
||||
}
|
||||
|
||||
struct ImportSymbol {
|
||||
module: String,
|
||||
span: Span,
|
||||
module_span: Span,
|
||||
names: Vec<NamedSymbol>,
|
||||
}
|
||||
|
||||
struct TypeAliasSymbol {
|
||||
name: NamedSymbol,
|
||||
span: Span,
|
||||
target_span: Option<Span>,
|
||||
}
|
||||
|
||||
struct StructSymbol {
|
||||
name: NamedSymbol,
|
||||
span: Span,
|
||||
fields: Vec<FieldSymbol>,
|
||||
}
|
||||
|
||||
struct FieldSymbol {
|
||||
name: NamedSymbol,
|
||||
span: Span,
|
||||
type_span: Option<Span>,
|
||||
}
|
||||
|
||||
struct EnumSymbol {
|
||||
name: NamedSymbol,
|
||||
span: Span,
|
||||
variants: Vec<VariantSymbol>,
|
||||
}
|
||||
|
||||
struct VariantSymbol {
|
||||
name: NamedSymbol,
|
||||
span: Span,
|
||||
payload_span: Option<Span>,
|
||||
}
|
||||
|
||||
struct FunctionSymbol {
|
||||
name: NamedSymbol,
|
||||
span: Span,
|
||||
params: Vec<ParamSymbol>,
|
||||
return_span: Option<Span>,
|
||||
}
|
||||
|
||||
struct ParamSymbol {
|
||||
name: NamedSymbol,
|
||||
span: Span,
|
||||
type_span: Option<Span>,
|
||||
}
|
||||
|
||||
struct TestSymbol {
|
||||
name: NamedSymbol,
|
||||
span: Span,
|
||||
}
|
||||
|
||||
struct Position {
|
||||
line: usize,
|
||||
column: usize,
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -154,6 +154,7 @@ fn benchmark_roots() -> Vec<PathBuf> {
|
||||
root.join("math-loop"),
|
||||
root.join("branch-loop"),
|
||||
root.join("parse-loop"),
|
||||
root.join("json-quote-loop"),
|
||||
root.join("array-index-loop"),
|
||||
root.join("string-eq-loop"),
|
||||
root.join("array-struct-field-loop"),
|
||||
|
||||
134
compiler/tests/benchmark_suite_catalog_beta14.rs
Normal file
134
compiler/tests/benchmark_suite_catalog_beta14.rs
Normal file
@ -0,0 +1,134 @@
|
||||
use std::{
|
||||
env,
|
||||
path::Path,
|
||||
process::{Command, Output},
|
||||
};
|
||||
|
||||
const BENCHMARKS: &[&str] = &[
|
||||
"array-index-loop",
|
||||
"array-struct-field-loop",
|
||||
"branch-loop",
|
||||
"enum-struct-payload-loop",
|
||||
"json-quote-loop",
|
||||
"math-loop",
|
||||
"parse-loop",
|
||||
"string-eq-loop",
|
||||
"vec-i32-index-loop",
|
||||
"vec-string-eq-loop",
|
||||
];
|
||||
|
||||
const IMPLEMENTATIONS: &[&str] = &["slovo", "c", "rust", "python", "clojure", "common_lisp"];
|
||||
|
||||
#[test]
|
||||
fn suite_catalog_is_byte_stable_and_lists_current_benchmarks() {
|
||||
let repo = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
|
||||
let python = python_command();
|
||||
|
||||
let first = run_suite_catalog(&repo, &python);
|
||||
let second = run_suite_catalog(&repo, &python);
|
||||
|
||||
assert_success("first suite catalog run", &first);
|
||||
assert_success("second suite catalog run", &second);
|
||||
assert_eq!(
|
||||
first.stdout, second.stdout,
|
||||
"suite catalog JSON must be byte-stable across runs"
|
||||
);
|
||||
|
||||
let stdout = String::from_utf8_lossy(&first.stdout);
|
||||
for needle in [
|
||||
r#""suite": "glagol-local-benchmark-suite""#,
|
||||
r#""benchmark_count": 10"#,
|
||||
r#""benchmark_metadata_files": 10"#,
|
||||
r#""required_files": 40"#,
|
||||
r#""missing_required_files": []"#,
|
||||
r#""implementation_slots": 60"#,
|
||||
r#""expected_implementation_slots": 60"#,
|
||||
r#""missing_implementation_slots": []"#,
|
||||
r#""status": "ok""#,
|
||||
r#""timing_scope": "local-machine comparison only""#,
|
||||
r#""timing_disclaimer": "Local timing comparison only; not a published benchmark result and not a cross-machine performance claim.""#,
|
||||
r#""cold-process""#,
|
||||
r#""hot-loop""#,
|
||||
r#""loop_count": 1000000"#,
|
||||
r#""hot_loop_count": 10000000"#,
|
||||
r#""checksum_metadata""#,
|
||||
r#""expected_checksum""#,
|
||||
r#""hot_expected_checksum""#,
|
||||
r#""required_files""#,
|
||||
r#""path": "benchmark.json""#,
|
||||
r#""path": "run.py""#,
|
||||
r#""path": "slovo.toml""#,
|
||||
r#""path": "src/main.slo""#,
|
||||
r#""status": "present""#,
|
||||
r#""implementation_slots""#,
|
||||
r#""loop_count_source": "stdin""#,
|
||||
] {
|
||||
assert_contains(&stdout, needle);
|
||||
}
|
||||
|
||||
for benchmark in BENCHMARKS {
|
||||
assert_contains(&stdout, &format!(r#""name": "{}""#, benchmark));
|
||||
assert_contains(&stdout, &format!(r#""directory": "{}""#, benchmark));
|
||||
}
|
||||
|
||||
for implementation in IMPLEMENTATIONS {
|
||||
assert_contains(&stdout, &format!(r#""name": "{}""#, implementation));
|
||||
}
|
||||
}
|
||||
|
||||
fn run_suite_catalog(repo: &Path, python: &str) -> Output {
|
||||
Command::new(python)
|
||||
.arg("benchmarks/runner.py")
|
||||
.arg("--suite-list")
|
||||
.arg("--json")
|
||||
.current_dir(repo)
|
||||
.output()
|
||||
.unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"run `{}` benchmarks/runner.py --suite-list --json: {}",
|
||||
python, err
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn python_command() -> String {
|
||||
if let Some(python) = env::var_os("PYTHON") {
|
||||
return python.to_string_lossy().into_owned();
|
||||
}
|
||||
|
||||
for candidate in ["python3", "python"] {
|
||||
if Command::new(candidate)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|output| output.status.success())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return candidate.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
panic!("benchmark suite catalog test requires python3 or python")
|
||||
}
|
||||
|
||||
fn assert_success(context: &str, output: &Output) {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{} failed\nstdout:\n{}\nstderr:\n{}",
|
||||
context,
|
||||
stdout,
|
||||
stderr
|
||||
);
|
||||
assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr);
|
||||
}
|
||||
|
||||
fn assert_contains(haystack: &str, needle: &str) {
|
||||
assert!(
|
||||
haystack.contains(needle),
|
||||
"suite catalog output missing `{}`\nstdout:\n{}",
|
||||
needle,
|
||||
haystack
|
||||
);
|
||||
}
|
||||
116
compiler/tests/concrete_type_aliases_beta.rs
Normal file
116
compiler/tests/concrete_type_aliases_beta.rs
Normal file
@ -0,0 +1,116 @@
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Output},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn concrete_type_alias_fixture_erases_before_llvm_and_runs_tests() {
|
||||
let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/type-aliases.slo");
|
||||
|
||||
let compile = run_glagol([fixture.as_os_str()]);
|
||||
let stdout = String::from_utf8_lossy(&compile.stdout);
|
||||
let stderr = String::from_utf8_lossy(&compile.stderr);
|
||||
assert!(
|
||||
compile.status.success(),
|
||||
"compiler rejected concrete type alias fixture\nstdout:\n{}\nstderr:\n{}",
|
||||
stdout,
|
||||
stderr
|
||||
);
|
||||
assert!(
|
||||
stdout.contains("define i32 @bump(i32 %value)")
|
||||
&& stdout.contains("define [3 x i32] @make_counts(i32 %base)")
|
||||
&& stdout.contains("define i32 @pick_count([3 x i32] %values)")
|
||||
&& stdout.contains("define { i32, ptr } @make_measure(i32 %amount)")
|
||||
&& stdout.contains("define { i1, i32 } @maybe_amount(i32 %amount)")
|
||||
&& stdout.contains("define { i1, i32 } @ok_amount(i32 %amount)")
|
||||
&& stdout.contains("define ptr @empty_counts()")
|
||||
&& stdout.contains("define { i32, i32 } @reading_value(i32 %amount)")
|
||||
&& stdout.contains("define i32 @reading_score({ i32, i32 } %reading)")
|
||||
&& stdout.contains("call ptr @__glagol_vec_i32_empty()"),
|
||||
"LLVM output did not show erased concrete alias shapes\nstdout:\n{}",
|
||||
stdout
|
||||
);
|
||||
assert!(
|
||||
!stdout.contains("Count")
|
||||
&& !stdout.contains("Score")
|
||||
&& !stdout.contains("MaybeCount")
|
||||
&& !stdout.contains("ReadingAlias"),
|
||||
"LLVM output leaked source alias names\nstdout:\n{}",
|
||||
stdout
|
||||
);
|
||||
assert!(stderr.is_empty(), "compiler wrote stderr:\n{}", stderr);
|
||||
|
||||
let run = run_glagol([OsStr::new("--run-tests"), fixture.as_os_str()]);
|
||||
assert_success_stdout(
|
||||
run,
|
||||
concat!(
|
||||
"test \"aliases erase through arrays and structs\" ... ok\n",
|
||||
"test \"aliases erase through option result vec and enum\" ... ok\n",
|
||||
"2 test(s) passed\n",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concrete_type_alias_fixture_formats_and_lowers_stably() {
|
||||
let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/type-aliases.slo");
|
||||
|
||||
let formatted = run_glagol([OsStr::new("--format"), fixture.as_os_str()]);
|
||||
assert_success_stdout(
|
||||
formatted,
|
||||
&std::fs::read_to_string(&fixture).expect("read type alias fixture"),
|
||||
);
|
||||
|
||||
let surface = run_glagol([
|
||||
OsStr::new("--inspect-lowering=surface"),
|
||||
fixture.as_os_str(),
|
||||
]);
|
||||
assert_success_stdout(
|
||||
surface,
|
||||
&std::fs::read_to_string(
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/type-aliases.surface.lower"),
|
||||
)
|
||||
.expect("read type alias surface snapshot"),
|
||||
);
|
||||
|
||||
let checked = run_glagol([
|
||||
OsStr::new("--inspect-lowering=checked"),
|
||||
fixture.as_os_str(),
|
||||
]);
|
||||
assert_success_stdout(
|
||||
checked,
|
||||
&std::fs::read_to_string(
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/type-aliases.checked.lower"),
|
||||
)
|
||||
.expect("read type alias checked snapshot"),
|
||||
);
|
||||
}
|
||||
|
||||
fn run_glagol<I, S>(args: I) -> Output
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<OsStr>,
|
||||
{
|
||||
Command::new(glagol_bin())
|
||||
.args(args)
|
||||
.output()
|
||||
.expect("run glagol")
|
||||
}
|
||||
|
||||
fn glagol_bin() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_BIN_EXE_glagol"))
|
||||
}
|
||||
|
||||
fn assert_success_stdout(output: Output, expected: &str) {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"command failed\nstdout:\n{}\nstderr:\n{}",
|
||||
stdout,
|
||||
stderr
|
||||
);
|
||||
assert_eq!(stdout, expected);
|
||||
assert!(stderr.is_empty(), "stderr was not empty:\n{}", stderr);
|
||||
}
|
||||
@ -83,6 +83,148 @@ const CASES: &[DiagnosticCase] = &[
|
||||
"#,
|
||||
snapshot: "../tests/unknown-top-level-form.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "malformed-type-alias",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(type Count)
|
||||
"#,
|
||||
snapshot: "../tests/malformed-type-alias.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "duplicate-type-alias",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(type Count i32)
|
||||
(type Count i64)
|
||||
"#,
|
||||
snapshot: "../tests/duplicate-type-alias.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "type-alias-name-conflict",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(struct Count
|
||||
(value i32))
|
||||
|
||||
(type Count i32)
|
||||
"#,
|
||||
snapshot: "../tests/type-alias-name-conflict.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "type-alias-value-name-conflict",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(import_c c_add ((value i32)) -> i32)
|
||||
|
||||
(type main i32)
|
||||
|
||||
(type c_add i32)
|
||||
|
||||
(fn main () -> i32
|
||||
0)
|
||||
"#,
|
||||
snapshot: "../tests/type-alias-value-name-conflict.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "unknown-type-alias-target",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(type Count Missing)
|
||||
"#,
|
||||
snapshot: "../tests/unknown-type-alias-target.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "unsupported-type-alias-target",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(type BadUnit unit)
|
||||
|
||||
(type BadPtr (ptr i32))
|
||||
|
||||
(type BadSlice (slice i32))
|
||||
|
||||
(type BadVec (vec u32))
|
||||
|
||||
(type BadResult (result i32 string))
|
||||
"#,
|
||||
snapshot: "../tests/unsupported-type-alias-target.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "self-type-alias",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(type Count Count)
|
||||
"#,
|
||||
snapshot: "../tests/self-type-alias.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "cyclic-type-alias",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(type A B)
|
||||
(type B A)
|
||||
"#,
|
||||
snapshot: "../tests/cyclic-type-alias.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "unsupported-generic-function",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn id (type_params T) ((value T)) -> T
|
||||
value)
|
||||
"#,
|
||||
snapshot: "../tests/unsupported-generic-function.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "unsupported-generic-type-alias",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(type VecOf (type_params T) (vec T))
|
||||
"#,
|
||||
snapshot: "../tests/unsupported-generic-type-alias.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "unsupported-generic-type-parameter",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(let xs (vec T) (std.vec.i32.empty))
|
||||
0)
|
||||
"#,
|
||||
snapshot: "../tests/unsupported-generic-type-parameter.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "unsupported-map-type",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> (map string i32)
|
||||
0)
|
||||
"#,
|
||||
snapshot: "../tests/unsupported-map-type.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "unsupported-set-type",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> (set string)
|
||||
0)
|
||||
"#,
|
||||
snapshot: "../tests/unsupported-set-type.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "enum-empty",
|
||||
source: r#"
|
||||
@ -894,6 +1036,250 @@ const CASES: &[DiagnosticCase] = &[
|
||||
"#,
|
||||
snapshot: "../tests/std-string-concat-unsupported-string-container.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-byte-at-result-arity",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> (result i32 i32)
|
||||
(std.string.byte_at_result "abc"))
|
||||
"#,
|
||||
snapshot: "../tests/std-string-byte-at-result-arity.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-byte-at-result-type",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> (result i32 i32)
|
||||
(std.string.byte_at_result "abc" "0"))
|
||||
"#,
|
||||
snapshot: "../tests/std-string-byte-at-result-type.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-byte-at-result-context",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(std.string.byte_at_result "abc" 0))
|
||||
"#,
|
||||
snapshot: "../tests/std-string-byte-at-result-context.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-byte-at-result-bool-context",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(if (std.string.byte_at_result "abc" 0) 1 0))
|
||||
"#,
|
||||
snapshot: "../tests/std-string-byte-at-result-bool-context.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-byte-at-result-name-shadow",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn std.string.byte_at_result ((text string) (index i32)) -> (result i32 i32)
|
||||
(err i32 i32 1))
|
||||
|
||||
(fn main () -> i32
|
||||
0)
|
||||
"#,
|
||||
snapshot: "../tests/std-string-byte-at-result-name-shadow.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-byte-at-result-helper-shadow",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn __glagol_string_byte_at_result ((text string) (index i32)) -> (result i32 i32)
|
||||
(err i32 i32 1))
|
||||
|
||||
(fn main () -> i32
|
||||
0)
|
||||
"#,
|
||||
snapshot: "../tests/std-string-byte-at-result-helper-shadow.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-slice-result-arity",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> (result string i32)
|
||||
(std.string.slice_result "abc" 0))
|
||||
"#,
|
||||
snapshot: "../tests/std-string-slice-result-arity.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-slice-result-type",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> (result string i32)
|
||||
(std.string.slice_result "abc" 0 "1"))
|
||||
"#,
|
||||
snapshot: "../tests/std-string-slice-result-type.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-slice-result-context",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> string
|
||||
(std.string.slice_result "abc" 0 1))
|
||||
"#,
|
||||
snapshot: "../tests/std-string-slice-result-context.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-slice-result-bool-context",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(if (std.string.slice_result "abc" 0 1) 1 0))
|
||||
"#,
|
||||
snapshot: "../tests/std-string-slice-result-bool-context.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-slice-result-name-shadow",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn std.string.slice_result ((text string) (start i32) (count i32)) -> (result string i32)
|
||||
(err string i32 1))
|
||||
|
||||
(fn main () -> i32
|
||||
0)
|
||||
"#,
|
||||
snapshot: "../tests/std-string-slice-result-name-shadow.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-slice-result-helper-shadow",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn __glagol_string_slice_result ((text string) (start i32) (count i32)) -> (result string i32)
|
||||
(err string i32 1))
|
||||
|
||||
(fn main () -> i32
|
||||
0)
|
||||
"#,
|
||||
snapshot: "../tests/std-string-slice-result-helper-shadow.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-starts-with-arity",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> bool
|
||||
(std.string.starts_with "abc"))
|
||||
"#,
|
||||
snapshot: "../tests/std-string-starts-with-arity.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-starts-with-type",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> bool
|
||||
(std.string.starts_with "abc" 1))
|
||||
"#,
|
||||
snapshot: "../tests/std-string-starts-with-type.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-starts-with-context",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(std.string.starts_with "abc" "a"))
|
||||
"#,
|
||||
snapshot: "../tests/std-string-starts-with-context.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-starts-with-name-shadow",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn std.string.starts_with ((text string) (prefix string)) -> bool
|
||||
false)
|
||||
|
||||
(fn main () -> i32
|
||||
0)
|
||||
"#,
|
||||
snapshot: "../tests/std-string-starts-with-name-shadow.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-starts-with-helper-shadow",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn __glagol_string_starts_with ((text string) (prefix string)) -> bool
|
||||
false)
|
||||
|
||||
(fn main () -> i32
|
||||
0)
|
||||
"#,
|
||||
snapshot: "../tests/std-string-starts-with-helper-shadow.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-ends-with-arity",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> bool
|
||||
(std.string.ends_with "abc"))
|
||||
"#,
|
||||
snapshot: "../tests/std-string-ends-with-arity.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-ends-with-type",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> bool
|
||||
(std.string.ends_with "abc" 1))
|
||||
"#,
|
||||
snapshot: "../tests/std-string-ends-with-type.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-ends-with-context",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(std.string.ends_with "abc" "c"))
|
||||
"#,
|
||||
snapshot: "../tests/std-string-ends-with-context.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-ends-with-name-shadow",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn std.string.ends_with ((text string) (suffix string)) -> bool
|
||||
false)
|
||||
|
||||
(fn main () -> i32
|
||||
0)
|
||||
"#,
|
||||
snapshot: "../tests/std-string-ends-with-name-shadow.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-ends-with-helper-shadow",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn __glagol_string_ends_with ((text string) (suffix string)) -> bool
|
||||
false)
|
||||
|
||||
(fn main () -> i32
|
||||
0)
|
||||
"#,
|
||||
snapshot: "../tests/std-string-ends-with-helper-shadow.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-parse-i32-result-arity",
|
||||
source: r#"
|
||||
@ -1388,6 +1774,16 @@ const CASES: &[DiagnosticCase] = &[
|
||||
"#,
|
||||
snapshot: "../tests/std-string-index-unsupported.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-byte-at-unsupported",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(std.string.byte_at "42" 0))
|
||||
"#,
|
||||
snapshot: "../tests/std-string-byte-at-unsupported.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-slice-unsupported",
|
||||
source: r#"
|
||||
@ -1399,6 +1795,39 @@ const CASES: &[DiagnosticCase] = &[
|
||||
"#,
|
||||
snapshot: "../tests/std-string-slice-unsupported.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-contains-unsupported",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(std.string.contains "slovo" "lo")
|
||||
0)
|
||||
"#,
|
||||
snapshot: "../tests/std-string-contains-unsupported.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-find-result-unsupported",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(std.string.find_result "slovo" "lo")
|
||||
0)
|
||||
"#,
|
||||
snapshot: "../tests/std-string-find-result-unsupported.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-split-unsupported",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(std.string.split "a,b" ",")
|
||||
0)
|
||||
"#,
|
||||
snapshot: "../tests/std-string-split-unsupported.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-string-tokenize-unsupported",
|
||||
source: r#"
|
||||
@ -2051,11 +2480,22 @@ const CASES: &[DiagnosticCase] = &[
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(std.result.map (ok string i32 "a"))
|
||||
(std.result.map (ok string i32 "a") mapper)
|
||||
0)
|
||||
"#,
|
||||
snapshot: "../tests/std-result-map-unsupported.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-vec-empty-generic-unsupported",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(std.vec.empty i32)
|
||||
0)
|
||||
"#,
|
||||
snapshot: "../tests/std-vec-empty-generic-unsupported.diag",
|
||||
},
|
||||
DiagnosticCase {
|
||||
name: "std-package-load-unsupported",
|
||||
source: r#"
|
||||
|
||||
486
compiler/tests/diagnostics_schema_beta13.rs
Normal file
486
compiler/tests/diagnostics_schema_beta13.rs
Normal file
@ -0,0 +1,486 @@
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Output},
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
|
||||
static NEXT_FIXTURE_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[test]
|
||||
fn sexpr_diagnostics_keep_v1_schema_across_source_pipelines() {
|
||||
let cases = [
|
||||
SourceDiagnosticCase {
|
||||
name: "parse",
|
||||
args: &["check"],
|
||||
source: "(module main",
|
||||
code: "UnclosedList",
|
||||
},
|
||||
SourceDiagnosticCase {
|
||||
name: "check",
|
||||
args: &["check"],
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn id ((value i32)) -> i32
|
||||
value)
|
||||
|
||||
(fn main () -> i32
|
||||
(id true))
|
||||
"#,
|
||||
code: "TypeMismatch",
|
||||
},
|
||||
SourceDiagnosticCase {
|
||||
name: "fmt",
|
||||
args: &["fmt"],
|
||||
source: r#"
|
||||
(module main) ; comments stay outside formatter input
|
||||
|
||||
(fn main () -> i32
|
||||
0)
|
||||
"#,
|
||||
code: "UnsupportedFormatterComment",
|
||||
},
|
||||
SourceDiagnosticCase {
|
||||
name: "test",
|
||||
args: &["test"],
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(test "false case"
|
||||
false)
|
||||
"#,
|
||||
code: "TestFailed",
|
||||
},
|
||||
];
|
||||
|
||||
for case in cases {
|
||||
let fixture = write_fixture(case.name, case.source);
|
||||
let mut args = case.args.iter().map(OsStr::new).collect::<Vec<_>>();
|
||||
args.push(fixture.as_os_str());
|
||||
|
||||
let output = run_glagol(args);
|
||||
|
||||
assert_exit_code(case.name, &output, 1);
|
||||
assert_stdout_empty(case.name, &output);
|
||||
assert_sexpr_diagnostic_schema(case.name, &output, case.code, 1);
|
||||
}
|
||||
|
||||
let project = write_project(
|
||||
"sexpr-project",
|
||||
"(module main)\n\n(import missing (value))\n",
|
||||
);
|
||||
let output = run_glagol(["check".as_ref(), project.as_os_str()]);
|
||||
|
||||
assert_exit_code("project", &output, 1);
|
||||
assert_stdout_empty("project", &output);
|
||||
assert_sexpr_diagnostic_schema("project", &output, "MissingImport", 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_diagnostics_keep_v1_schema_across_policy_boundaries() {
|
||||
let cases = [
|
||||
SourceDiagnosticCase {
|
||||
name: "json-parse",
|
||||
args: &["--json-diagnostics", "check"],
|
||||
source: "(module main",
|
||||
code: "UnclosedList",
|
||||
},
|
||||
SourceDiagnosticCase {
|
||||
name: "json-check",
|
||||
args: &["--json-diagnostics", "check"],
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn id ((value i32)) -> i32
|
||||
value)
|
||||
|
||||
(fn main () -> i32
|
||||
(id true))
|
||||
"#,
|
||||
code: "TypeMismatch",
|
||||
},
|
||||
SourceDiagnosticCase {
|
||||
name: "json-fmt",
|
||||
args: &["--json-diagnostics", "fmt"],
|
||||
source: r#"
|
||||
(module main) ; comments stay outside formatter input
|
||||
|
||||
(fn main () -> i32
|
||||
0)
|
||||
"#,
|
||||
code: "UnsupportedFormatterComment",
|
||||
},
|
||||
SourceDiagnosticCase {
|
||||
name: "json-test",
|
||||
args: &["--json-diagnostics", "test"],
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(test "false case"
|
||||
false)
|
||||
"#,
|
||||
code: "TestFailed",
|
||||
},
|
||||
];
|
||||
|
||||
for case in cases {
|
||||
let fixture = write_fixture(case.name, case.source);
|
||||
let mut args = case.args.iter().map(OsStr::new).collect::<Vec<_>>();
|
||||
args.push(fixture.as_os_str());
|
||||
|
||||
let output = run_glagol(args);
|
||||
|
||||
assert_exit_code(case.name, &output, 1);
|
||||
assert_stdout_empty(case.name, &output);
|
||||
assert_json_diagnostic_schema(case.name, &output, case.code, JsonSource::Source);
|
||||
}
|
||||
|
||||
let project = write_project(
|
||||
"json-project",
|
||||
"(module main)\n\n(import missing (value))\n",
|
||||
);
|
||||
let output = run_glagol([
|
||||
"--json-diagnostics".as_ref(),
|
||||
"check".as_ref(),
|
||||
project.as_os_str(),
|
||||
]);
|
||||
|
||||
assert_exit_code("json project", &output, 1);
|
||||
assert_stdout_empty("json project", &output);
|
||||
assert_json_diagnostic_schema("json project", &output, "MissingImport", JsonSource::Source);
|
||||
|
||||
let usage_manifest = temp_path("json-usage", "manifest.slo");
|
||||
let usage = run_glagol([
|
||||
"--json-diagnostics".as_ref(),
|
||||
"--manifest".as_ref(),
|
||||
usage_manifest.as_os_str(),
|
||||
]);
|
||||
|
||||
assert_exit_code("json usage", &usage, 2);
|
||||
assert_stdout_empty("json usage", &usage);
|
||||
assert_json_diagnostic_schema("json usage", &usage, "UsageError", JsonSource::SourceLess);
|
||||
assert_manifest_schema_fields(&read_manifest(&usage_manifest), "json");
|
||||
|
||||
let toolchain_fixture = write_fixture(
|
||||
"json-toolchain",
|
||||
"(module main)\n\n(fn main () -> i32\n 0)\n",
|
||||
);
|
||||
let toolchain_manifest = temp_path("json-toolchain", "manifest.slo");
|
||||
let output_path = temp_path("json-toolchain", "bin");
|
||||
let missing_clang = temp_path("json-toolchain", "not-a-clang");
|
||||
let toolchain = Command::new(compiler_path())
|
||||
.arg("--json-diagnostics")
|
||||
.arg("build")
|
||||
.arg(&toolchain_fixture)
|
||||
.arg("-o")
|
||||
.arg(&output_path)
|
||||
.arg("--manifest")
|
||||
.arg(&toolchain_manifest)
|
||||
.env("GLAGOL_CLANG", &missing_clang)
|
||||
.env_remove("GLAGOL_RUNTIME_C")
|
||||
.env_remove("SLOVO_RUNTIME_C")
|
||||
.output()
|
||||
.unwrap_or_else(|err| panic!("run glagol build: {}", err));
|
||||
|
||||
assert_exit_code("json toolchain", &toolchain, 3);
|
||||
assert_stdout_empty("json toolchain", &toolchain);
|
||||
assert_json_diagnostic_schema(
|
||||
"json toolchain",
|
||||
&toolchain,
|
||||
"ToolchainUnavailable",
|
||||
JsonSource::SourceLess,
|
||||
);
|
||||
assert_manifest_schema_fields(&read_manifest(&toolchain_manifest), "json");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_failure_manifests_record_schema_encoding_and_count_deterministically() {
|
||||
let project = write_project(
|
||||
"manifest-project",
|
||||
"(module main)\n\n(import missing (value))\n",
|
||||
);
|
||||
|
||||
let first_sexpr = run_project_failure_manifest(&project, "sexpr", false);
|
||||
let second_sexpr = run_project_failure_manifest(&project, "sexpr-repeat", false);
|
||||
assert_manifest_schema_fields(&first_sexpr, "sexpr");
|
||||
assert_project_diagnostics_count(&first_sexpr, 1);
|
||||
assert_eq!(
|
||||
project_block(&first_sexpr),
|
||||
project_block(&second_sexpr),
|
||||
"S-expression failure manifest project block drifted"
|
||||
);
|
||||
|
||||
let first_json = run_project_failure_manifest(&project, "json", true);
|
||||
let second_json = run_project_failure_manifest(&project, "json-repeat", true);
|
||||
assert_manifest_schema_fields(&first_json, "json");
|
||||
assert_project_diagnostics_count(&first_json, 1);
|
||||
assert_eq!(
|
||||
project_block(&first_json),
|
||||
project_block(&second_json),
|
||||
"JSON failure manifest project block drifted"
|
||||
);
|
||||
}
|
||||
|
||||
struct SourceDiagnosticCase {
|
||||
name: &'static str,
|
||||
args: &'static [&'static str],
|
||||
source: &'static str,
|
||||
code: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum JsonSource {
|
||||
Source,
|
||||
SourceLess,
|
||||
}
|
||||
|
||||
fn run_project_failure_manifest(project: &Path, name: &str, json: bool) -> String {
|
||||
let manifest = temp_path(name, "manifest.slo");
|
||||
let output = if json {
|
||||
run_glagol([
|
||||
"--json-diagnostics".as_ref(),
|
||||
"check".as_ref(),
|
||||
"--manifest".as_ref(),
|
||||
manifest.as_os_str(),
|
||||
project.as_os_str(),
|
||||
])
|
||||
} else {
|
||||
run_glagol([
|
||||
"check".as_ref(),
|
||||
"--manifest".as_ref(),
|
||||
manifest.as_os_str(),
|
||||
project.as_os_str(),
|
||||
])
|
||||
};
|
||||
|
||||
assert_exit_code(name, &output, 1);
|
||||
assert_stdout_empty(name, &output);
|
||||
read_manifest(&manifest)
|
||||
}
|
||||
|
||||
fn assert_sexpr_diagnostic_schema(
|
||||
context: &str,
|
||||
output: &Output,
|
||||
expected_code: &str,
|
||||
expected_count: usize,
|
||||
) {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(
|
||||
!stderr.trim().is_empty(),
|
||||
"{} did not emit diagnostics",
|
||||
context
|
||||
);
|
||||
assert!(
|
||||
!stderr
|
||||
.lines()
|
||||
.all(|line| line.starts_with('{') && line.ends_with('}')),
|
||||
"{} unexpectedly emitted JSON diagnostics:\n{}",
|
||||
context,
|
||||
stderr
|
||||
);
|
||||
assert_eq!(
|
||||
stderr.matches("(diagnostic\n").count(),
|
||||
expected_count,
|
||||
"{} diagnostic block count drifted:\n{}",
|
||||
context,
|
||||
stderr
|
||||
);
|
||||
assert_eq!(
|
||||
stderr.matches(" (schema slovo.diagnostic)\n").count(),
|
||||
expected_count,
|
||||
"{} diagnostic schema name drifted:\n{}",
|
||||
context,
|
||||
stderr
|
||||
);
|
||||
assert_eq!(
|
||||
stderr.matches(" (version 1)\n").count(),
|
||||
expected_count,
|
||||
"{} diagnostic schema version drifted:\n{}",
|
||||
context,
|
||||
stderr
|
||||
);
|
||||
assert!(
|
||||
stderr.contains(" (severity error)\n")
|
||||
&& stderr.contains(&format!(" (code {})\n", expected_code))
|
||||
&& stderr.contains(" (file ")
|
||||
&& stderr.contains(" (span\n"),
|
||||
"{} S-expression diagnostic missed required structural fields:\n{}",
|
||||
context,
|
||||
stderr
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_json_diagnostic_schema(
|
||||
context: &str,
|
||||
output: &Output,
|
||||
expected_code: &str,
|
||||
source: JsonSource,
|
||||
) {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let lines = stderr
|
||||
.lines()
|
||||
.filter(|line| !line.trim().is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
assert!(!lines.is_empty(), "{} did not emit diagnostics", context);
|
||||
|
||||
for line in &lines {
|
||||
assert!(
|
||||
line.starts_with('{') && line.ends_with('}'),
|
||||
"{} emitted non-JSON diagnostic text:\n{}",
|
||||
context,
|
||||
stderr
|
||||
);
|
||||
assert!(
|
||||
line.contains(r#""schema":"slovo.diagnostic""#)
|
||||
&& line.contains(r#""version":1"#)
|
||||
&& line.contains(r#""severity":"error""#)
|
||||
&& line.contains(r#""message":"#)
|
||||
&& line.contains(r#""file":"#)
|
||||
&& line.contains(r#""span":"#),
|
||||
"{} JSON diagnostic missed required schema fields:\n{}",
|
||||
context,
|
||||
line
|
||||
);
|
||||
}
|
||||
|
||||
assert!(
|
||||
lines
|
||||
.iter()
|
||||
.any(|line| line.contains(&format!(r#""code":"{}""#, expected_code))),
|
||||
"{} JSON diagnostics did not include code `{}`:\n{}",
|
||||
context,
|
||||
expected_code,
|
||||
stderr
|
||||
);
|
||||
|
||||
match source {
|
||||
JsonSource::Source => assert!(
|
||||
lines
|
||||
.iter()
|
||||
.any(|line| line.contains(r#""span":{"byte_start":"#)),
|
||||
"{} JSON diagnostics should include a concrete source span:\n{}",
|
||||
context,
|
||||
stderr
|
||||
),
|
||||
JsonSource::SourceLess => assert!(
|
||||
lines
|
||||
.iter()
|
||||
.any(|line| line.contains(r#""file":null"#) && line.contains(r#""span":null"#)),
|
||||
"{} JSON diagnostics should be source-less:\n{}",
|
||||
context,
|
||||
stderr
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_manifest_schema_fields(manifest: &str, encoding: &str) {
|
||||
assert!(
|
||||
manifest.contains(" (schema slovo.artifact-manifest)\n")
|
||||
&& manifest.contains(" (version 1)\n")
|
||||
&& manifest.contains(" (diagnostics-schema-version 1)\n")
|
||||
&& manifest.contains(&format!(" (diagnostics-encoding {})\n", encoding))
|
||||
&& manifest.contains("slovo.diagnostic"),
|
||||
"manifest diagnostic schema fields drifted:\n{}",
|
||||
manifest
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_project_diagnostics_count(manifest: &str, expected: usize) {
|
||||
assert!(
|
||||
manifest.contains(&format!(" (diagnostics_count {})\n", expected)),
|
||||
"manifest diagnostics count drifted:\n{}",
|
||||
manifest
|
||||
);
|
||||
}
|
||||
|
||||
fn project_block(manifest: &str) -> &str {
|
||||
let start = manifest
|
||||
.find(" (project\n")
|
||||
.expect("manifest did not contain project block");
|
||||
manifest[start..]
|
||||
.strip_suffix("\n)\n")
|
||||
.expect("manifest did not end with artifact-manifest close")
|
||||
}
|
||||
|
||||
fn run_glagol<I, S>(args: I) -> Output
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<OsStr>,
|
||||
{
|
||||
Command::new(compiler_path())
|
||||
.args(args)
|
||||
.output()
|
||||
.unwrap_or_else(|err| panic!("run glagol: {}", err))
|
||||
}
|
||||
|
||||
fn compiler_path() -> &'static str {
|
||||
env!("CARGO_BIN_EXE_glagol")
|
||||
}
|
||||
|
||||
fn write_fixture(name: &str, source: &str) -> PathBuf {
|
||||
let path = temp_path(name, "slo");
|
||||
fs::write(&path, source).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err));
|
||||
path
|
||||
}
|
||||
|
||||
fn write_project(name: &str, main_source: &str) -> PathBuf {
|
||||
let root = temp_dir(name);
|
||||
let source_root = root.join("src");
|
||||
fs::create_dir_all(&source_root)
|
||||
.unwrap_or_else(|err| panic!("create `{}`: {}", source_root.display(), err));
|
||||
fs::write(
|
||||
root.join("slovo.toml"),
|
||||
"[project]\nname = \"beta13\"\nsource_root = \"src\"\nentry = \"main\"\n",
|
||||
)
|
||||
.unwrap_or_else(|err| panic!("write project manifest: {}", err));
|
||||
fs::write(source_root.join("main.slo"), main_source)
|
||||
.unwrap_or_else(|err| panic!("write main source: {}", err));
|
||||
root
|
||||
}
|
||||
|
||||
fn read_manifest(path: &Path) -> String {
|
||||
fs::read_to_string(path).unwrap_or_else(|err| panic!("read `{}`: {}", path.display(), err))
|
||||
}
|
||||
|
||||
fn temp_path(name: &str, extension: &str) -> PathBuf {
|
||||
let mut path = std::env::temp_dir();
|
||||
let id = NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed);
|
||||
path.push(format!(
|
||||
"glagol-beta13-{}-{}-{}.{}",
|
||||
std::process::id(),
|
||||
id,
|
||||
name,
|
||||
extension,
|
||||
));
|
||||
path
|
||||
}
|
||||
|
||||
fn temp_dir(name: &str) -> PathBuf {
|
||||
let mut path = std::env::temp_dir();
|
||||
let id = NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed);
|
||||
path.push(format!(
|
||||
"glagol-beta13-{}-{}-{}",
|
||||
std::process::id(),
|
||||
id,
|
||||
name,
|
||||
));
|
||||
path
|
||||
}
|
||||
|
||||
fn assert_exit_code(context: &str, output: &Output, expected: i32) {
|
||||
assert_eq!(
|
||||
output.status.code(),
|
||||
Some(expected),
|
||||
"{} exit code mismatch\nstdout:\n{}\nstderr:\n{}",
|
||||
context,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_stdout_empty(context: &str, output: &Output) {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.is_empty(), "{} wrote stdout:\n{}", context, stdout);
|
||||
}
|
||||
420
compiler/tests/doc_api_beta11.rs
Normal file
420
compiler/tests/doc_api_beta11.rs
Normal file
@ -0,0 +1,420 @@
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Output},
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
|
||||
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[test]
|
||||
fn doc_file_renders_public_api_with_signatures_and_shapes() {
|
||||
let source = r#"(module api (export Point Status make))
|
||||
|
||||
(struct Point
|
||||
(x i32)
|
||||
(label string))
|
||||
|
||||
(enum Status Ready (Blocked i32))
|
||||
|
||||
(fn helper ((value i32)) -> i32
|
||||
value)
|
||||
|
||||
(fn make ((x i32) (label string)) -> Point
|
||||
(Point (x x) (label label)))
|
||||
|
||||
(test "make is documented"
|
||||
true)
|
||||
"#;
|
||||
let file = write_file("file-api", source);
|
||||
let docs = unique_path("file-api-docs");
|
||||
|
||||
let output = run_glagol([
|
||||
OsStr::new("doc"),
|
||||
file.as_os_str(),
|
||||
OsStr::new("-o"),
|
||||
docs.as_os_str(),
|
||||
]);
|
||||
|
||||
assert_success("doc file", &output);
|
||||
let index = read_index(&docs);
|
||||
assert!(index.contains("## Module api"));
|
||||
assert!(index.contains("### Imports\n\nNone.\n\n"));
|
||||
assert!(index.contains("- `Point`"));
|
||||
assert!(index.contains("- `make(x i32, label string) -> Point`"));
|
||||
assert!(index.contains("- `make is documented`"));
|
||||
|
||||
let api = public_api_for_module(&index, "api");
|
||||
assert!(api.contains("- `fn make(x: i32, label: string) -> Point`"));
|
||||
assert!(api.contains("- `struct Point`"));
|
||||
assert!(api.contains(" - `x: i32`"));
|
||||
assert!(api.contains(" - `label: string`"));
|
||||
assert!(api.contains("- `enum Status`"));
|
||||
assert!(api.contains(" - `Ready`"));
|
||||
assert!(api.contains(" - `Blocked(i32)`"));
|
||||
assert!(
|
||||
!api.contains("helper"),
|
||||
"non-exported helper leaked into public API:\n{}",
|
||||
api
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doc_project_renders_package_and_module_public_api() {
|
||||
let project = write_project(
|
||||
"project-api",
|
||||
&[(
|
||||
"math",
|
||||
r#"(module math (export add Pair))
|
||||
|
||||
(struct Pair
|
||||
(left i32)
|
||||
(right i32))
|
||||
|
||||
(fn add ((left i32) (right i32)) -> i32
|
||||
(+ left right))
|
||||
|
||||
(fn private_double ((value i32)) -> i32
|
||||
(+ value value))
|
||||
"#,
|
||||
)],
|
||||
"(module main)\n\n(import math (add Pair))\n\n(fn main () -> i32\n (add 1 2))\n",
|
||||
);
|
||||
let docs = unique_path("project-api-docs");
|
||||
|
||||
let output = run_glagol([
|
||||
OsStr::new("doc"),
|
||||
project.as_os_str(),
|
||||
OsStr::new("-o"),
|
||||
docs.as_os_str(),
|
||||
]);
|
||||
|
||||
assert_success("doc project", &output);
|
||||
let index = read_index(&docs);
|
||||
assert!(index.contains("# Project project-api"));
|
||||
assert!(index.contains("## Package API project-api"));
|
||||
assert!(index.contains("## Module math"));
|
||||
assert!(index.contains("## Module main"));
|
||||
assert!(index.contains("- `math`"));
|
||||
assert!(index.contains("- `add`"));
|
||||
|
||||
let package_api = package_api(&index, "project-api");
|
||||
assert!(package_api.contains("### Module math"));
|
||||
assert!(package_api.contains("- `fn add(left: i32, right: i32) -> i32`"));
|
||||
assert!(package_api.contains("- `struct Pair`"));
|
||||
assert!(
|
||||
!package_api.contains("private_double"),
|
||||
"non-exported function leaked into package API:\n{}",
|
||||
package_api
|
||||
);
|
||||
|
||||
let math_api = public_api_for_module(&index, "math");
|
||||
assert!(math_api.contains("- `fn add(left: i32, right: i32) -> i32`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doc_workspace_renders_each_package_api_deterministically() {
|
||||
let workspace = unique_path("workspace-api");
|
||||
let scaffold = run_glagol([
|
||||
OsStr::new("new"),
|
||||
workspace.as_os_str(),
|
||||
OsStr::new("--template"),
|
||||
OsStr::new("workspace"),
|
||||
]);
|
||||
assert_success("workspace scaffold", &scaffold);
|
||||
|
||||
let docs = unique_path("workspace-api-docs");
|
||||
let output = run_glagol([
|
||||
OsStr::new("doc"),
|
||||
workspace.as_os_str(),
|
||||
OsStr::new("-o"),
|
||||
docs.as_os_str(),
|
||||
]);
|
||||
|
||||
assert_success("doc workspace", &output);
|
||||
let index = read_index(&docs);
|
||||
assert!(index.contains("## Workspace"));
|
||||
assert!(index.contains("- `packages/app`"));
|
||||
assert!(index.contains("- `packages/libutil`"));
|
||||
assert!(index.contains("## Package API app 0.1.0"));
|
||||
assert!(index.contains("## Package API libutil 0.1.0"));
|
||||
|
||||
let app_api = package_api(&index, "app 0.1.0");
|
||||
assert!(app_api.contains("None."));
|
||||
|
||||
let lib_api = package_api(&index, "libutil 0.1.0");
|
||||
assert!(lib_api.contains("### Module libutil"));
|
||||
assert!(lib_api.contains("- `fn answer() -> i32`"));
|
||||
assert!(lib_api.contains("- `fn label() -> string`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doc_workspace_package_api_excludes_loaded_std_modules() {
|
||||
let workspace = write_workspace_with_std_import("workspace-std-api");
|
||||
let docs = unique_path("workspace-std-api-docs");
|
||||
|
||||
let output = run_glagol([
|
||||
OsStr::new("doc"),
|
||||
workspace.as_os_str(),
|
||||
OsStr::new("-o"),
|
||||
docs.as_os_str(),
|
||||
]);
|
||||
|
||||
assert_success("doc workspace std import", &output);
|
||||
let index = read_index(&docs);
|
||||
assert!(
|
||||
index.contains("## Module option"),
|
||||
"module summaries should still include loaded std module docs:\n{}",
|
||||
index
|
||||
);
|
||||
|
||||
let app_api = package_api(&index, "app 0.1.0");
|
||||
assert!(app_api.contains("### Module main"));
|
||||
assert!(app_api.contains("- `fn local_some(value: i32) -> (option i32)`"));
|
||||
assert!(
|
||||
!app_api.contains("Module std.option") && !app_api.contains("Module option"),
|
||||
"loaded std module leaked into package API:\n{}",
|
||||
app_api
|
||||
);
|
||||
assert!(
|
||||
!app_api.contains("some_i32"),
|
||||
"loaded std helper leaked into package API:\n{}",
|
||||
app_api
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_api_normalizes_local_aliases_and_omits_alias_exports() {
|
||||
let source = r#"(module aliases (export Count Score Status measure))
|
||||
|
||||
(type Count i32)
|
||||
(type MaybeCount (option Count))
|
||||
|
||||
(struct Score
|
||||
(value Count)
|
||||
(maybe MaybeCount))
|
||||
|
||||
(enum Status Ready (Blocked Count) (Maybe MaybeCount))
|
||||
|
||||
(fn hidden ((value Count)) -> Count
|
||||
value)
|
||||
|
||||
(fn measure ((value Count) (maybe MaybeCount)) -> Count
|
||||
value)
|
||||
"#;
|
||||
let file = write_file("alias-api", source);
|
||||
let docs = unique_path("alias-api-docs");
|
||||
|
||||
let output = run_glagol([
|
||||
OsStr::new("doc"),
|
||||
file.as_os_str(),
|
||||
OsStr::new("-o"),
|
||||
docs.as_os_str(),
|
||||
]);
|
||||
|
||||
assert_success("doc aliases", &output);
|
||||
let index = read_index(&docs);
|
||||
assert!(
|
||||
index.contains("- `Count`"),
|
||||
"exports summary should retain the alias name"
|
||||
);
|
||||
assert!(
|
||||
index.contains("- `hidden(value Count) -> Count`"),
|
||||
"function summary should retain non-public declarations"
|
||||
);
|
||||
|
||||
let api = public_api_for_module(&index, "aliases");
|
||||
assert!(api.contains("- `fn measure(value: i32, maybe: (option i32)) -> i32`"));
|
||||
assert!(api.contains(" - `value: i32`"));
|
||||
assert!(api.contains(" - `maybe: (option i32)`"));
|
||||
assert!(api.contains(" - `Blocked(i32)`"));
|
||||
assert!(api.contains(" - `Maybe((option i32))`"));
|
||||
assert!(
|
||||
!api.contains("Count"),
|
||||
"alias names leaked into public API:\n{}",
|
||||
api
|
||||
);
|
||||
assert!(
|
||||
!api.contains("hidden"),
|
||||
"non-exported function leaked into public API:\n{}",
|
||||
api
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_doc_generation_is_byte_identical() {
|
||||
let source = r#"(module stable (export value))
|
||||
|
||||
(fn value () -> i32
|
||||
42)
|
||||
"#;
|
||||
let file = write_file("stable-api", source);
|
||||
let docs = unique_path("stable-api-docs");
|
||||
|
||||
let first = run_glagol([
|
||||
OsStr::new("doc"),
|
||||
file.as_os_str(),
|
||||
OsStr::new("-o"),
|
||||
docs.as_os_str(),
|
||||
]);
|
||||
assert_success("first doc", &first);
|
||||
let first_bytes = fs::read(docs.join("index.md")).expect("read first docs");
|
||||
|
||||
let second = run_glagol([
|
||||
OsStr::new("doc"),
|
||||
file.as_os_str(),
|
||||
OsStr::new("-o"),
|
||||
docs.as_os_str(),
|
||||
]);
|
||||
assert_success("second doc", &second);
|
||||
let second_bytes = fs::read(docs.join("index.md")).expect("read second docs");
|
||||
|
||||
assert_eq!(first_bytes, second_bytes);
|
||||
}
|
||||
|
||||
fn write_project(name: &str, modules: &[(&str, &str)], main: &str) -> PathBuf {
|
||||
let project = unique_path(name);
|
||||
fs::create_dir_all(project.join("src")).expect("create project src");
|
||||
fs::write(
|
||||
project.join("slovo.toml"),
|
||||
format!(
|
||||
"[project]\nname = \"{}\"\nsource_root = \"src\"\nentry = \"main\"\n",
|
||||
name
|
||||
),
|
||||
)
|
||||
.expect("write manifest");
|
||||
for (module, source) in modules {
|
||||
fs::write(project.join("src").join(format!("{}.slo", module)), source)
|
||||
.expect("write module");
|
||||
}
|
||||
fs::write(project.join("src/main.slo"), main).expect("write main");
|
||||
project
|
||||
}
|
||||
|
||||
fn write_workspace_with_std_import(name: &str) -> PathBuf {
|
||||
let workspace = unique_path(name);
|
||||
let package = workspace.join("packages/app");
|
||||
fs::create_dir_all(package.join("src")).expect("create workspace package src");
|
||||
fs::write(
|
||||
workspace.join("slovo.toml"),
|
||||
"[workspace]\nmembers = [\"packages/app\"]\ndefault_package = \"app\"\n",
|
||||
)
|
||||
.expect("write workspace manifest");
|
||||
fs::write(
|
||||
package.join("slovo.toml"),
|
||||
"[package]\nname = \"app\"\nversion = \"0.1.0\"\nsource_root = \"src\"\nentry = \"main\"\n",
|
||||
)
|
||||
.expect("write package manifest");
|
||||
fs::write(
|
||||
package.join("src/main.slo"),
|
||||
r#"(module main (export local_some))
|
||||
|
||||
(import std.option (some_i32))
|
||||
|
||||
(fn local_some ((value i32)) -> (option i32)
|
||||
(some_i32 value))
|
||||
"#,
|
||||
)
|
||||
.expect("write package main");
|
||||
workspace
|
||||
}
|
||||
|
||||
fn write_file(name: &str, source: &str) -> PathBuf {
|
||||
let path = unique_path(name).with_extension("slo");
|
||||
fs::write(&path, source).expect("write fixture");
|
||||
path
|
||||
}
|
||||
|
||||
fn read_index(docs: &Path) -> String {
|
||||
fs::read_to_string(docs.join("index.md")).expect("read generated docs")
|
||||
}
|
||||
|
||||
fn package_api<'a>(docs: &'a str, package: &str) -> &'a str {
|
||||
let heading = format!("## Package API {}", package);
|
||||
let start = docs
|
||||
.find(&heading)
|
||||
.unwrap_or_else(|| panic!("missing package API heading `{}`\n{}", heading, docs));
|
||||
let rest = &docs[start..];
|
||||
let end = rest
|
||||
.find("\n## Package API ")
|
||||
.or_else(|| rest.find("\n## Module "))
|
||||
.unwrap_or(rest.len());
|
||||
&rest[..end]
|
||||
}
|
||||
|
||||
fn public_api_for_module<'a>(docs: &'a str, module: &str) -> &'a str {
|
||||
let heading = format!("## Module {}", module);
|
||||
let module_start = if docs.starts_with(&heading) {
|
||||
0
|
||||
} else {
|
||||
let marker = format!("\n{}", heading);
|
||||
docs.find(&marker)
|
||||
.map(|index| index + 1)
|
||||
.unwrap_or_else(|| panic!("missing module heading `{}`\n{}", heading, docs))
|
||||
};
|
||||
let module_docs = &docs[module_start..];
|
||||
let module_end = module_docs
|
||||
.find("\n## Module ")
|
||||
.unwrap_or(module_docs.len());
|
||||
let module_docs = &module_docs[..module_end];
|
||||
let public_start = module_docs.find("### Public API").unwrap_or_else(|| {
|
||||
panic!(
|
||||
"missing public API for module `{}`\n{}",
|
||||
module, module_docs
|
||||
)
|
||||
});
|
||||
let public_docs = &module_docs[public_start..];
|
||||
let public_end = public_docs
|
||||
.find("\n### Structs")
|
||||
.unwrap_or(public_docs.len());
|
||||
&public_docs[..public_end]
|
||||
}
|
||||
|
||||
fn unique_path(name: &str) -> PathBuf {
|
||||
let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
|
||||
let nanos = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.expect("system clock before UNIX_EPOCH")
|
||||
.as_nanos();
|
||||
std::env::temp_dir().join(format!(
|
||||
"glagol-doc-api-beta11-{}-{}-{}-{}",
|
||||
std::process::id(),
|
||||
nanos,
|
||||
id,
|
||||
name
|
||||
))
|
||||
}
|
||||
|
||||
fn run_glagol<I, S>(args: I) -> Output
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<OsStr>,
|
||||
{
|
||||
Command::new(env!("CARGO_BIN_EXE_glagol"))
|
||||
.args(args)
|
||||
.output()
|
||||
.expect("run glagol")
|
||||
}
|
||||
|
||||
fn assert_success(context: &str, output: &Output) {
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{} failed\nstdout:\n{}\nstderr:\n{}",
|
||||
context,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
assert!(
|
||||
output.stdout.is_empty(),
|
||||
"{} wrote stdout:\n{}",
|
||||
context,
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
);
|
||||
assert!(
|
||||
output.stderr.is_empty(),
|
||||
"{} wrote stderr:\n{}",
|
||||
context,
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
@ -70,6 +70,179 @@ fn new_creates_minimal_valid_project() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_builds_executes_and_clean_removes_generated_artifacts() {
|
||||
let project = unique_path("run-project");
|
||||
|
||||
let new_output = run_glagol(["new".as_ref(), project.as_os_str()]);
|
||||
assert_success("glagol new for run", &new_output);
|
||||
|
||||
let run = run_glagol(["run".as_ref(), project.as_os_str()]);
|
||||
if run.status.success() {
|
||||
assert!(run.stdout.is_empty(), "run wrote stdout");
|
||||
assert!(run.stderr.is_empty(), "run wrote stderr");
|
||||
assert!(
|
||||
project.join(".slovo/build").is_dir(),
|
||||
"run did not create generated build directory"
|
||||
);
|
||||
} else {
|
||||
assert_stderr_contains("generated project run", &run, "ToolchainUnavailable");
|
||||
fs::create_dir_all(project.join(".slovo/build")).expect("create synthetic build dir");
|
||||
fs::write(project.join(".slovo/build/stale"), "").expect("write stale build file");
|
||||
}
|
||||
|
||||
let clean = run_glagol(["clean".as_ref(), project.as_os_str()]);
|
||||
assert_success("glagol clean", &clean);
|
||||
assert!(clean.stdout.is_empty(), "clean wrote stdout");
|
||||
assert!(clean.stderr.is_empty(), "clean wrote stderr");
|
||||
assert!(
|
||||
!project.join(".slovo/build").exists(),
|
||||
"clean left generated build directory behind"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_forwards_program_arguments_when_host_toolchain_is_available() {
|
||||
let project = write_project(
|
||||
"run-args-project",
|
||||
&[],
|
||||
"(module main)\n\n(import std.process (argc))\n\n(fn main () -> i32\n (if (= (argc) 3)\n 0\n 1))\n",
|
||||
);
|
||||
|
||||
let run = run_glagol([
|
||||
"run".as_ref(),
|
||||
project.as_os_str(),
|
||||
"--".as_ref(),
|
||||
"alpha".as_ref(),
|
||||
"beta".as_ref(),
|
||||
]);
|
||||
|
||||
if run.status.success() {
|
||||
assert!(run.stdout.is_empty(), "run args wrote stdout");
|
||||
assert!(run.stderr.is_empty(), "run args wrote stderr");
|
||||
} else {
|
||||
assert_stderr_contains("run args project", &run, "ToolchainUnavailable");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installed_layout_discovers_std_and_runtime_without_checkout_paths() {
|
||||
let prefix = unique_path("installed-layout");
|
||||
let bin_dir = prefix.join("bin");
|
||||
let std_dir = prefix.join("share/slovo/std");
|
||||
let runtime_dir = prefix.join("share/slovo/runtime");
|
||||
fs::create_dir_all(&bin_dir).expect("create installed bin dir");
|
||||
fs::create_dir_all(&std_dir).expect("create installed std dir");
|
||||
fs::create_dir_all(&runtime_dir).expect("create installed runtime dir");
|
||||
|
||||
let installed_glagol = bin_dir.join(format!("glagol{}", std::env::consts::EXE_SUFFIX));
|
||||
fs::copy(env!("CARGO_BIN_EXE_glagol"), &installed_glagol).expect("copy glagol");
|
||||
make_executable(&installed_glagol);
|
||||
|
||||
let repo_root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.expect("compiler has repo parent");
|
||||
fs::copy(
|
||||
repo_root.join("runtime/runtime.c"),
|
||||
runtime_dir.join("runtime.c"),
|
||||
)
|
||||
.expect("copy runtime");
|
||||
for entry in fs::read_dir(repo_root.join("lib/std")).expect("read std dir") {
|
||||
let entry = entry.expect("read std entry");
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|ext| ext.to_str()) == Some("slo") {
|
||||
fs::copy(
|
||||
&path,
|
||||
std_dir.join(path.file_name().expect("std file name")),
|
||||
)
|
||||
.expect("copy std module");
|
||||
}
|
||||
}
|
||||
|
||||
let project = write_project(
|
||||
"installed-layout-project",
|
||||
&[],
|
||||
"(module main)\n\n(import std.io (print_string_zero))\n(import std.string (concat))\n\n(fn main () -> i32\n (print_string_zero (concat \"installed\" \"-ok\")))\n",
|
||||
);
|
||||
|
||||
let check = run_installed_glagol(&installed_glagol, ["check".as_ref(), project.as_os_str()]);
|
||||
assert_success("installed layout check", &check);
|
||||
|
||||
let run = run_installed_glagol(&installed_glagol, ["run".as_ref(), project.as_os_str()]);
|
||||
if host_clang_available() {
|
||||
assert_success("installed layout run", &run);
|
||||
assert_eq!(String::from_utf8_lossy(&run.stdout), "installed-ok\n");
|
||||
assert!(run.stderr.is_empty(), "installed run wrote stderr");
|
||||
} else {
|
||||
assert_exit_code("installed layout run without clang", &run, 3);
|
||||
assert_stderr_contains(
|
||||
"installed layout run without clang",
|
||||
&run,
|
||||
"ToolchainUnavailable",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_library_template_creates_checkable_testable_library_project() {
|
||||
let project = unique_path("library-template");
|
||||
|
||||
let output = run_glagol([
|
||||
"new".as_ref(),
|
||||
project.as_os_str(),
|
||||
"--template".as_ref(),
|
||||
"library".as_ref(),
|
||||
"--name".as_ref(),
|
||||
"numbers".as_ref(),
|
||||
]);
|
||||
|
||||
assert_success("glagol new --template library", &output);
|
||||
assert_eq!(
|
||||
fs::read_to_string(project.join("slovo.toml")).expect("read library manifest"),
|
||||
"[project]\nname = \"numbers\"\nsource_root = \"src\"\nentry = \"lib\"\n"
|
||||
);
|
||||
let source = fs::read_to_string(project.join("src/lib.slo")).expect("read library source");
|
||||
assert!(source.contains("(module lib (export answer double))"));
|
||||
|
||||
let check = run_glagol(["check".as_ref(), project.as_os_str()]);
|
||||
assert_success("library template check", &check);
|
||||
let test = run_glagol(["test".as_ref(), project.as_os_str()]);
|
||||
assert_success("library template test", &test);
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&test.stdout),
|
||||
"test \"answer is stable\" ... ok\ntest \"double works\" ... ok\n2 test(s) passed\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_workspace_template_creates_local_package_workspace() {
|
||||
let workspace = unique_path("workspace-template");
|
||||
|
||||
let output = run_glagol([
|
||||
"new".as_ref(),
|
||||
workspace.as_os_str(),
|
||||
"--template".as_ref(),
|
||||
"workspace".as_ref(),
|
||||
]);
|
||||
|
||||
assert_success("glagol new --template workspace", &output);
|
||||
assert_eq!(
|
||||
fs::read_to_string(workspace.join("slovo.toml")).expect("read workspace manifest"),
|
||||
"[workspace]\nmembers = [\"packages/app\", \"packages/libutil\"]\ndefault_package = \"app\"\n"
|
||||
);
|
||||
assert!(workspace.join("packages/app/slovo.toml").is_file());
|
||||
assert!(workspace.join("packages/libutil/slovo.toml").is_file());
|
||||
|
||||
let check = run_glagol(["check".as_ref(), workspace.as_os_str()]);
|
||||
assert_success("workspace template check", &check);
|
||||
let test = run_glagol(["test".as_ref(), workspace.as_os_str()]);
|
||||
assert_success("workspace template test", &test);
|
||||
let stdout = String::from_utf8_lossy(&test.stdout);
|
||||
assert!(stdout.contains("test \"app uses libutil\" ... ok"));
|
||||
assert!(stdout.contains("test \"answer is stable\" ... ok"));
|
||||
assert!(stdout.contains("2 test(s) passed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rejects_non_empty_target_with_structured_diagnostic() {
|
||||
let project = unique_path("new-non-empty");
|
||||
@ -178,6 +351,38 @@ fn doc_generates_markdown_for_file_and_project() {
|
||||
assert!(project_index.contains("- `math`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doc_generates_workspace_package_summary() {
|
||||
let workspace = unique_path("doc-workspace");
|
||||
let new_output = run_glagol([
|
||||
"new".as_ref(),
|
||||
workspace.as_os_str(),
|
||||
"--template".as_ref(),
|
||||
"workspace".as_ref(),
|
||||
]);
|
||||
assert_success("doc workspace scaffold", &new_output);
|
||||
|
||||
let workspace_docs = unique_path("doc-workspace-out");
|
||||
let doc_output = run_glagol([
|
||||
"doc".as_ref(),
|
||||
workspace.as_os_str(),
|
||||
"-o".as_ref(),
|
||||
workspace_docs.as_os_str(),
|
||||
]);
|
||||
assert_success("doc workspace", &doc_output);
|
||||
|
||||
let index = fs::read_to_string(workspace_docs.join("index.md")).expect("read workspace docs");
|
||||
assert!(index.contains("## Workspace"));
|
||||
assert!(index.contains("### Members"));
|
||||
assert!(index.contains("- `packages/app`"));
|
||||
assert!(index.contains("- `packages/libutil`"));
|
||||
assert!(index.contains("### Packages"));
|
||||
assert!(index.contains("- `app 0.1.0 (entry main)`"));
|
||||
assert!(index.contains("- `libutil 0.1.0 (entry main)`"));
|
||||
assert!(index.contains("### Package Dependencies"));
|
||||
assert!(index.contains("- `app -> libutil (local-path, packages/libutil)`"));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn project_tooling_rejects_symlinked_module_escape() {
|
||||
@ -214,6 +419,7 @@ fn release_gate_script_exists_and_names_required_commands() {
|
||||
let script = Path::new("../scripts/release-gate.sh");
|
||||
let text = fs::read_to_string(script).expect("read release gate script");
|
||||
assert!(text.contains("git diff --check"));
|
||||
assert!(text.contains("scripts/install.sh"));
|
||||
assert!(text.contains("cargo fmt --check"));
|
||||
assert!(text.contains("cargo test"));
|
||||
assert!(text.contains("dx_v1_7"));
|
||||
@ -275,6 +481,39 @@ where
|
||||
.expect("run glagol")
|
||||
}
|
||||
|
||||
fn run_installed_glagol<I, S>(binary: &Path, args: I) -> Output
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<std::ffi::OsStr>,
|
||||
{
|
||||
Command::new(binary)
|
||||
.env_remove("SLOVO_STD_PATH")
|
||||
.env_remove("SLOVO_RUNTIME_C")
|
||||
.env_remove("GLAGOL_RUNTIME_C")
|
||||
.args(args)
|
||||
.output()
|
||||
.expect("run installed glagol")
|
||||
}
|
||||
|
||||
fn host_clang_available() -> bool {
|
||||
let clang = std::env::var("GLAGOL_CLANG").unwrap_or_else(|_| "clang".to_string());
|
||||
Command::new(clang)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|output| output.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn make_executable(path: &Path) {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut permissions = fs::metadata(path).expect("stat executable").permissions();
|
||||
permissions.set_mode(0o755);
|
||||
fs::set_permissions(path, permissions).expect("chmod executable");
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_success(context: &str, output: &Output) {
|
||||
assert!(
|
||||
output.status.success(),
|
||||
|
||||
@ -334,6 +334,103 @@ fn formatter_reports_unsupported_standard_library_calls() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formatter_rejects_reserved_generic_collection_syntax() {
|
||||
let compiler = env!("CARGO_BIN_EXE_glagol");
|
||||
let cases = [
|
||||
(
|
||||
"generic-function",
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(fn id (type_params T) ((value T)) -> T
|
||||
value)
|
||||
"#,
|
||||
"UnsupportedGenericFunction",
|
||||
),
|
||||
(
|
||||
"generic-type-alias",
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(type VecOf (type_params T) (vec T))
|
||||
"#,
|
||||
"UnsupportedGenericTypeAlias",
|
||||
),
|
||||
(
|
||||
"generic-type-parameter",
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(let xs (vec T) (std.vec.i32.empty))
|
||||
0)
|
||||
"#,
|
||||
"UnsupportedGenericTypeParameter",
|
||||
),
|
||||
(
|
||||
"map-type",
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> (map string i32)
|
||||
0)
|
||||
"#,
|
||||
"UnsupportedMapType",
|
||||
),
|
||||
(
|
||||
"set-type",
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> (set string)
|
||||
0)
|
||||
"#,
|
||||
"UnsupportedSetType",
|
||||
),
|
||||
(
|
||||
"generic-std-call",
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(std.vec.empty i32)
|
||||
0)
|
||||
"#,
|
||||
"UnsupportedGenericStandardLibraryCall",
|
||||
),
|
||||
];
|
||||
|
||||
for (name, source, code) in cases {
|
||||
let fixture = write_fixture(name, source);
|
||||
let output = run_formatter(compiler, &fixture);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"formatter unexpectedly accepted reserved generic syntax `{}`\nstdout:\n{}\nstderr:\n{}",
|
||||
name,
|
||||
stdout,
|
||||
stderr,
|
||||
);
|
||||
assert!(
|
||||
stdout.is_empty(),
|
||||
"formatter emitted stdout for reserved generic syntax `{}`\nstdout:\n{}\nstderr:\n{}",
|
||||
name,
|
||||
stdout,
|
||||
stderr,
|
||||
);
|
||||
assert!(
|
||||
stderr.contains(code),
|
||||
"formatter stderr did not contain {} for `{}`\nstderr:\n{}",
|
||||
code,
|
||||
name,
|
||||
stderr,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formatter_preserves_full_line_comments_inside_function_bodies() {
|
||||
let compiler = env!("CARGO_BIN_EXE_glagol");
|
||||
|
||||
@ -13,6 +13,12 @@ const LOWERING_FIXTURES: &[LoweringFixture] = &[
|
||||
surface_snapshot: "../tests/top-level-test.surface.lower",
|
||||
checked_snapshot: "../tests/top-level-test.checked.lower",
|
||||
},
|
||||
LoweringFixture {
|
||||
name: "type-aliases",
|
||||
source: "../tests/type-aliases.slo",
|
||||
surface_snapshot: "../tests/type-aliases.surface.lower",
|
||||
checked_snapshot: "../tests/type-aliases.checked.lower",
|
||||
},
|
||||
LoweringFixture {
|
||||
name: "comments",
|
||||
source: "../tests/comments.slo",
|
||||
|
||||
229
compiler/tests/package_workspace_discipline_beta24.rs
Normal file
229
compiler/tests/package_workspace_discipline_beta24.rs
Normal file
@ -0,0 +1,229 @@
|
||||
use std::{
|
||||
fs,
|
||||
path::PathBuf,
|
||||
process::{Command, Output},
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
static NEXT_WORKSPACE_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[test]
|
||||
fn duplicate_package_keys_report_package_manifest_invalid() {
|
||||
let workspace = write_workspace(
|
||||
"duplicate-package-key",
|
||||
"[workspace]\nmembers = [\"packages/app\"]\n",
|
||||
&[WorkspacePackageSpec {
|
||||
member: "packages/app",
|
||||
manifest: "[package]\nname = \"app\"\nname = \"other\"\nversion = \"0.1.0\"\n",
|
||||
modules: &[("main", "(module main)\n\n(fn main () -> i32\n 0)\n")],
|
||||
}],
|
||||
);
|
||||
|
||||
let output = run_glagol([
|
||||
"--json-diagnostics".as_ref(),
|
||||
"check".as_ref(),
|
||||
workspace.as_os_str(),
|
||||
]);
|
||||
assert_exit_code("duplicate package key", &output, 1);
|
||||
assert_json_diagnostic_code("duplicate package key", &output, "PackageManifestInvalid");
|
||||
assert_json_diagnostic_code_absent("duplicate package key", &output, "ProjectManifestInvalid");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_dependency_key_reports_invalid_package_dependency_name() {
|
||||
let workspace = write_workspace(
|
||||
"invalid-dependency-key",
|
||||
"[workspace]\nmembers = [\"packages/app\"]\n",
|
||||
&[WorkspacePackageSpec {
|
||||
member: "packages/app",
|
||||
manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nBad_Name = { path = \"../util\" }\n",
|
||||
modules: &[("main", "(module main)\n\n(fn main () -> i32\n 0)\n")],
|
||||
}],
|
||||
);
|
||||
|
||||
let output = run_glagol([
|
||||
"--json-diagnostics".as_ref(),
|
||||
"check".as_ref(),
|
||||
workspace.as_os_str(),
|
||||
]);
|
||||
assert_exit_code("invalid dependency key", &output, 1);
|
||||
assert_json_diagnostic_code(
|
||||
"invalid dependency key",
|
||||
&output,
|
||||
"InvalidPackageDependencyName",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_dependency_keys_report_duplicate_package_dependency_name() {
|
||||
let workspace = write_workspace(
|
||||
"duplicate-dependency-key",
|
||||
"[workspace]\nmembers = [\"packages/app\"]\n",
|
||||
&[WorkspacePackageSpec {
|
||||
member: "packages/app",
|
||||
manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nutil = { path = \"../util\" }\nutil = { path = \"../util-again\" }\n",
|
||||
modules: &[("main", "(module main)\n\n(fn main () -> i32\n 0)\n")],
|
||||
}],
|
||||
);
|
||||
|
||||
let output = run_glagol([
|
||||
"--json-diagnostics".as_ref(),
|
||||
"check".as_ref(),
|
||||
workspace.as_os_str(),
|
||||
]);
|
||||
assert_exit_code("duplicate dependency key", &output, 1);
|
||||
assert_json_diagnostic_code(
|
||||
"duplicate dependency key",
|
||||
&output,
|
||||
"DuplicatePackageDependencyName",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_dependency_identity_checks_cleanly() {
|
||||
let workspace = write_workspace(
|
||||
"valid-dependency-identity",
|
||||
"[workspace]\nmembers = [\"packages/app\", \"packages/util\"]\n",
|
||||
&[
|
||||
WorkspacePackageSpec {
|
||||
member: "packages/util",
|
||||
manifest: "[package]\nname = \"util\"\nversion = \"0.1.0\"\n",
|
||||
modules: &[(
|
||||
"util",
|
||||
"(module util (export answer))\n\n(fn answer () -> i32\n 42)\n",
|
||||
)],
|
||||
},
|
||||
WorkspacePackageSpec {
|
||||
member: "packages/app",
|
||||
manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nutil = { path = \"../util\" }\n",
|
||||
modules: &[(
|
||||
"main",
|
||||
"(module main)\n\n(import util.util (answer))\n\n(fn main () -> i32\n (answer))\n",
|
||||
)],
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
let output = run_glagol(["check".as_ref(), workspace.as_os_str()]);
|
||||
assert_success_stdout("valid dependency identity", output, "");
|
||||
}
|
||||
|
||||
struct WorkspacePackageSpec<'a> {
|
||||
member: &'a str,
|
||||
manifest: &'a str,
|
||||
modules: &'a [(&'a str, &'a str)],
|
||||
}
|
||||
|
||||
fn write_workspace(
|
||||
name: &str,
|
||||
workspace_manifest: &str,
|
||||
packages: &[WorkspacePackageSpec<'_>],
|
||||
) -> PathBuf {
|
||||
let root = unique_path(name);
|
||||
fs::create_dir_all(&root).expect("create workspace root");
|
||||
fs::write(root.join("slovo.toml"), workspace_manifest).expect("write workspace manifest");
|
||||
|
||||
for package in packages {
|
||||
let package_root = root.join(package.member);
|
||||
let src = package_root.join("src");
|
||||
fs::create_dir_all(&src).expect("create workspace package src");
|
||||
fs::write(package_root.join("slovo.toml"), package.manifest)
|
||||
.expect("write workspace package manifest");
|
||||
for (module, source) in package.modules {
|
||||
fs::write(src.join(format!("{}.slo", module)), source)
|
||||
.expect("write workspace package module");
|
||||
}
|
||||
}
|
||||
|
||||
root
|
||||
}
|
||||
|
||||
fn unique_path(name: &str) -> PathBuf {
|
||||
let id = NEXT_WORKSPACE_ID.fetch_add(1, Ordering::SeqCst);
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_nanos())
|
||||
.unwrap_or(0);
|
||||
std::env::temp_dir().join(format!(
|
||||
"glagol-package-workspace-discipline-beta24-{}-{}-{}-{}",
|
||||
std::process::id(),
|
||||
nanos,
|
||||
id,
|
||||
name
|
||||
))
|
||||
}
|
||||
|
||||
fn run_glagol<I, S>(args: I) -> Output
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<std::ffi::OsStr>,
|
||||
{
|
||||
Command::new(env!("CARGO_BIN_EXE_glagol"))
|
||||
.args(args)
|
||||
.output()
|
||||
.expect("run glagol")
|
||||
}
|
||||
|
||||
fn assert_success_stdout(context: &str, output: Output, expected: &str) {
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{} failed\nstdout:\n{}\nstderr:\n{}",
|
||||
context,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
expected,
|
||||
"{} stdout mismatch",
|
||||
context
|
||||
);
|
||||
assert!(
|
||||
output.stderr.is_empty(),
|
||||
"{} wrote stderr:\n{}",
|
||||
context,
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_exit_code(context: &str, output: &Output, expected: i32) {
|
||||
assert_eq!(
|
||||
output.status.code(),
|
||||
Some(expected),
|
||||
"{} exit code mismatch\nstdout:\n{}\nstderr:\n{}",
|
||||
context,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_json_diagnostic_code(context: &str, output: &Output, expected: &str) {
|
||||
let diagnostics = diagnostic_text(output);
|
||||
assert!(
|
||||
diagnostics.contains(&format!(r#""code":"{}""#, expected)),
|
||||
"{} did not report `{}`:\n{}",
|
||||
context,
|
||||
expected,
|
||||
diagnostics
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_json_diagnostic_code_absent(context: &str, output: &Output, unexpected: &str) {
|
||||
let diagnostics = diagnostic_text(output);
|
||||
assert!(
|
||||
!diagnostics.contains(&format!(r#""code":"{}""#, unexpected)),
|
||||
"{} unexpectedly reported `{}`:\n{}",
|
||||
context,
|
||||
unexpected,
|
||||
diagnostics
|
||||
);
|
||||
}
|
||||
|
||||
fn diagnostic_text(output: &Output) -> String {
|
||||
format!(
|
||||
"{}{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
)
|
||||
}
|
||||
@ -587,6 +587,117 @@ fn workspace_build_requires_one_entry_package() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_default_package_selects_build_entry() {
|
||||
let workspace = write_workspace(
|
||||
"workspace-default-build-entry",
|
||||
"[workspace]\nmembers = [\"packages/app\", \"packages/tool\"]\ndefault_package = \"app\"\n",
|
||||
&[
|
||||
WorkspacePackageSpec {
|
||||
member: "packages/app",
|
||||
manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n",
|
||||
modules: &[("main", "(module main)\n\n(fn main () -> i32\n 0)\n")],
|
||||
},
|
||||
WorkspacePackageSpec {
|
||||
member: "packages/tool",
|
||||
manifest: "[package]\nname = \"tool\"\nversion = \"0.1.0\"\n",
|
||||
modules: &[("main", "(module main)\n\n(fn main () -> i32\n 7)\n")],
|
||||
},
|
||||
],
|
||||
);
|
||||
let binary = unique_path("workspace-default-build-bin");
|
||||
|
||||
let output = run_glagol([
|
||||
"build".as_ref(),
|
||||
"-o".as_ref(),
|
||||
binary.as_os_str(),
|
||||
workspace.as_os_str(),
|
||||
]);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(
|
||||
!stderr.contains("WorkspaceBuildAmbiguousEntryPackage"),
|
||||
"default package should avoid ambiguous build entry diagnostic:\n{}",
|
||||
stderr
|
||||
);
|
||||
|
||||
if output.status.code() == Some(3) {
|
||||
assert_stderr_contains(
|
||||
"workspace default package build toolchain",
|
||||
&output,
|
||||
"ToolchainUnavailable",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
assert_success("workspace default package build", &output);
|
||||
let run = Command::new(&binary)
|
||||
.output()
|
||||
.expect("run workspace default package build output");
|
||||
assert_success("workspace default package build binary", &run);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_build_reports_entry_main_contract() {
|
||||
let missing_main = write_workspace(
|
||||
"workspace-missing-entry-main",
|
||||
"[workspace]\nmembers = [\"packages/app\"]\n",
|
||||
&[WorkspacePackageSpec {
|
||||
member: "packages/app",
|
||||
manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n",
|
||||
modules: &[("main", "(module main)\n\n(test \"ok\"\n true)\n")],
|
||||
}],
|
||||
);
|
||||
let missing_binary = unique_path("workspace-missing-entry-main-bin");
|
||||
let missing_output = run_glagol([
|
||||
"build".as_ref(),
|
||||
"-o".as_ref(),
|
||||
missing_binary.as_os_str(),
|
||||
missing_main.as_os_str(),
|
||||
]);
|
||||
assert_exit_code("workspace missing entry main", &missing_output, 1);
|
||||
assert_stderr_contains(
|
||||
"workspace missing entry main",
|
||||
&missing_output,
|
||||
"WorkspaceEntryMainMissing",
|
||||
);
|
||||
assert_stderr_contains(
|
||||
"workspace missing entry main",
|
||||
&missing_output,
|
||||
"build/run require `(fn main () -> i32 ...)`",
|
||||
);
|
||||
|
||||
let bad_signature = write_workspace(
|
||||
"workspace-bad-entry-main",
|
||||
"[workspace]\nmembers = [\"packages/app\"]\n",
|
||||
&[WorkspacePackageSpec {
|
||||
member: "packages/app",
|
||||
manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n",
|
||||
modules: &[(
|
||||
"main",
|
||||
"(module main)\n\n(fn main () -> string\n \"bad\")\n",
|
||||
)],
|
||||
}],
|
||||
);
|
||||
let bad_binary = unique_path("workspace-bad-entry-main-bin");
|
||||
let bad_output = run_glagol([
|
||||
"build".as_ref(),
|
||||
"-o".as_ref(),
|
||||
bad_binary.as_os_str(),
|
||||
bad_signature.as_os_str(),
|
||||
]);
|
||||
assert_exit_code("workspace bad entry main", &bad_output, 1);
|
||||
assert_stderr_contains(
|
||||
"workspace bad entry main",
|
||||
&bad_output,
|
||||
"WorkspaceEntryMainInvalidSignature",
|
||||
);
|
||||
assert_stderr_contains(
|
||||
"workspace bad entry main",
|
||||
&bad_output,
|
||||
"found 0 parameter(s) and return `string`",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_package_boundaries_are_diagnostics() {
|
||||
let missing = write_workspace(
|
||||
@ -714,6 +825,74 @@ fn workspace_package_boundaries_are_diagnostics() {
|
||||
&escape_output,
|
||||
"WorkspaceMemberPathEscape",
|
||||
);
|
||||
|
||||
let duplicate_member = write_workspace(
|
||||
"workspace-duplicate-member",
|
||||
"[workspace]\nmembers = [\"packages/app\", \"packages/./app\"]\n",
|
||||
&[WorkspacePackageSpec {
|
||||
member: "packages/app",
|
||||
manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n",
|
||||
modules: &[("main", "(module main)\n\n(fn main () -> i32\n 0)\n")],
|
||||
}],
|
||||
);
|
||||
let duplicate_member_output = run_glagol(["check".as_ref(), duplicate_member.as_os_str()]);
|
||||
assert_exit_code("duplicate workspace member", &duplicate_member_output, 1);
|
||||
assert_stderr_contains(
|
||||
"duplicate workspace member",
|
||||
&duplicate_member_output,
|
||||
"DuplicateWorkspaceMember",
|
||||
);
|
||||
|
||||
let missing_default = write_workspace(
|
||||
"workspace-missing-default-package",
|
||||
"[workspace]\nmembers = [\"packages/app\"]\ndefault_package = \"tool\"\n",
|
||||
&[WorkspacePackageSpec {
|
||||
member: "packages/app",
|
||||
manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n",
|
||||
modules: &[("main", "(module main)\n\n(fn main () -> i32\n 0)\n")],
|
||||
}],
|
||||
);
|
||||
let missing_default_output = run_glagol(["check".as_ref(), missing_default.as_os_str()]);
|
||||
assert_exit_code("missing default package", &missing_default_output, 1);
|
||||
assert_stderr_contains(
|
||||
"missing default package",
|
||||
&missing_default_output,
|
||||
"WorkspaceDefaultPackageMissing",
|
||||
);
|
||||
|
||||
let missing_default_entry = write_workspace(
|
||||
"workspace-missing-default-entry",
|
||||
"[workspace]\nmembers = [\"packages/app\", \"packages/tool\"]\ndefault_package = \"app\"\n",
|
||||
&[
|
||||
WorkspacePackageSpec {
|
||||
member: "packages/app",
|
||||
manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\nentry = \"app\"\n",
|
||||
modules: &[("main", "(module main)\n\n(fn main () -> i32\n 0)\n")],
|
||||
},
|
||||
WorkspacePackageSpec {
|
||||
member: "packages/tool",
|
||||
manifest: "[package]\nname = \"tool\"\nversion = \"0.1.0\"\n",
|
||||
modules: &[("main", "(module main)\n\n(fn main () -> i32\n 0)\n")],
|
||||
},
|
||||
],
|
||||
);
|
||||
let missing_default_entry_binary = unique_path("workspace-missing-default-entry-bin");
|
||||
let missing_default_entry_output = run_glagol([
|
||||
"build".as_ref(),
|
||||
"-o".as_ref(),
|
||||
missing_default_entry_binary.as_os_str(),
|
||||
missing_default_entry.as_os_str(),
|
||||
]);
|
||||
assert_exit_code(
|
||||
"missing default package entry",
|
||||
&missing_default_entry_output,
|
||||
1,
|
||||
);
|
||||
assert_stderr_contains(
|
||||
"missing default package entry",
|
||||
&missing_default_entry_output,
|
||||
"WorkspaceDefaultPackageEntryMissing",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1100,6 +1279,98 @@ fn enum_import_visibility_and_duplicate_cases_are_diagnostics() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_aliases_are_local_across_project_visibility() {
|
||||
let erased_signature = write_project(
|
||||
"alias-erased-signature",
|
||||
&[(
|
||||
"types",
|
||||
"(module types (export make_count))\n\n(type Count i32)\n\n(fn make_count ((value Count)) -> Count\n value)\n",
|
||||
)],
|
||||
"(module main)\n\n(import types (make_count))\n\n(fn main () -> i32\n (make_count 42))\n",
|
||||
);
|
||||
let erased_output = run_glagol(["check".as_ref(), erased_signature.as_os_str()]);
|
||||
assert_success_stdout("alias erased project signature", erased_output, "");
|
||||
|
||||
let export_alias = write_project(
|
||||
"alias-export",
|
||||
&[(
|
||||
"types",
|
||||
"(module types (export Count))\n\n(type Count i32)\n\n(fn make_count ((value Count)) -> Count\n value)\n",
|
||||
)],
|
||||
"(module main)\n",
|
||||
);
|
||||
let export_output = run_glagol(["check".as_ref(), export_alias.as_os_str()]);
|
||||
assert_exit_code("alias export", &export_output, 1);
|
||||
assert_stderr_contains("alias export", &export_output, "Visibility");
|
||||
assert_stderr_contains(
|
||||
"alias export message",
|
||||
&export_output,
|
||||
"type alias `Count` is module-local and cannot be exported",
|
||||
);
|
||||
|
||||
let import_alias = write_project(
|
||||
"alias-import",
|
||||
&[("types", "(module types)\n\n(type Count i32)\n")],
|
||||
"(module main)\n\n(import types (Count))\n",
|
||||
);
|
||||
let import_output = run_glagol(["check".as_ref(), import_alias.as_os_str()]);
|
||||
assert_exit_code("alias import", &import_output, 1);
|
||||
assert_stderr_contains("alias import", &import_output, "Visibility");
|
||||
assert_stderr_contains(
|
||||
"alias import message",
|
||||
&import_output,
|
||||
"type alias `Count` is module-local and cannot be imported",
|
||||
);
|
||||
|
||||
let duplicate_local = write_project(
|
||||
"alias-import-duplicate",
|
||||
&[(
|
||||
"types",
|
||||
"(module types (export value))\n\n(fn value () -> i32\n 1)\n",
|
||||
)],
|
||||
"(module main)\n\n(import types (Count))\n\n(type Count i32)\n",
|
||||
);
|
||||
let duplicate_output = run_glagol(["check".as_ref(), duplicate_local.as_os_str()]);
|
||||
assert_exit_code("alias import duplicate", &duplicate_output, 1);
|
||||
assert_stderr_contains("alias import duplicate", &duplicate_output, "DuplicateName");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_rejects_exported_generic_alias_and_function_before_visibility_leakage() {
|
||||
let generic_function = write_project(
|
||||
"generic-function-export",
|
||||
&[(
|
||||
"ids",
|
||||
"(module ids (export id))\n\n(fn id (type_params T) ((value T)) -> T\n value)\n",
|
||||
)],
|
||||
"(module main)\n",
|
||||
);
|
||||
let function_output = run_glagol(["check".as_ref(), generic_function.as_os_str()]);
|
||||
assert_exit_code("generic function export", &function_output, 1);
|
||||
assert_stderr_contains(
|
||||
"generic function export",
|
||||
&function_output,
|
||||
"UnsupportedGenericFunction",
|
||||
);
|
||||
|
||||
let generic_alias = write_project(
|
||||
"generic-alias-export",
|
||||
&[(
|
||||
"types",
|
||||
"(module types (export VecOf))\n\n(type VecOf (type_params T) (vec T))\n",
|
||||
)],
|
||||
"(module main)\n",
|
||||
);
|
||||
let alias_output = run_glagol(["check".as_ref(), generic_alias.as_os_str()]);
|
||||
assert_exit_code("generic alias export", &alias_output, 1);
|
||||
assert_stderr_contains(
|
||||
"generic alias export",
|
||||
&alias_output,
|
||||
"UnsupportedGenericTypeAlias",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_diagnostic_families_have_json_golden_coverage() {
|
||||
let duplicate = write_project(
|
||||
@ -1244,7 +1515,16 @@ fn project_build_requires_entry_main_contract() {
|
||||
missing_main.as_os_str(),
|
||||
]);
|
||||
assert_exit_code("missing entry main", &missing_output, 1);
|
||||
assert_stderr_contains("missing entry main", &missing_output, "MissingImport");
|
||||
assert_stderr_contains(
|
||||
"missing entry main",
|
||||
&missing_output,
|
||||
"ProjectEntryMainMissing",
|
||||
);
|
||||
assert_stderr_contains(
|
||||
"missing entry main",
|
||||
&missing_output,
|
||||
"build/run require `(fn main () -> i32 ...)`",
|
||||
);
|
||||
|
||||
let bad_signature = write_project(
|
||||
"bad-main-signature",
|
||||
@ -1259,7 +1539,16 @@ fn project_build_requires_entry_main_contract() {
|
||||
bad_signature.as_os_str(),
|
||||
]);
|
||||
assert_exit_code("bad entry main signature", &bad_output, 1);
|
||||
assert_stderr_contains("bad entry main signature", &bad_output, "MissingImport");
|
||||
assert_stderr_contains(
|
||||
"bad entry main signature",
|
||||
&bad_output,
|
||||
"ProjectEntryMainInvalidSignature",
|
||||
);
|
||||
assert_stderr_contains(
|
||||
"bad entry main signature",
|
||||
&bad_output,
|
||||
"found 1 parameter(s) and return `i32`",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
239
compiler/tests/reserved_generic_collection_beta15.rs
Normal file
239
compiler/tests/reserved_generic_collection_beta15.rs
Normal file
@ -0,0 +1,239 @@
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
fs,
|
||||
path::PathBuf,
|
||||
process::{Command, Output},
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
static NEXT_FIXTURE_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
const CASES: &[ReservedCase] = &[
|
||||
ReservedCase {
|
||||
name: "generic-function",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn id (type_params T) ((value T)) -> T
|
||||
value)
|
||||
|
||||
(fn main () -> i32
|
||||
0)
|
||||
"#,
|
||||
code: "UnsupportedGenericFunction",
|
||||
message: "generic function declarations are reserved but not supported in the current beta",
|
||||
},
|
||||
ReservedCase {
|
||||
name: "generic-type-alias",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(type VecOf (type_params T) (vec T))
|
||||
|
||||
(fn main () -> i32
|
||||
0)
|
||||
"#,
|
||||
code: "UnsupportedGenericTypeAlias",
|
||||
message: "parameterized type aliases are reserved but not supported in the current beta",
|
||||
},
|
||||
ReservedCase {
|
||||
name: "generic-type-parameter",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(let xs (vec T) (std.vec.i32.empty))
|
||||
0)
|
||||
"#,
|
||||
code: "UnsupportedGenericTypeParameter",
|
||||
message: "generic type parameter `T` is reserved but not supported in the current beta",
|
||||
},
|
||||
ReservedCase {
|
||||
name: "generic-vector-spelling",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> (vec)
|
||||
(std.vec.i32.empty))
|
||||
"#,
|
||||
code: "UnsupportedGenericTypeParameter",
|
||||
message: "generic vector syntax is reserved but not supported in the current beta",
|
||||
},
|
||||
ReservedCase {
|
||||
name: "map-type",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> (map string i32)
|
||||
0)
|
||||
"#,
|
||||
code: "UnsupportedMapType",
|
||||
message: "`map` types are reserved but not supported in the current beta",
|
||||
},
|
||||
ReservedCase {
|
||||
name: "set-type",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> (set string)
|
||||
0)
|
||||
"#,
|
||||
code: "UnsupportedSetType",
|
||||
message: "`set` types are reserved but not supported in the current beta",
|
||||
},
|
||||
ReservedCase {
|
||||
name: "std-vec-empty",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(std.vec.empty i32)
|
||||
0)
|
||||
"#,
|
||||
code: "UnsupportedGenericStandardLibraryCall",
|
||||
message:
|
||||
"generic standard-library call `std.vec.empty` is reserved but not supported in the current beta",
|
||||
},
|
||||
ReservedCase {
|
||||
name: "std-result-map",
|
||||
source: r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(std.result.map (ok string i32 "a") mapper)
|
||||
0)
|
||||
"#,
|
||||
code: "UnsupportedGenericStandardLibraryCall",
|
||||
message:
|
||||
"generic standard-library call `std.result.map` is reserved but not supported in the current beta",
|
||||
},
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn check_fmt_and_project_paths_reject_reserved_generic_collection_surface() {
|
||||
for case in CASES {
|
||||
let fixture = write_fixture(case);
|
||||
|
||||
let check = run_glagol([OsStr::new("check"), fixture.as_os_str()]);
|
||||
assert_rejection(&format!("{} check", case.name), &check, case);
|
||||
|
||||
let fmt = run_glagol([
|
||||
OsStr::new("fmt"),
|
||||
OsStr::new("--check"),
|
||||
fixture.as_os_str(),
|
||||
]);
|
||||
assert_rejection(&format!("{} fmt --check", case.name), &fmt, case);
|
||||
|
||||
let project = write_project(case);
|
||||
let project_check = run_glagol([OsStr::new("check"), project.as_os_str()]);
|
||||
assert_rejection(
|
||||
&format!("{} project check", case.name),
|
||||
&project_check,
|
||||
case,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
struct ReservedCase {
|
||||
name: &'static str,
|
||||
source: &'static str,
|
||||
code: &'static str,
|
||||
message: &'static str,
|
||||
}
|
||||
|
||||
fn assert_rejection(context: &str, output: &Output, case: &ReservedCase) {
|
||||
assert_eq!(
|
||||
output.status.code(),
|
||||
Some(1),
|
||||
"{} exit code mismatch\nstdout:\n{}\nstderr:\n{}",
|
||||
context,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
assert!(
|
||||
output.stdout.is_empty(),
|
||||
"{} wrote stdout:\n{}",
|
||||
context,
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
);
|
||||
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(
|
||||
stderr.contains(&format!("error[{}]", case.code)),
|
||||
"{} human diagnostic did not contain code `{}`:\n{}",
|
||||
context,
|
||||
case.code,
|
||||
stderr
|
||||
);
|
||||
assert!(
|
||||
stderr.contains(&format!(" (code {})", case.code)),
|
||||
"{} machine diagnostic did not contain code `{}`:\n{}",
|
||||
context,
|
||||
case.code,
|
||||
stderr
|
||||
);
|
||||
assert!(
|
||||
stderr.contains(case.message),
|
||||
"{} stderr did not contain message `{}`:\n{}",
|
||||
context,
|
||||
case.message,
|
||||
stderr
|
||||
);
|
||||
assert!(
|
||||
!stderr.contains("beta.9"),
|
||||
"{} stderr still used beta.9 wording:\n{}",
|
||||
context,
|
||||
stderr
|
||||
);
|
||||
}
|
||||
|
||||
fn write_fixture(case: &ReservedCase) -> PathBuf {
|
||||
let id = NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed);
|
||||
let path = unique_base_path(&format!("file-{}-{}", id, case.name)).with_extension("slo");
|
||||
fs::write(&path, case.source)
|
||||
.unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err));
|
||||
path
|
||||
}
|
||||
|
||||
fn write_project(case: &ReservedCase) -> PathBuf {
|
||||
let id = NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed);
|
||||
let root = unique_base_path(&format!("project-{}-{}", id, case.name));
|
||||
let src = root.join("src");
|
||||
fs::create_dir_all(&src).unwrap_or_else(|err| panic!("create `{}`: {}", src.display(), err));
|
||||
fs::write(
|
||||
root.join("slovo.toml"),
|
||||
format!(
|
||||
"[project]\nname = \"reserved-beta15-{}\"\nsource_root = \"src\"\nentry = \"main\"\n",
|
||||
case.name
|
||||
),
|
||||
)
|
||||
.unwrap_or_else(|err| panic!("write project manifest for `{}`: {}", case.name, err));
|
||||
fs::write(src.join("main.slo"), case.source)
|
||||
.unwrap_or_else(|err| panic!("write project source for `{}`: {}", case.name, err));
|
||||
root
|
||||
}
|
||||
|
||||
fn unique_base_path(name: &str) -> PathBuf {
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_nanos())
|
||||
.unwrap_or(0);
|
||||
std::env::temp_dir().join(format!(
|
||||
"glagol-reserved-beta15-{}-{}-{}",
|
||||
std::process::id(),
|
||||
nanos,
|
||||
name
|
||||
))
|
||||
}
|
||||
|
||||
fn run_glagol<I, S>(args: I) -> Output
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<OsStr>,
|
||||
{
|
||||
Command::new(env!("CARGO_BIN_EXE_glagol"))
|
||||
.args(args)
|
||||
.output()
|
||||
.expect("run glagol")
|
||||
}
|
||||
@ -6,6 +6,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Output, Stdio},
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
static NEXT_FIXTURE_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
@ -255,6 +256,61 @@ fn exp10_diagnostics_cover_promoted_and_deferred_boundaries() {
|
||||
(fn main () -> i32
|
||||
(std.fs.write_text_result "path" 1)
|
||||
0)
|
||||
"#,
|
||||
"TypeMismatch",
|
||||
),
|
||||
(
|
||||
"fs-exists-type",
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(std.fs.exists 1)
|
||||
0)
|
||||
"#,
|
||||
"TypeMismatch",
|
||||
),
|
||||
(
|
||||
"fs-remove-file-type",
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(std.fs.remove_file_result 1)
|
||||
0)
|
||||
"#,
|
||||
"TypeMismatch",
|
||||
),
|
||||
(
|
||||
"fs-create-dir-type",
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(std.fs.create_dir_result 1)
|
||||
0)
|
||||
"#,
|
||||
"TypeMismatch",
|
||||
),
|
||||
(
|
||||
"fs-open-handle-type",
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(std.fs.open_text_read_result 1)
|
||||
0)
|
||||
"#,
|
||||
"TypeMismatch",
|
||||
),
|
||||
(
|
||||
"fs-close-handle-type",
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(std.fs.close_result "handle")
|
||||
0)
|
||||
"#,
|
||||
"TypeMismatch",
|
||||
),
|
||||
@ -298,7 +354,7 @@ fn exp10_diagnostics_cover_promoted_and_deferred_boundaries() {
|
||||
(std.result.map (ok string i32 "a"))
|
||||
0)
|
||||
"#,
|
||||
"UnsupportedStandardLibraryCall",
|
||||
"UnsupportedGenericStandardLibraryCall",
|
||||
),
|
||||
(
|
||||
"promoted-name-shadow",
|
||||
@ -357,6 +413,348 @@ fn exp10_diagnostics_cover_promoted_and_deferred_boundaries() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn beta2_file_resource_handles_execute_in_test_runner() {
|
||||
let root = temp_root("resource-test-runner");
|
||||
fs::create_dir_all(&root).unwrap_or_else(|err| panic!("create `{}`: {}", root.display(), err));
|
||||
let existing = root.join("resource.txt");
|
||||
let missing = root.join("missing-resource.txt");
|
||||
fs::write(&existing, "resource text")
|
||||
.unwrap_or_else(|err| panic!("write `{}`: {}", existing.display(), err));
|
||||
|
||||
let source = format!(
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(fn read_len_and_close ((path string)) -> i32
|
||||
(match (std.fs.open_text_read_result path)
|
||||
((ok handle)
|
||||
(let text string (unwrap_ok (std.fs.read_open_text_result handle)))
|
||||
(let close_status i32 (unwrap_ok (std.fs.close_result handle)))
|
||||
(+ (std.string.len text) close_status))
|
||||
((err code)
|
||||
code)))
|
||||
|
||||
(fn read_after_close_err ((path string)) -> bool
|
||||
(match (std.fs.open_text_read_result path)
|
||||
((ok handle)
|
||||
(std.fs.close_result handle)
|
||||
(= (unwrap_err (std.fs.read_open_text_result handle)) 1))
|
||||
((err code)
|
||||
false)))
|
||||
|
||||
(test "resource handle open read close"
|
||||
(= (read_len_and_close "{}") 13))
|
||||
|
||||
(test "resource handle missing open is err one"
|
||||
(= (unwrap_err (std.fs.open_text_read_result "{}")) 1))
|
||||
|
||||
(test "resource handle closed read is err one"
|
||||
(read_after_close_err "{}"))
|
||||
|
||||
(test "resource handle invalid close is err one"
|
||||
(= (unwrap_err (std.fs.close_result -1)) 1))
|
||||
"#,
|
||||
slovo_path(&existing),
|
||||
slovo_path(&missing),
|
||||
slovo_path(&existing)
|
||||
);
|
||||
let fixture = write_fixture("resource-test-runner", &source);
|
||||
let run = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
|
||||
|
||||
assert_success_stdout(
|
||||
run,
|
||||
concat!(
|
||||
"test \"resource handle open read close\" ... ok\n",
|
||||
"test \"resource handle missing open is err one\" ... ok\n",
|
||||
"test \"resource handle closed read is err one\" ... ok\n",
|
||||
"test \"resource handle invalid close is err one\" ... ok\n",
|
||||
"4 test(s) passed\n",
|
||||
),
|
||||
"beta.2 resource handle test-runner output",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn beta2_file_resource_handles_emit_expected_private_runtime_shape() {
|
||||
let source = r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(match (std.fs.open_text_read_result "missing.txt")
|
||||
((ok handle)
|
||||
(std.string.len (unwrap_ok (std.fs.read_open_text_result handle))))
|
||||
((err code)
|
||||
(unwrap_err (std.fs.close_result -1)))))
|
||||
"#;
|
||||
let fixture = write_fixture("resource-lowering", source);
|
||||
let compile = run_glagol([fixture.as_os_str()]);
|
||||
assert_success("compile beta.2 resource handle lowering", &compile);
|
||||
let stdout = String::from_utf8_lossy(&compile.stdout);
|
||||
|
||||
assert!(
|
||||
stdout.contains("__glagol_fs_open_text_read_result")
|
||||
&& stdout.contains("__glagol_fs_read_open_text_result")
|
||||
&& stdout.contains("__glagol_fs_close_result")
|
||||
&& stdout.contains("declare i64 @__glagol_fs_open_text_read_result(ptr)")
|
||||
&& stdout.contains("declare ptr @__glagol_fs_read_open_text_result(i32)")
|
||||
&& stdout.contains("declare i32 @__glagol_fs_close_result(i32)")
|
||||
&& !stdout.contains("@std.fs.open_text_read_result")
|
||||
&& !stdout.contains("@std.fs.read_open_text_result")
|
||||
&& !stdout.contains("@std.fs.close_result"),
|
||||
"LLVM output did not contain expected beta.2 resource runtime shape\nstdout:\n{}",
|
||||
stdout
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn beta2_file_status_and_mutation_execute_in_test_runner() {
|
||||
let root = temp_root("resource-status-test-runner");
|
||||
fs::create_dir_all(&root).unwrap_or_else(|err| panic!("create `{}`: {}", root.display(), err));
|
||||
let existing = root.join("existing.txt");
|
||||
let removable = root.join("removable.txt");
|
||||
let missing = root.join("missing.txt");
|
||||
let created_dir = root.join("created-dir");
|
||||
fs::write(&existing, "existing")
|
||||
.unwrap_or_else(|err| panic!("write `{}`: {}", existing.display(), err));
|
||||
fs::write(&removable, "remove me")
|
||||
.unwrap_or_else(|err| panic!("write `{}`: {}", removable.display(), err));
|
||||
|
||||
let source = format!(
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(fn existing_file_status_ok ((path string)) -> bool
|
||||
(if (std.fs.exists path)
|
||||
(if (std.fs.is_file path)
|
||||
(if (std.fs.is_dir path)
|
||||
false
|
||||
true)
|
||||
false)
|
||||
false))
|
||||
|
||||
(test "fs existing file status"
|
||||
(existing_file_status_ok "{}"))
|
||||
|
||||
(test "fs missing status false"
|
||||
(if (std.fs.exists "{}")
|
||||
false
|
||||
true))
|
||||
|
||||
(test "fs create dir result ok"
|
||||
(= (unwrap_ok (std.fs.create_dir_result "{}")) 0))
|
||||
|
||||
(test "fs created path is dir"
|
||||
(std.fs.is_dir "{}"))
|
||||
|
||||
(test "fs create existing dir err one"
|
||||
(= (unwrap_err (std.fs.create_dir_result "{}")) 1))
|
||||
|
||||
(test "fs remove file result ok"
|
||||
(= (unwrap_ok (std.fs.remove_file_result "{}")) 0))
|
||||
|
||||
(test "fs removed file no longer exists"
|
||||
(if (std.fs.exists "{}")
|
||||
false
|
||||
true))
|
||||
|
||||
(test "fs remove missing file err one"
|
||||
(= (unwrap_err (std.fs.remove_file_result "{}")) 1))
|
||||
"#,
|
||||
slovo_path(&existing),
|
||||
slovo_path(&missing),
|
||||
slovo_path(&created_dir),
|
||||
slovo_path(&created_dir),
|
||||
slovo_path(&created_dir),
|
||||
slovo_path(&removable),
|
||||
slovo_path(&removable),
|
||||
slovo_path(&missing)
|
||||
);
|
||||
let fixture = write_fixture("resource-status-test-runner", &source);
|
||||
let run = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
|
||||
|
||||
assert_success_stdout(
|
||||
run,
|
||||
concat!(
|
||||
"test \"fs existing file status\" ... ok\n",
|
||||
"test \"fs missing status false\" ... ok\n",
|
||||
"test \"fs create dir result ok\" ... ok\n",
|
||||
"test \"fs created path is dir\" ... ok\n",
|
||||
"test \"fs create existing dir err one\" ... ok\n",
|
||||
"test \"fs remove file result ok\" ... ok\n",
|
||||
"test \"fs removed file no longer exists\" ... ok\n",
|
||||
"test \"fs remove missing file err one\" ... ok\n",
|
||||
"8 test(s) passed\n",
|
||||
),
|
||||
"beta.2 fs status/mutation test-runner output",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn beta2_file_status_and_mutation_emit_expected_private_runtime_shape() {
|
||||
let source = r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(if (std.fs.exists "existing.txt")
|
||||
(if (std.fs.is_file "existing.txt")
|
||||
(if (std.fs.is_dir "existing.txt")
|
||||
1
|
||||
(unwrap_ok (std.fs.remove_file_result "existing.txt")))
|
||||
2)
|
||||
(unwrap_err (std.fs.create_dir_result "created-dir"))))
|
||||
"#;
|
||||
let fixture = write_fixture("resource-status-lowering", source);
|
||||
let compile = run_glagol([fixture.as_os_str()]);
|
||||
assert_success("compile beta.2 fs status/mutation lowering", &compile);
|
||||
let stdout = String::from_utf8_lossy(&compile.stdout);
|
||||
|
||||
assert!(
|
||||
stdout.contains("call i1 @__glagol_fs_exists")
|
||||
&& stdout.contains("call i1 @__glagol_fs_is_file")
|
||||
&& stdout.contains("call i1 @__glagol_fs_is_dir")
|
||||
&& stdout.contains("call i32 @__glagol_fs_remove_file_result")
|
||||
&& stdout.contains("call i32 @__glagol_fs_create_dir_result")
|
||||
&& !stdout.contains("@std.fs.exists")
|
||||
&& !stdout.contains("@std.fs.is_file")
|
||||
&& !stdout.contains("@std.fs.is_dir")
|
||||
&& !stdout.contains("@std.fs.remove_file_result")
|
||||
&& !stdout.contains("@std.fs.create_dir_result"),
|
||||
"LLVM output did not contain expected beta.2 fs status/mutation runtime shape\nstdout:\n{}",
|
||||
stdout
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hosted_runtime_executes_beta2_file_resource_handles_when_clang_is_available() {
|
||||
let Some(clang) = find_clang() else {
|
||||
eprintln!("skipping beta.2 resource runtime smoke: set GLAGOL_CLANG or install clang");
|
||||
return;
|
||||
};
|
||||
|
||||
let root = temp_root("resource-native");
|
||||
fs::create_dir_all(&root).unwrap_or_else(|err| panic!("create `{}`: {}", root.display(), err));
|
||||
let existing = root.join("native-resource.txt");
|
||||
fs::write(&existing, "native handle")
|
||||
.unwrap_or_else(|err| panic!("write `{}`: {}", existing.display(), err));
|
||||
|
||||
let source = format!(
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(match (std.fs.open_text_read_result "{}")
|
||||
((ok handle)
|
||||
(let text string (unwrap_ok (std.fs.read_open_text_result handle)))
|
||||
(let close_status i32 (unwrap_ok (std.fs.close_result handle)))
|
||||
(+ (std.string.len text) close_status))
|
||||
((err code)
|
||||
code)))
|
||||
"#,
|
||||
slovo_path(&existing)
|
||||
);
|
||||
let fixture = write_fixture("resource-native", &source);
|
||||
let compile = run_glagol([fixture.as_os_str()]);
|
||||
assert_success("compile beta.2 resource native smoke", &compile);
|
||||
|
||||
let run =
|
||||
compile_and_run_with_runtime(&clang, "beta2-resource-native", &compile.stdout, |_| {});
|
||||
assert_eq!(
|
||||
run.status.code(),
|
||||
Some(13),
|
||||
"beta.2 resource native smoke exit code drifted\nstdout:\n{}\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&run.stdout),
|
||||
String::from_utf8_lossy(&run.stderr)
|
||||
);
|
||||
assert!(
|
||||
run.stdout.is_empty(),
|
||||
"beta.2 resource native smoke wrote stdout:\n{}",
|
||||
String::from_utf8_lossy(&run.stdout)
|
||||
);
|
||||
assert!(
|
||||
run.stderr.is_empty(),
|
||||
"beta.2 resource native smoke wrote stderr:\n{}",
|
||||
String::from_utf8_lossy(&run.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hosted_runtime_executes_beta2_file_status_and_mutation_when_clang_is_available() {
|
||||
let Some(clang) = find_clang() else {
|
||||
eprintln!(
|
||||
"skipping beta.2 fs status/mutation runtime smoke: set GLAGOL_CLANG or install clang"
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
let root = temp_root("resource-status-native");
|
||||
fs::create_dir_all(&root).unwrap_or_else(|err| panic!("create `{}`: {}", root.display(), err));
|
||||
let existing = root.join("native-existing.txt");
|
||||
let removable = root.join("native-removable.txt");
|
||||
let created_dir = root.join("native-created-dir");
|
||||
fs::write(&existing, "native existing")
|
||||
.unwrap_or_else(|err| panic!("write `{}`: {}", existing.display(), err));
|
||||
fs::write(&removable, "native removable")
|
||||
.unwrap_or_else(|err| panic!("write `{}`: {}", removable.display(), err));
|
||||
|
||||
let source = format!(
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(if (std.fs.exists "{}")
|
||||
(if (std.fs.is_file "{}")
|
||||
(if (std.fs.is_dir "{}")
|
||||
1
|
||||
(if (= (unwrap_ok (std.fs.create_dir_result "{}")) 0)
|
||||
(if (std.fs.is_dir "{}")
|
||||
(if (= (unwrap_ok (std.fs.remove_file_result "{}")) 0)
|
||||
(if (std.fs.exists "{}")
|
||||
2
|
||||
0)
|
||||
3)
|
||||
4)
|
||||
5))
|
||||
6)
|
||||
7))
|
||||
"#,
|
||||
slovo_path(&existing),
|
||||
slovo_path(&existing),
|
||||
slovo_path(&existing),
|
||||
slovo_path(&created_dir),
|
||||
slovo_path(&created_dir),
|
||||
slovo_path(&removable),
|
||||
slovo_path(&removable)
|
||||
);
|
||||
let fixture = write_fixture("resource-status-native", &source);
|
||||
let compile = run_glagol([fixture.as_os_str()]);
|
||||
assert_success("compile beta.2 fs status/mutation native smoke", &compile);
|
||||
|
||||
let run = compile_and_run_with_runtime(
|
||||
&clang,
|
||||
"beta2-resource-status-native",
|
||||
&compile.stdout,
|
||||
|_| {},
|
||||
);
|
||||
assert_eq!(
|
||||
run.status.code(),
|
||||
Some(0),
|
||||
"beta.2 fs status/mutation native smoke exit code drifted\nstdout:\n{}\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&run.stdout),
|
||||
String::from_utf8_lossy(&run.stderr)
|
||||
);
|
||||
assert!(
|
||||
run.stdout.is_empty(),
|
||||
"beta.2 fs status/mutation native smoke wrote stdout:\n{}",
|
||||
String::from_utf8_lossy(&run.stdout)
|
||||
);
|
||||
assert!(
|
||||
run.stderr.is_empty(),
|
||||
"beta.2 fs status/mutation native smoke wrote stderr:\n{}",
|
||||
String::from_utf8_lossy(&run.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hosted_runtime_executes_exp10_results_when_clang_is_available() {
|
||||
let Some(clang) = find_clang() else {
|
||||
@ -554,11 +952,16 @@ fn write_fixture(name: &str, source: &str) -> PathBuf {
|
||||
}
|
||||
|
||||
fn temp_root(name: &str) -> PathBuf {
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("system clock before Unix epoch")
|
||||
.as_nanos();
|
||||
env::temp_dir().join(format!(
|
||||
"glagol-exp10-host-result-{}-{}-{}",
|
||||
"glagol-exp10-host-result-{}-{}-{}-{}",
|
||||
name,
|
||||
std::process::id(),
|
||||
NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed)
|
||||
NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed),
|
||||
nanos
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@ -94,7 +94,7 @@ fn result_helpers_alpha_rejects_deferred_and_misused_std_names() {
|
||||
(std.result.map (ok i32 i32 1))
|
||||
0)
|
||||
"#,
|
||||
"UnsupportedStandardLibraryCall",
|
||||
"UnsupportedGenericStandardLibraryCall",
|
||||
),
|
||||
(
|
||||
"unwrap-or-deferred",
|
||||
|
||||
267
compiler/tests/run_manifest_beta22.rs
Normal file
267
compiler/tests/run_manifest_beta22.rs
Normal file
@ -0,0 +1,267 @@
|
||||
use std::{
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Output},
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
|
||||
static NEXT_FIXTURE_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[test]
|
||||
fn run_manifest_records_success_report_stdout_and_program_args() {
|
||||
let Some(clang) = find_clang() else {
|
||||
eprintln!("skipping run manifest success report: set GLAGOL_CLANG or install clang");
|
||||
return;
|
||||
};
|
||||
|
||||
let source = write_fixture(
|
||||
"success",
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(print_string "beta22-out")
|
||||
0)
|
||||
"#,
|
||||
"slo",
|
||||
);
|
||||
let manifest_path = temp_path("success-manifest", "manifest.slo");
|
||||
|
||||
let mut command = Command::new(compiler_path());
|
||||
command
|
||||
.arg("run")
|
||||
.arg(&source)
|
||||
.arg("--manifest")
|
||||
.arg(&manifest_path)
|
||||
.arg("--")
|
||||
.arg("alpha")
|
||||
.arg("two words")
|
||||
.arg("--literal")
|
||||
.env("GLAGOL_CLANG", &clang);
|
||||
configure_clang_runtime_env(&mut command, &clang);
|
||||
|
||||
let output = command.output().expect("run glagol success manifest");
|
||||
assert_success_stdout("run manifest success", &output, "beta22-out\n");
|
||||
|
||||
let manifest = read_manifest(&manifest_path);
|
||||
assert!(
|
||||
manifest.contains(" (mode run)\n")
|
||||
&& manifest.contains(" (success true)\n")
|
||||
&& manifest.contains(" (run-report\n")
|
||||
&& manifest.contains(" (exit-status 0)\n")
|
||||
&& manifest.contains(" (stdout \"beta22-out\\n\")\n")
|
||||
&& manifest.contains(" (stderr \"\")\n")
|
||||
&& manifest.contains(" (arg \"alpha\")\n")
|
||||
&& manifest.contains(" (arg \"two words\")\n")
|
||||
&& manifest.contains(" (arg \"--literal\")\n"),
|
||||
"successful run manifest mismatch:\n{}",
|
||||
manifest
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_manifest_records_nonzero_report_and_preserves_program_stderr() {
|
||||
let Some(clang) = find_clang() else {
|
||||
eprintln!("skipping run manifest nonzero report: set GLAGOL_CLANG or install clang");
|
||||
return;
|
||||
};
|
||||
|
||||
let source = write_fixture(
|
||||
"nonzero",
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(import_c beta22_stderr () -> i32)
|
||||
|
||||
(fn emit_stderr () -> i32
|
||||
(unsafe
|
||||
(beta22_stderr)))
|
||||
|
||||
(fn main () -> i32
|
||||
(emit_stderr)
|
||||
7)
|
||||
"#,
|
||||
"slo",
|
||||
);
|
||||
let c_source = write_fixture(
|
||||
"nonzero-stderr",
|
||||
r#"
|
||||
#include <stdio.h>
|
||||
|
||||
int beta22_stderr(void) {
|
||||
fputs("beta22-err\n", stderr);
|
||||
return 0;
|
||||
}
|
||||
"#,
|
||||
"c",
|
||||
);
|
||||
let manifest_path = temp_path("nonzero-manifest", "manifest.slo");
|
||||
|
||||
let mut command = Command::new(compiler_path());
|
||||
command
|
||||
.arg("run")
|
||||
.arg(&source)
|
||||
.arg("--link-c")
|
||||
.arg(&c_source)
|
||||
.arg("--manifest")
|
||||
.arg(&manifest_path)
|
||||
.env("GLAGOL_CLANG", &clang);
|
||||
configure_clang_runtime_env(&mut command, &clang);
|
||||
|
||||
let output = command.output().expect("run glagol nonzero manifest");
|
||||
assert_exit_code("run manifest nonzero", &output, 7);
|
||||
assert_eq!(output.stdout, b"", "nonzero run stdout drifted");
|
||||
assert_eq!(output.stderr, b"beta22-err\n", "nonzero run stderr drifted");
|
||||
|
||||
let manifest = read_manifest(&manifest_path);
|
||||
assert!(
|
||||
manifest.contains(" (mode run)\n")
|
||||
&& manifest.contains(" (success false)\n")
|
||||
&& manifest.contains(" (run-report\n")
|
||||
&& manifest.contains(" (exit-status 7)\n")
|
||||
&& manifest.contains(" (stdout \"\")\n")
|
||||
&& manifest.contains(" (stderr \"beta22-err\\n\")\n")
|
||||
&& manifest.contains(" (args)\n"),
|
||||
"nonzero run manifest mismatch:\n{}",
|
||||
manifest
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_manifest_source_failure_does_not_record_fake_run_report() {
|
||||
let source = write_fixture(
|
||||
"source-failure",
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
true)
|
||||
"#,
|
||||
"slo",
|
||||
);
|
||||
let manifest_path = temp_path("source-failure-manifest", "manifest.slo");
|
||||
|
||||
let output = run_glagol([
|
||||
"run".as_ref(),
|
||||
source.as_os_str(),
|
||||
"--manifest".as_ref(),
|
||||
manifest_path.as_os_str(),
|
||||
]);
|
||||
assert_exit_code("run manifest source failure", &output, 1);
|
||||
|
||||
let manifest = read_manifest(&manifest_path);
|
||||
assert!(
|
||||
manifest.contains(" (mode run)\n")
|
||||
&& manifest.contains(" (success false)\n")
|
||||
&& manifest.contains(" (kind diagnostics)\n")
|
||||
&& manifest.contains("TypeMismatch")
|
||||
&& !manifest.contains(" (run-report\n"),
|
||||
"source failure manifest included fake run report:\n{}",
|
||||
manifest
|
||||
);
|
||||
}
|
||||
|
||||
fn run_glagol<const N: usize>(args: [&std::ffi::OsStr; N]) -> Output {
|
||||
Command::new(compiler_path())
|
||||
.args(args)
|
||||
.output()
|
||||
.expect("run glagol")
|
||||
}
|
||||
|
||||
fn compiler_path() -> &'static str {
|
||||
env!("CARGO_BIN_EXE_glagol")
|
||||
}
|
||||
|
||||
fn write_fixture(name: &str, source: &str, extension: &str) -> PathBuf {
|
||||
let path = temp_path(name, extension);
|
||||
fs::write(&path, source).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err));
|
||||
path
|
||||
}
|
||||
|
||||
fn temp_path(name: &str, extension: &str) -> PathBuf {
|
||||
let id = NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed);
|
||||
let mut path = env::temp_dir();
|
||||
path.push(format!(
|
||||
"glagol-run-manifest-beta22-{}-{}-{}.{}",
|
||||
std::process::id(),
|
||||
id,
|
||||
name,
|
||||
extension
|
||||
));
|
||||
path
|
||||
}
|
||||
|
||||
fn read_manifest(path: &Path) -> String {
|
||||
fs::read_to_string(path).unwrap_or_else(|err| panic!("read `{}`: {}", path.display(), err))
|
||||
}
|
||||
|
||||
fn assert_success_stdout(context: &str, output: &Output, expected: &str) {
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{} failed\nstdout:\n{}\nstderr:\n{}",
|
||||
context,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
expected,
|
||||
"{} stdout mismatch",
|
||||
context
|
||||
);
|
||||
assert!(
|
||||
output.stderr.is_empty(),
|
||||
"{} wrote stderr:\n{}",
|
||||
context,
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_exit_code(context: &str, output: &Output, expected: i32) {
|
||||
assert_eq!(
|
||||
output.status.code(),
|
||||
Some(expected),
|
||||
"{} exit code mismatch\nstdout:\n{}\nstderr:\n{}",
|
||||
context,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
fn find_clang() -> Option<PathBuf> {
|
||||
if let Some(path) = env::var_os("GLAGOL_CLANG").filter(|value| !value.is_empty()) {
|
||||
let path = PathBuf::from(path);
|
||||
if path.is_file() {
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
|
||||
let hermetic_clang = PathBuf::from("/tmp/glagol-clang-root/usr/bin/clang");
|
||||
if hermetic_clang.is_file() {
|
||||
return Some(hermetic_clang);
|
||||
}
|
||||
|
||||
find_on_path("clang")
|
||||
}
|
||||
|
||||
fn find_on_path(name: &str) -> Option<PathBuf> {
|
||||
let path = env::var_os("PATH")?;
|
||||
env::split_paths(&path)
|
||||
.map(|dir| dir.join(name))
|
||||
.find(|candidate| candidate.is_file())
|
||||
}
|
||||
|
||||
fn configure_clang_runtime_env(command: &mut Command, clang: &Path) {
|
||||
if !clang.starts_with("/tmp/glagol-clang-root") {
|
||||
return;
|
||||
}
|
||||
|
||||
let root = Path::new("/tmp/glagol-clang-root");
|
||||
let lib64 = root.join("usr/lib64");
|
||||
let lib = root.join("usr/lib");
|
||||
let existing = env::var_os("LD_LIBRARY_PATH").unwrap_or_default();
|
||||
let mut paths = vec![lib64, lib];
|
||||
paths.extend(env::split_paths(&existing));
|
||||
let joined = env::join_paths(paths).expect("join LD_LIBRARY_PATH");
|
||||
command.env("LD_LIBRARY_PATH", joined);
|
||||
}
|
||||
@ -7,13 +7,18 @@ use std::{
|
||||
|
||||
const EXPECTED_STD_STRING_OUTPUT: &str = concat!(
|
||||
"test \"explicit std string len concat\" ... ok\n",
|
||||
"test \"explicit std string byte_at_result wrapper\" ... ok\n",
|
||||
"test \"explicit std string slice_result wrapper\" ... ok\n",
|
||||
"test \"explicit std string boundary wrappers\" ... ok\n",
|
||||
"test \"explicit std string parse result wrappers\" ... ok\n",
|
||||
"test \"explicit std string parse option wrappers\" ... ok\n",
|
||||
"test \"explicit std string parse integer fallbacks\" ... ok\n",
|
||||
"test \"explicit std string parse float bool fallbacks\" ... ok\n",
|
||||
"test \"explicit std string parse custom fallbacks\" ... ok\n",
|
||||
"test \"explicit std string search helpers\" ... ok\n",
|
||||
"test \"explicit std string ascii trim helpers\" ... ok\n",
|
||||
"test \"explicit std string helpers all\" ... ok\n",
|
||||
"7 test(s) passed\n",
|
||||
"12 test(s) passed\n",
|
||||
);
|
||||
|
||||
const EXPECTED_STD_NUM_OUTPUT: &str = concat!(
|
||||
@ -32,6 +37,16 @@ fn explicit_std_string_import_loads_repo_root_standard_source() {
|
||||
&[
|
||||
"len",
|
||||
"concat",
|
||||
"byte_at_result",
|
||||
"slice_result",
|
||||
"starts_with",
|
||||
"ends_with",
|
||||
"contains",
|
||||
"index_of_option",
|
||||
"last_index_of_option",
|
||||
"trim_ascii_start",
|
||||
"trim_ascii_end",
|
||||
"trim_ascii",
|
||||
"parse_i32_result",
|
||||
"parse_i32_option",
|
||||
"parse_u32_result",
|
||||
|
||||
@ -14,6 +14,17 @@ const EXPECTED_TEST_OUTPUT: &str = concat!(
|
||||
"test \"explicit local fs read text facade\" ... ok\n",
|
||||
"test \"explicit local fs write text result facade\" ... ok\n",
|
||||
"test \"explicit local fs read text result facade\" ... ok\n",
|
||||
"test \"explicit local fs read text via handle facade\" ... ok\n",
|
||||
"test \"explicit local fs open read close handle facade\" ... ok\n",
|
||||
"test \"explicit local fs closed handle read err facade\" ... ok\n",
|
||||
"test \"explicit local fs invalid close err facade\" ... ok\n",
|
||||
"test \"explicit local fs exists file facade\" ... ok\n",
|
||||
"test \"explicit local fs exists missing facade\" ... ok\n",
|
||||
"test \"explicit local fs create dir result facade\" ... ok\n",
|
||||
"test \"explicit local fs is dir facade\" ... ok\n",
|
||||
"test \"explicit local fs create dir ok facade\" ... ok\n",
|
||||
"test \"explicit local fs remove file result facade\" ... ok\n",
|
||||
"test \"explicit local fs remove file ok facade\" ... ok\n",
|
||||
"test \"explicit local fs read text option missing facade\" ... ok\n",
|
||||
"test \"explicit local fs read text option present facade\" ... ok\n",
|
||||
"test \"explicit local fs read text or missing facade\" ... ok\n",
|
||||
@ -34,7 +45,7 @@ const EXPECTED_TEST_OUTPUT: &str = concat!(
|
||||
"test \"explicit local fs typed option facade\" ... ok\n",
|
||||
"test \"explicit local fs typed custom fallback facade\" ... ok\n",
|
||||
"test \"explicit local fs facade all\" ... ok\n",
|
||||
"24 test(s) passed\n",
|
||||
"35 test(s) passed\n",
|
||||
);
|
||||
|
||||
const STANDARD_FS_SOURCE_FACADE_ALPHA: &[&str] = &[
|
||||
@ -43,6 +54,18 @@ const STANDARD_FS_SOURCE_FACADE_ALPHA: &[&str] = &[
|
||||
"read_text_option",
|
||||
"write_text_status",
|
||||
"write_text_result",
|
||||
"exists",
|
||||
"is_file",
|
||||
"is_dir",
|
||||
"remove_file_result",
|
||||
"create_dir_result",
|
||||
"remove_file_ok",
|
||||
"create_dir_ok",
|
||||
"open_text_read_result",
|
||||
"read_open_text_result",
|
||||
"close_result",
|
||||
"read_text_via_handle_result",
|
||||
"close_ok",
|
||||
"read_text_or",
|
||||
"write_text_ok",
|
||||
"read_i32_result",
|
||||
@ -144,7 +167,10 @@ fn assert_local_fs_fixture_is_source_authored(project: &Path) {
|
||||
&& main.contains("\"glagol-std-layout-local-fs-alpha-u64.txt\"")
|
||||
&& main.contains("\"glagol-std-layout-local-fs-alpha-f64.txt\"")
|
||||
&& main.contains("\"glagol-std-layout-local-fs-alpha-bool.txt\"")
|
||||
&& main.contains("\"glagol-std-layout-local-fs-alpha-invalid.txt\""),
|
||||
&& main.contains("\"glagol-std-layout-local-fs-alpha-invalid.txt\"")
|
||||
&& main.contains("\"glagol-std-layout-local-fs-alpha-dir\"")
|
||||
&& main.contains("\"glagol-std-layout-local-fs-alpha-dir-helper\"")
|
||||
&& main.contains("\"glagol-std-layout-local-fs-alpha-remove.txt\""),
|
||||
"fs fixture must use deterministic relative paths and text content"
|
||||
);
|
||||
assert!(
|
||||
@ -153,7 +179,15 @@ fn assert_local_fs_fixture_is_source_authored(project: &Path) {
|
||||
&& fs_source.contains("(std.fs.read_text path)")
|
||||
&& fs_source.contains("(std.fs.read_text_result path)")
|
||||
&& fs_source.contains("(std.fs.write_text path text)")
|
||||
&& fs_source.contains("(std.fs.write_text_result path text)"),
|
||||
&& fs_source.contains("(std.fs.write_text_result path text)")
|
||||
&& fs_source.contains("(std.fs.exists path)")
|
||||
&& fs_source.contains("(std.fs.is_file path)")
|
||||
&& fs_source.contains("(std.fs.is_dir path)")
|
||||
&& fs_source.contains("(std.fs.remove_file_result path)")
|
||||
&& fs_source.contains("(std.fs.create_dir_result path)")
|
||||
&& fs_source.contains("(std.fs.open_text_read_result path)")
|
||||
&& fs_source.contains("(std.fs.read_open_text_result handle)")
|
||||
&& fs_source.contains("(std.fs.close_result handle)"),
|
||||
"fs.slo must stay a local source facade over fs calls plus local string and result bridge helpers"
|
||||
);
|
||||
assert!(
|
||||
|
||||
@ -72,6 +72,17 @@ const EXPECTED_STD_FS_OUTPUT: &str = concat!(
|
||||
"test \"explicit std fs read text facade\" ... ok\n",
|
||||
"test \"explicit std fs write text result facade\" ... ok\n",
|
||||
"test \"explicit std fs read text result facade\" ... ok\n",
|
||||
"test \"explicit std fs read text via handle facade\" ... ok\n",
|
||||
"test \"explicit std fs open read close handle facade\" ... ok\n",
|
||||
"test \"explicit std fs closed handle read err facade\" ... ok\n",
|
||||
"test \"explicit std fs invalid close err facade\" ... ok\n",
|
||||
"test \"explicit std fs exists file facade\" ... ok\n",
|
||||
"test \"explicit std fs exists missing facade\" ... ok\n",
|
||||
"test \"explicit std fs create dir result facade\" ... ok\n",
|
||||
"test \"explicit std fs is dir facade\" ... ok\n",
|
||||
"test \"explicit std fs create dir ok facade\" ... ok\n",
|
||||
"test \"explicit std fs remove file result facade\" ... ok\n",
|
||||
"test \"explicit std fs remove file ok facade\" ... ok\n",
|
||||
"test \"explicit std fs read text option missing facade\" ... ok\n",
|
||||
"test \"explicit std fs read text option present facade\" ... ok\n",
|
||||
"test \"explicit std fs read text or missing facade\" ... ok\n",
|
||||
@ -92,7 +103,7 @@ const EXPECTED_STD_FS_OUTPUT: &str = concat!(
|
||||
"test \"explicit std fs typed option facade\" ... ok\n",
|
||||
"test \"explicit std fs typed custom fallback facade\" ... ok\n",
|
||||
"test \"explicit std fs facade all\" ... ok\n",
|
||||
"24 test(s) passed\n",
|
||||
"35 test(s) passed\n",
|
||||
);
|
||||
|
||||
const EXPECTED_STD_PROCESS_OUTPUT: &str = concat!(
|
||||
@ -204,6 +215,18 @@ fn explicit_std_fs_import_loads_repo_root_standard_source() {
|
||||
"read_text_option",
|
||||
"write_text_status",
|
||||
"write_text_result",
|
||||
"exists",
|
||||
"is_file",
|
||||
"is_dir",
|
||||
"remove_file_result",
|
||||
"create_dir_result",
|
||||
"remove_file_ok",
|
||||
"create_dir_ok",
|
||||
"open_text_read_result",
|
||||
"read_open_text_result",
|
||||
"close_result",
|
||||
"read_text_via_handle_result",
|
||||
"close_ok",
|
||||
"read_text_or",
|
||||
"write_text_ok",
|
||||
"read_i32_result",
|
||||
|
||||
226
compiler/tests/standard_json.rs
Normal file
226
compiler/tests/standard_json.rs
Normal file
@ -0,0 +1,226 @@
|
||||
use std::{
|
||||
env,
|
||||
ffi::OsStr,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Output},
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
|
||||
static NEXT_FIXTURE_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[test]
|
||||
fn standard_json_lowers_to_private_runtime_helper() {
|
||||
let fixture = write_fixture(
|
||||
"lowering",
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(if (= (std.json.quote_string "slo\"vo") "\"slo\\\"vo\"")
|
||||
0
|
||||
1))
|
||||
"#,
|
||||
);
|
||||
let output = run_glagol([fixture.as_os_str()]);
|
||||
assert_success("compile standard json lowering", &output);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
assert!(
|
||||
stdout.contains("declare ptr @__glagol_json_quote_string(ptr)")
|
||||
&& stdout.contains("call ptr @__glagol_json_quote_string(")
|
||||
&& !stdout.contains("@std.json.quote_string"),
|
||||
"standard json LLVM shape drifted\nstdout:\n{}",
|
||||
stdout
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_runner_reports_deterministic_json_quoting() {
|
||||
let fixture = write_fixture(
|
||||
"test-runner",
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(test "quote plain string"
|
||||
(= (std.json.quote_string "slovo") "\"slovo\""))
|
||||
|
||||
(test "quote embedded quote"
|
||||
(= (std.json.quote_string "slo\"vo") "\"slo\\\"vo\""))
|
||||
|
||||
(test "quote backslash"
|
||||
(= (std.json.quote_string "slo\\vo") "\"slo\\\\vo\""))
|
||||
|
||||
(test "quote newline tab"
|
||||
(= (std.json.quote_string "line\n\tnext") "\"line\\n\\tnext\""))
|
||||
"#,
|
||||
);
|
||||
let output = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
|
||||
assert_success("run standard json tests", &output);
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
concat!(
|
||||
"test \"quote plain string\" ... ok\n",
|
||||
"test \"quote embedded quote\" ... ok\n",
|
||||
"test \"quote backslash\" ... ok\n",
|
||||
"test \"quote newline tab\" ... ok\n",
|
||||
"4 test(s) passed\n",
|
||||
),
|
||||
"standard json test runner stdout drifted"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_json_diagnostics_cover_promoted_and_deferred_names() {
|
||||
let cases = [
|
||||
(
|
||||
"quote-arity",
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(std.json.quote_string))
|
||||
"#,
|
||||
"ArityMismatch",
|
||||
),
|
||||
(
|
||||
"quote-type",
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(std.json.quote_string 42)
|
||||
0)
|
||||
"#,
|
||||
"TypeMismatch",
|
||||
),
|
||||
(
|
||||
"parse-object-deferred",
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(std.json.parse_object_result "{}"))
|
||||
"#,
|
||||
"UnsupportedStandardLibraryCall",
|
||||
),
|
||||
(
|
||||
"promoted-shadow",
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(fn std.json.quote_string ((value string)) -> string
|
||||
value)
|
||||
|
||||
(fn main () -> i32
|
||||
(if (= (std.json.quote_string "x") "\"x\"") 0 1))
|
||||
"#,
|
||||
"DuplicateFunction",
|
||||
),
|
||||
];
|
||||
|
||||
for (name, source, diagnostic) in cases {
|
||||
let fixture = write_fixture(name, source);
|
||||
let output = run_glagol([fixture.as_os_str()]);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"compiler unexpectedly accepted `{}`\nstdout:\n{}\nstderr:\n{}",
|
||||
name,
|
||||
stdout,
|
||||
stderr
|
||||
);
|
||||
assert!(
|
||||
stdout.is_empty(),
|
||||
"rejected compile wrote stdout:\n{}",
|
||||
stdout
|
||||
);
|
||||
assert!(
|
||||
stderr.contains(diagnostic),
|
||||
"diagnostic `{}` was not reported for `{}`\nstderr:\n{}",
|
||||
diagnostic,
|
||||
name,
|
||||
stderr
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hosted_json_quote_smoke_when_clang_is_available() {
|
||||
if !clang_is_available() {
|
||||
eprintln!("skipping standard json runtime smoke: set GLAGOL_CLANG or install clang");
|
||||
return;
|
||||
}
|
||||
|
||||
let fixture = write_fixture(
|
||||
"hosted",
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(if (= (std.json.quote_string "line\nnext") "\"line\\nnext\"")
|
||||
0
|
||||
1))
|
||||
"#,
|
||||
);
|
||||
let binary = fixture.with_extension(env::consts::EXE_EXTENSION);
|
||||
let build = run_glagol([
|
||||
OsStr::new("build"),
|
||||
fixture.as_os_str(),
|
||||
OsStr::new("-o"),
|
||||
binary.as_os_str(),
|
||||
]);
|
||||
assert_success("build standard json hosted smoke", &build);
|
||||
|
||||
let run = Command::new(&binary)
|
||||
.output()
|
||||
.unwrap_or_else(|err| panic!("run `{}`: {}", binary.display(), err));
|
||||
assert_success("run standard json hosted smoke", &run);
|
||||
}
|
||||
|
||||
fn write_fixture(name: &str, source: &str) -> PathBuf {
|
||||
let id = NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed);
|
||||
let dir = env::temp_dir().join(format!("glagol-standard-json-{id}-{name}"));
|
||||
fs::create_dir_all(&dir).unwrap_or_else(|err| panic!("create `{}`: {}", dir.display(), err));
|
||||
let path = dir.join("main.slo");
|
||||
fs::write(&path, source).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err));
|
||||
path
|
||||
}
|
||||
|
||||
fn clang_is_available() -> bool {
|
||||
if env::var_os("GLAGOL_CLANG").is_some() {
|
||||
return true;
|
||||
}
|
||||
Command::new("clang")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.is_ok_and(|output| output.status.success())
|
||||
}
|
||||
|
||||
fn run_glagol<I, S>(args: I) -> Output
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<std::ffi::OsStr>,
|
||||
{
|
||||
Command::new(env!("CARGO_BIN_EXE_glagol"))
|
||||
.args(args)
|
||||
.current_dir(Path::new(env!("CARGO_MANIFEST_DIR")))
|
||||
.output()
|
||||
.expect("run glagol")
|
||||
}
|
||||
|
||||
fn assert_success(context: &str, output: &Output) {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{} failed\nstdout:\n{}\nstderr:\n{}",
|
||||
context,
|
||||
stdout,
|
||||
stderr
|
||||
);
|
||||
assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr);
|
||||
}
|
||||
313
compiler/tests/standard_json_document_scalar_parsing_beta21.rs
Normal file
313
compiler/tests/standard_json_document_scalar_parsing_beta21.rs
Normal file
@ -0,0 +1,313 @@
|
||||
use std::{
|
||||
env,
|
||||
ffi::OsStr,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Output},
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
|
||||
static NEXT_TEMP_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
const EXPECTED_TEST_OUTPUT: &str = concat!(
|
||||
"test \"explicit std json document scalar string\" ... ok\n",
|
||||
"test \"explicit std json document scalar bool\" ... ok\n",
|
||||
"test \"explicit std json document scalar integer\" ... ok\n",
|
||||
"test \"explicit std json document scalar float null\" ... ok\n",
|
||||
"test \"explicit std json document scalar failures\" ... ok\n",
|
||||
"test \"explicit std json document scalar all\" ... ok\n",
|
||||
"6 test(s) passed\n",
|
||||
);
|
||||
|
||||
const STANDARD_JSON_DOCUMENT_SCALAR_BETA21: &[&str] = &[
|
||||
"parse_string_document_result",
|
||||
"parse_bool_document_result",
|
||||
"parse_i32_document_result",
|
||||
"parse_u32_document_result",
|
||||
"parse_i64_document_result",
|
||||
"parse_u64_document_result",
|
||||
"parse_f64_document_result",
|
||||
"parse_null_document_result",
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn explicit_std_json_document_scalar_helpers_check_and_test() {
|
||||
let project = write_project(
|
||||
"std-json-document-scalar-beta21",
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(import std.json (parse_string_document_result parse_bool_document_result parse_i32_document_result parse_u32_document_result parse_i64_document_result parse_u64_document_result parse_f64_document_result parse_null_document_result))
|
||||
|
||||
(fn imported_json_document_string_ok () -> bool
|
||||
(if (= (std.result.unwrap_ok (parse_string_document_result " \"slovo\" ")) "slovo")
|
||||
(= (std.result.unwrap_ok (parse_string_document_result "\n\t\"slo\\\"vo\"\t")) "slo\"vo")
|
||||
false))
|
||||
|
||||
(fn imported_json_document_bool_ok () -> bool
|
||||
(if (std.result.unwrap_ok (parse_bool_document_result " true "))
|
||||
(= (std.result.unwrap_ok (parse_bool_document_result "\nfalse\t")) false)
|
||||
false))
|
||||
|
||||
(fn imported_json_document_integer_ok () -> bool
|
||||
(if (= (std.result.unwrap_ok (parse_i32_document_result " -7 ")) -7)
|
||||
(if (= (std.result.unwrap_ok (parse_u32_document_result "\n7\t")) 7u32)
|
||||
(if (= (std.result.unwrap_ok (parse_i64_document_result " -8 ")) -8i64)
|
||||
(= (std.result.unwrap_ok (parse_u64_document_result "9 ")) 9u64)
|
||||
false)
|
||||
false)
|
||||
false))
|
||||
|
||||
(fn imported_json_document_float_null_ok () -> bool
|
||||
(if (= (std.result.unwrap_ok (parse_f64_document_result " 1e2 ")) 100.0)
|
||||
(std.result.unwrap_ok (parse_null_document_result "\nnull\t"))
|
||||
false))
|
||||
|
||||
(fn imported_json_document_failures_ok () -> bool
|
||||
(if (= (std.result.unwrap_err (parse_string_document_result "\"slovo\" x")) 1)
|
||||
(if (= (std.result.unwrap_err (parse_bool_document_result " TRUE ")) 1)
|
||||
(if (= (std.result.unwrap_err (parse_i32_document_result " 01 ")) 1)
|
||||
(if (= (std.result.unwrap_err (parse_u32_document_result " -1 ")) 1)
|
||||
(if (= (std.result.unwrap_err (parse_i64_document_result " 8i64 ")) 1)
|
||||
(if (= (std.result.unwrap_err (parse_u64_document_result " ")) 1)
|
||||
(if (= (std.result.unwrap_err (parse_f64_document_result " 01.0 ")) 1)
|
||||
(= (std.result.unwrap_err (parse_null_document_result " NULL ")) 1)
|
||||
false)
|
||||
false)
|
||||
false)
|
||||
false)
|
||||
false)
|
||||
false)
|
||||
false))
|
||||
|
||||
(fn imported_json_document_scalar_all_ok () -> bool
|
||||
(if (imported_json_document_string_ok)
|
||||
(if (imported_json_document_bool_ok)
|
||||
(if (imported_json_document_integer_ok)
|
||||
(if (imported_json_document_float_null_ok)
|
||||
(imported_json_document_failures_ok)
|
||||
false)
|
||||
false)
|
||||
false)
|
||||
false))
|
||||
|
||||
(fn main () -> i32
|
||||
(if (imported_json_document_scalar_all_ok)
|
||||
42
|
||||
1))
|
||||
|
||||
(test "explicit std json document scalar string"
|
||||
(imported_json_document_string_ok))
|
||||
|
||||
(test "explicit std json document scalar bool"
|
||||
(imported_json_document_bool_ok))
|
||||
|
||||
(test "explicit std json document scalar integer"
|
||||
(imported_json_document_integer_ok))
|
||||
|
||||
(test "explicit std json document scalar float null"
|
||||
(imported_json_document_float_null_ok))
|
||||
|
||||
(test "explicit std json document scalar failures"
|
||||
(imported_json_document_failures_ok))
|
||||
|
||||
(test "explicit std json document scalar all"
|
||||
(= (main) 42))
|
||||
"#,
|
||||
);
|
||||
|
||||
let source = read(&project.join("src/main.slo"));
|
||||
let std_json = read(&std_json_path());
|
||||
|
||||
assert!(
|
||||
!project.join("src/json.slo").exists(),
|
||||
"beta21 fixture must exercise repo-root std.json, not a local module copy"
|
||||
);
|
||||
assert!(
|
||||
source.starts_with("(module main)\n\n(import std.json ("),
|
||||
"beta21 fixture must use an explicit std.json import"
|
||||
);
|
||||
assert_json_document_scalar_helpers_are_source_authored(&std_json);
|
||||
|
||||
let fmt = run_glagol([
|
||||
OsStr::new("fmt"),
|
||||
OsStr::new("--check"),
|
||||
project.as_os_str(),
|
||||
]);
|
||||
assert_success("std json document scalar fmt --check", &fmt);
|
||||
|
||||
let check = run_glagol([OsStr::new("check"), project.as_os_str()]);
|
||||
assert_success_stdout(check, "", "std json document scalar check");
|
||||
|
||||
let test = run_glagol([OsStr::new("test"), project.as_os_str()]);
|
||||
assert_success_stdout(test, EXPECTED_TEST_OUTPUT, "std json document scalar test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_document_scalar_helpers_are_not_compiler_known_runtime_calls() {
|
||||
let std_json = read(&std_json_path());
|
||||
assert_json_document_scalar_helpers_are_source_authored(&std_json);
|
||||
|
||||
for helper in STANDARD_JSON_DOCUMENT_SCALAR_BETA21 {
|
||||
let fixture = write_fixture(
|
||||
helper,
|
||||
&format!(
|
||||
"(module main)\n\n(fn main () -> i32\n (std.result.unwrap_err (std.json.{} \"invalid\")))\n",
|
||||
helper
|
||||
),
|
||||
);
|
||||
let output = run_glagol([fixture.as_os_str()]);
|
||||
assert_failure_stderr_contains(
|
||||
&format!("direct std.json.{} runtime call", helper),
|
||||
&output,
|
||||
&format!(
|
||||
"standard library call `std.json.{}` is not supported",
|
||||
helper
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_json_document_scalar_helpers_are_source_authored(std_json: &str) {
|
||||
assert!(
|
||||
std_json.starts_with("(module json (export "),
|
||||
"lib/std/json.slo must stay a source-authored module export"
|
||||
);
|
||||
|
||||
for helper in STANDARD_JSON_DOCUMENT_SCALAR_BETA21 {
|
||||
assert!(
|
||||
std_json.contains(&format!("(fn {} ", helper)),
|
||||
"lib/std/json.slo is missing source facade `{}`",
|
||||
helper
|
||||
);
|
||||
assert!(
|
||||
!std_json.contains(&format!("std.json.{}", helper)),
|
||||
"std.json.{} must remain source-authored, not a compiler-known runtime call",
|
||||
helper
|
||||
);
|
||||
}
|
||||
|
||||
for private_prefix in [
|
||||
"__glagol_json_parse_string_document",
|
||||
"__glagol_json_parse_bool_document",
|
||||
"__glagol_json_parse_i32_document",
|
||||
"__glagol_json_parse_u32_document",
|
||||
"__glagol_json_parse_i64_document",
|
||||
"__glagol_json_parse_u64_document",
|
||||
"__glagol_json_parse_f64_document",
|
||||
"__glagol_json_parse_null_document",
|
||||
] {
|
||||
assert!(
|
||||
!std_json.contains(private_prefix),
|
||||
"lib/std/json.slo must not introduce private JSON document runtime symbol `{}`",
|
||||
private_prefix
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn run_glagol<I, S>(args: I) -> Output
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<OsStr>,
|
||||
{
|
||||
Command::new(env!("CARGO_BIN_EXE_glagol"))
|
||||
.args(args)
|
||||
.current_dir(Path::new(env!("CARGO_MANIFEST_DIR")))
|
||||
.output()
|
||||
.expect("run glagol")
|
||||
}
|
||||
|
||||
fn write_project(name: &str, source: &str) -> PathBuf {
|
||||
let root = temp_root(name);
|
||||
let src = root.join("src");
|
||||
fs::create_dir_all(&src).unwrap_or_else(|err| panic!("create `{}`: {}", src.display(), err));
|
||||
fs::write(
|
||||
root.join("slovo.toml"),
|
||||
format!(
|
||||
"[project]\nname = \"{}\"\nsource_root = \"src\"\nentry = \"main\"\n",
|
||||
name
|
||||
),
|
||||
)
|
||||
.unwrap_or_else(|err| panic!("write project manifest: {}", err));
|
||||
fs::write(src.join("main.slo"), source.trim_start())
|
||||
.unwrap_or_else(|err| panic!("write project main.slo: {}", err));
|
||||
root
|
||||
}
|
||||
|
||||
fn write_fixture(name: &str, source: &str) -> PathBuf {
|
||||
let mut path = env::temp_dir();
|
||||
path.push(format!(
|
||||
"glagol-standard-json-document-scalar-beta21-{}-{}-{}.slo",
|
||||
name,
|
||||
std::process::id(),
|
||||
NEXT_TEMP_ID.fetch_add(1, Ordering::Relaxed)
|
||||
));
|
||||
fs::write(&path, source.trim_start())
|
||||
.unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err));
|
||||
path
|
||||
}
|
||||
|
||||
fn temp_root(name: &str) -> PathBuf {
|
||||
let root = env::temp_dir().join(format!(
|
||||
"glagol-standard-json-document-scalar-beta21-{}-{}-{}",
|
||||
name,
|
||||
std::process::id(),
|
||||
NEXT_TEMP_ID.fetch_add(1, Ordering::Relaxed)
|
||||
));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
fs::create_dir_all(&root).unwrap_or_else(|err| panic!("create `{}`: {}", root.display(), err));
|
||||
root
|
||||
}
|
||||
|
||||
fn std_json_path() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../lib/std/json.slo")
|
||||
}
|
||||
|
||||
fn read(path: &Path) -> String {
|
||||
fs::read_to_string(path).unwrap_or_else(|err| panic!("read `{}`: {}", path.display(), err))
|
||||
}
|
||||
|
||||
fn assert_success(context: &str, output: &Output) {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{} failed\nstatus: {:?}\nstdout:\n{}\nstderr:\n{}",
|
||||
context,
|
||||
output.status.code(),
|
||||
stdout,
|
||||
stderr
|
||||
);
|
||||
assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr);
|
||||
}
|
||||
|
||||
fn assert_success_stdout(output: Output, expected: &str, context: &str) {
|
||||
assert_success(context, &output);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert_eq!(stdout, expected, "{}", context);
|
||||
}
|
||||
|
||||
fn assert_failure_stderr_contains(context: &str, output: &Output, needle: &str) {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"{} unexpectedly passed\nstdout:\n{}\nstderr:\n{}",
|
||||
context,
|
||||
stdout,
|
||||
stderr
|
||||
);
|
||||
assert!(
|
||||
stdout.is_empty(),
|
||||
"{} rejected compile wrote stdout:\n{}",
|
||||
context,
|
||||
stdout
|
||||
);
|
||||
assert!(
|
||||
stderr.contains(needle),
|
||||
"{} stderr did not contain `{}`:\n{}",
|
||||
context,
|
||||
needle,
|
||||
stderr
|
||||
);
|
||||
}
|
||||
424
compiler/tests/standard_json_scalar_parsing_beta17.rs
Normal file
424
compiler/tests/standard_json_scalar_parsing_beta17.rs
Normal file
@ -0,0 +1,424 @@
|
||||
use std::{
|
||||
env,
|
||||
ffi::OsStr,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Output},
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
|
||||
static NEXT_FIXTURE_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
const JSON_SCALAR_PARSE_NAMES: &[(&str, &str, &str)] = &[
|
||||
(
|
||||
"parse_bool_value_result",
|
||||
"__glagol_json_parse_bool_value_result",
|
||||
"(result bool i32)",
|
||||
),
|
||||
(
|
||||
"parse_i32_value_result",
|
||||
"__glagol_json_parse_i32_value_result",
|
||||
"(result i32 i32)",
|
||||
),
|
||||
(
|
||||
"parse_u32_value_result",
|
||||
"__glagol_json_parse_u32_value_result",
|
||||
"(result u32 i32)",
|
||||
),
|
||||
(
|
||||
"parse_i64_value_result",
|
||||
"__glagol_json_parse_i64_value_result",
|
||||
"(result i64 i32)",
|
||||
),
|
||||
(
|
||||
"parse_u64_value_result",
|
||||
"__glagol_json_parse_u64_value_result",
|
||||
"(result u64 i32)",
|
||||
),
|
||||
(
|
||||
"parse_f64_value_result",
|
||||
"__glagol_json_parse_f64_value_result",
|
||||
"(result f64 i32)",
|
||||
),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn json_scalar_parsers_lower_to_private_runtime_helpers() {
|
||||
let fixture = write_fixture(
|
||||
"lowering",
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(std.result.unwrap_ok (std.json.parse_bool_value_result "true"))
|
||||
(std.result.unwrap_ok (std.json.parse_i32_value_result "-0"))
|
||||
(std.result.unwrap_ok (std.json.parse_u32_value_result "4294967295"))
|
||||
(std.result.unwrap_ok (std.json.parse_i64_value_result "-9223372036854775808"))
|
||||
(std.result.unwrap_ok (std.json.parse_u64_value_result "18446744073709551615"))
|
||||
(std.result.unwrap_ok (std.json.parse_f64_value_result "1e2"))
|
||||
0)
|
||||
"#,
|
||||
);
|
||||
let output = run_glagol([fixture.as_os_str()]);
|
||||
assert_success("compile json scalar parser lowering", &output);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
for (_, symbol, _) in JSON_SCALAR_PARSE_NAMES {
|
||||
assert!(
|
||||
stdout.contains(&format!("@{}", symbol)),
|
||||
"missing JSON scalar parser runtime symbol `{}`\nstdout:\n{}",
|
||||
symbol,
|
||||
stdout
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
stdout.contains("declare i64 @__glagol_json_parse_i32_value_result(ptr)")
|
||||
&& stdout.contains("declare i64 @__glagol_json_parse_u32_value_result(ptr)")
|
||||
&& stdout.contains("declare i32 @__glagol_json_parse_i64_value_result(ptr, ptr)")
|
||||
&& stdout.contains("declare i32 @__glagol_json_parse_u64_value_result(ptr, ptr)")
|
||||
&& stdout.contains("declare i32 @__glagol_json_parse_f64_value_result(ptr, ptr)")
|
||||
&& stdout.contains("declare i32 @__glagol_json_parse_bool_value_result(ptr, ptr)")
|
||||
&& stdout.contains("call i64 @__glagol_json_parse_i32_value_result(")
|
||||
&& stdout.contains("call i64 @__glagol_json_parse_u32_value_result(")
|
||||
&& stdout.contains("call i32 @__glagol_json_parse_i64_value_result(")
|
||||
&& stdout.contains("call i32 @__glagol_json_parse_u64_value_result(")
|
||||
&& stdout.contains("call i32 @__glagol_json_parse_f64_value_result(")
|
||||
&& stdout.contains("call i32 @__glagol_json_parse_bool_value_result(")
|
||||
&& !stdout.contains("@std.json.parse_i32_value_result"),
|
||||
"JSON scalar parser LLVM shape drifted\nstdout:\n{}",
|
||||
stdout
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_runner_enforces_json_token_scalar_contract() {
|
||||
let fixture = write_fixture(
|
||||
"test-runner",
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(test "json bool true ok"
|
||||
(std.result.unwrap_ok (std.json.parse_bool_value_result "true")))
|
||||
|
||||
(test "json bool uppercase err"
|
||||
(= (std.result.unwrap_err (std.json.parse_bool_value_result "TRUE")) 1))
|
||||
|
||||
(test "json i32 negative zero ok"
|
||||
(= (std.result.unwrap_ok (std.json.parse_i32_value_result "-0")) 0))
|
||||
|
||||
(test "json i32 leading zero err"
|
||||
(= (std.result.unwrap_err (std.json.parse_i32_value_result "01")) 1))
|
||||
|
||||
(test "json i32 plus err"
|
||||
(= (std.result.unwrap_err (std.json.parse_i32_value_result "+1")) 1))
|
||||
|
||||
(test "json u32 negative err"
|
||||
(= (std.result.unwrap_err (std.json.parse_u32_value_result "-1")) 1))
|
||||
|
||||
(test "json i64 max ok"
|
||||
(= (std.result.unwrap_ok (std.json.parse_i64_value_result "9223372036854775807")) 9223372036854775807i64))
|
||||
|
||||
(test "json u64 max ok"
|
||||
(= (std.result.unwrap_ok (std.json.parse_u64_value_result "18446744073709551615")) 18446744073709551615u64))
|
||||
|
||||
(test "json f64 exponent ok"
|
||||
(= (std.result.unwrap_ok (std.json.parse_f64_value_result "1e2")) 100.0))
|
||||
|
||||
(test "json f64 leading zero err"
|
||||
(= (std.result.unwrap_err (std.json.parse_f64_value_result "01.0")) 1))
|
||||
|
||||
(test "json f64 nonfinite err"
|
||||
(= (std.result.unwrap_err (std.json.parse_f64_value_result "1e309")) 1))
|
||||
|
||||
(test "json no leading whitespace"
|
||||
(= (std.result.unwrap_err (std.json.parse_i64_value_result " 1")) 1))
|
||||
|
||||
(test "json no trailing whitespace"
|
||||
(= (std.result.unwrap_err (std.json.parse_f64_value_result "1 ")) 1))
|
||||
"#,
|
||||
);
|
||||
let output = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
|
||||
assert_success("run json scalar parser tests", &output);
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
concat!(
|
||||
"test \"json bool true ok\" ... ok\n",
|
||||
"test \"json bool uppercase err\" ... ok\n",
|
||||
"test \"json i32 negative zero ok\" ... ok\n",
|
||||
"test \"json i32 leading zero err\" ... ok\n",
|
||||
"test \"json i32 plus err\" ... ok\n",
|
||||
"test \"json u32 negative err\" ... ok\n",
|
||||
"test \"json i64 max ok\" ... ok\n",
|
||||
"test \"json u64 max ok\" ... ok\n",
|
||||
"test \"json f64 exponent ok\" ... ok\n",
|
||||
"test \"json f64 leading zero err\" ... ok\n",
|
||||
"test \"json f64 nonfinite err\" ... ok\n",
|
||||
"test \"json no leading whitespace\" ... ok\n",
|
||||
"test \"json no trailing whitespace\" ... ok\n",
|
||||
"13 test(s) passed\n",
|
||||
),
|
||||
"json scalar parser test runner stdout drifted"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hosted_json_scalar_parsers_smoke_when_toolchain_is_available() {
|
||||
let fixture = write_fixture(
|
||||
"runtime-smoke",
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(std.io.print_bool (std.result.unwrap_ok (std.json.parse_bool_value_result "true")))
|
||||
(std.io.print_i32 (std.result.unwrap_ok (std.json.parse_i32_value_result "-0")))
|
||||
(std.io.print_u32 (std.result.unwrap_ok (std.json.parse_u32_value_result "4294967295")))
|
||||
(std.io.print_i64 (std.result.unwrap_ok (std.json.parse_i64_value_result "-9223372036854775808")))
|
||||
(std.io.print_u64 (std.result.unwrap_ok (std.json.parse_u64_value_result "18446744073709551615")))
|
||||
(std.io.print_string (std.num.f64_to_string (std.result.unwrap_ok (std.json.parse_f64_value_result "1e2"))))
|
||||
(std.result.unwrap_err (std.json.parse_i32_value_result "01")))
|
||||
"#,
|
||||
);
|
||||
let binary = unique_path("json-scalar-parsing-beta17-bin");
|
||||
let build = run_glagol([
|
||||
OsStr::new("build"),
|
||||
fixture.as_os_str(),
|
||||
OsStr::new("-o"),
|
||||
binary.as_os_str(),
|
||||
]);
|
||||
if !build.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&build.stderr);
|
||||
assert!(
|
||||
stderr.contains("ToolchainUnavailable"),
|
||||
"json scalar parser build failed unexpectedly\nstdout:\n{}\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&build.stdout),
|
||||
stderr
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let run = Command::new(&binary)
|
||||
.output()
|
||||
.unwrap_or_else(|err| panic!("run `{}`: {}", binary.display(), err));
|
||||
assert_eq!(
|
||||
run.status.code(),
|
||||
Some(1),
|
||||
"json scalar parser binary exit code drifted\nstdout:\n{}\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&run.stdout),
|
||||
String::from_utf8_lossy(&run.stderr)
|
||||
);
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&run.stdout),
|
||||
concat!(
|
||||
"true\n",
|
||||
"0\n",
|
||||
"4294967295\n",
|
||||
"-9223372036854775808\n",
|
||||
"18446744073709551615\n",
|
||||
"100.0\n",
|
||||
),
|
||||
"json scalar parser binary stdout drifted"
|
||||
);
|
||||
assert!(
|
||||
run.stderr.is_empty(),
|
||||
"json scalar parser binary wrote stderr:\n{}",
|
||||
String::from_utf8_lossy(&run.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_scalar_parser_diagnostics_cover_promoted_names_and_shadowing() {
|
||||
for (name, _, return_type) in JSON_SCALAR_PARSE_NAMES {
|
||||
let arity = write_fixture(
|
||||
&format!("{name}-arity"),
|
||||
&format!(
|
||||
"(module main)\n\n(fn main () -> {}\n (std.json.{}))\n",
|
||||
return_type, name
|
||||
),
|
||||
);
|
||||
assert_rejected(
|
||||
&format!("std.json.{name} arity"),
|
||||
run_glagol([arity.as_os_str()]),
|
||||
"wrong number of arguments",
|
||||
);
|
||||
|
||||
let type_mismatch = write_fixture(
|
||||
&format!("{name}-type"),
|
||||
&format!(
|
||||
"(module main)\n\n(fn main () -> {}\n (std.json.{} 1))\n",
|
||||
return_type, name
|
||||
),
|
||||
);
|
||||
assert_rejected(
|
||||
&format!("std.json.{name} type"),
|
||||
run_glagol([type_mismatch.as_os_str()]),
|
||||
&format!(
|
||||
"cannot call `std.json.{}` with argument of wrong type",
|
||||
name
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
let source_shadow = write_fixture(
|
||||
"source-shadow",
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(fn std.json.parse_i32_value_result ((text string)) -> (result i32 i32)
|
||||
(err i32 i32 1))
|
||||
|
||||
(fn main () -> i32
|
||||
0)
|
||||
"#,
|
||||
);
|
||||
assert_rejected(
|
||||
"std.json.parse_i32_value_result source shadow",
|
||||
run_glagol([source_shadow.as_os_str()]),
|
||||
"DuplicateFunction",
|
||||
);
|
||||
|
||||
let helper_shadow = write_fixture(
|
||||
"helper-shadow",
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(fn __glagol_json_parse_i32_value_result ((text string)) -> (result i32 i32)
|
||||
(err i32 i32 1))
|
||||
|
||||
(fn main () -> i32
|
||||
0)
|
||||
"#,
|
||||
);
|
||||
assert_rejected(
|
||||
"__glagol_json_parse_i32_value_result helper shadow",
|
||||
run_glagol([helper_shadow.as_os_str()]),
|
||||
"DuplicateFunction",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deferred_json_parser_families_remain_unsupported() {
|
||||
for name in [
|
||||
"parse_object_result",
|
||||
"parse_array_result",
|
||||
"parse_value_result",
|
||||
"tokenize_result",
|
||||
"schema_validate_result",
|
||||
"stream_parse_result",
|
||||
] {
|
||||
let fixture = write_fixture(
|
||||
name,
|
||||
&format!(
|
||||
"(module main)\n\n(fn main () -> i32\n (std.json.{} \"[]\")\n 0)\n",
|
||||
name
|
||||
),
|
||||
);
|
||||
assert_rejected(
|
||||
&format!("std.json.{name} deferred"),
|
||||
run_glagol([fixture.as_os_str()]),
|
||||
&format!("standard library call `std.json.{}` is not supported", name),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_json_diagnostics_list_beta17_promoted_scalar_parsers() {
|
||||
let fixture = write_fixture(
|
||||
"unsupported-guidance",
|
||||
r#"
|
||||
(module main)
|
||||
|
||||
(fn main () -> i32
|
||||
(std.json.parse_value_result "null")
|
||||
0)
|
||||
"#,
|
||||
);
|
||||
let output = run_glagol([fixture.as_os_str()]);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"unsupported JSON value parser unexpectedly compiled\nstdout:\n{}\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
stderr
|
||||
);
|
||||
assert!(
|
||||
stderr.contains("standard library call `std.json.parse_value_result` is not supported"),
|
||||
"unsupported JSON parser diagnostic drifted\nstderr:\n{}",
|
||||
stderr
|
||||
);
|
||||
for (name, _, _) in JSON_SCALAR_PARSE_NAMES {
|
||||
let promoted = format!("std.json.{name}");
|
||||
assert!(
|
||||
stderr.contains(&promoted),
|
||||
"unsupported std guidance omitted promoted beta17 name `{}`\nstderr:\n{}",
|
||||
promoted,
|
||||
stderr
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn write_fixture(name: &str, source: &str) -> PathBuf {
|
||||
let id = NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed);
|
||||
let dir = env::temp_dir().join(format!("glagol-json-scalar-beta17-{id}-{name}"));
|
||||
fs::create_dir_all(&dir).unwrap_or_else(|err| panic!("create `{}`: {}", dir.display(), err));
|
||||
let path = dir.join("main.slo");
|
||||
fs::write(&path, source).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err));
|
||||
path
|
||||
}
|
||||
|
||||
fn unique_path(name: &str) -> PathBuf {
|
||||
let id = NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed);
|
||||
env::temp_dir().join(format!("glagol-{name}-{id}{}", env::consts::EXE_SUFFIX))
|
||||
}
|
||||
|
||||
fn run_glagol<I, S>(args: I) -> Output
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<OsStr>,
|
||||
{
|
||||
Command::new(env!("CARGO_BIN_EXE_glagol"))
|
||||
.args(args)
|
||||
.current_dir(Path::new(env!("CARGO_MANIFEST_DIR")))
|
||||
.output()
|
||||
.expect("run glagol")
|
||||
}
|
||||
|
||||
fn assert_success(context: &str, output: &Output) {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{} failed\nstdout:\n{}\nstderr:\n{}",
|
||||
context,
|
||||
stdout,
|
||||
stderr
|
||||
);
|
||||
assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr);
|
||||
}
|
||||
|
||||
fn assert_rejected(context: &str, output: Output, expected: &str) {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"{} unexpectedly succeeded\nstdout:\n{}\nstderr:\n{}",
|
||||
context,
|
||||
stdout,
|
||||
stderr
|
||||
);
|
||||
assert!(
|
||||
stdout.is_empty(),
|
||||
"{} rejected compile wrote stdout:\n{}",
|
||||
context,
|
||||
stdout
|
||||
);
|
||||
assert!(
|
||||
stderr.contains(expected),
|
||||
"{} diagnostic drifted; expected `{}`\nstderr:\n{}",
|
||||
context,
|
||||
expected,
|
||||
stderr
|
||||
);
|
||||
}
|
||||
350
compiler/tests/standard_json_source_facade_alpha.rs
Normal file
350
compiler/tests/standard_json_source_facade_alpha.rs
Normal file
@ -0,0 +1,350 @@
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
fs,
|
||||
path::Path,
|
||||
process::{Command, Output},
|
||||
};
|
||||
|
||||
const EXPECTED_LOCAL_TEST_OUTPUT: &str = concat!(
|
||||
"test \"explicit local json quote escapes facade\" ... ok\n",
|
||||
"test \"explicit local json scalar values facade\" ... ok\n",
|
||||
"test \"explicit local json primitive scalar parse success facade\" ... ok\n",
|
||||
"test \"explicit local json primitive scalar parse failure facade\" ... ok\n",
|
||||
"test \"explicit local json string token parse success facade\" ... ok\n",
|
||||
"test \"explicit local json string token parse failure facade\" ... ok\n",
|
||||
"test \"explicit local json document parse trimmed success facade\" ... ok\n",
|
||||
"test \"explicit local json document parse plain success facade\" ... ok\n",
|
||||
"test \"explicit local json document parse trailing failure facade\" ... ok\n",
|
||||
"test \"explicit local json fields facade\" ... ok\n",
|
||||
"test \"explicit local json arrays objects facade\" ... ok\n",
|
||||
"test \"explicit local json facade all\" ... ok\n",
|
||||
"12 test(s) passed\n",
|
||||
);
|
||||
|
||||
const EXPECTED_STD_IMPORT_TEST_OUTPUT: &str = concat!(
|
||||
"test \"explicit std json quote escapes facade\" ... ok\n",
|
||||
"test \"explicit std json scalar values facade\" ... ok\n",
|
||||
"test \"explicit std json primitive scalar parse success facade\" ... ok\n",
|
||||
"test \"explicit std json primitive scalar parse failure facade\" ... ok\n",
|
||||
"test \"explicit std json string token parse success facade\" ... ok\n",
|
||||
"test \"explicit std json string token parse failure facade\" ... ok\n",
|
||||
"test \"explicit std json document parse trimmed success facade\" ... ok\n",
|
||||
"test \"explicit std json document parse plain success facade\" ... ok\n",
|
||||
"test \"explicit std json document parse trailing failure facade\" ... ok\n",
|
||||
"test \"explicit std json fields facade\" ... ok\n",
|
||||
"test \"explicit std json arrays objects facade\" ... ok\n",
|
||||
"test \"explicit std json facade all\" ... ok\n",
|
||||
"12 test(s) passed\n",
|
||||
);
|
||||
|
||||
const STANDARD_JSON_SOURCE_FACADE_ALPHA: &[&str] = &[
|
||||
"quote_string",
|
||||
"null_value",
|
||||
"bool_value",
|
||||
"i32_value",
|
||||
"u32_value",
|
||||
"i64_value",
|
||||
"u64_value",
|
||||
"f64_value",
|
||||
"parse_string_value_result",
|
||||
"parse_bool_value_result",
|
||||
"parse_i32_value_result",
|
||||
"parse_u32_value_result",
|
||||
"parse_i64_value_result",
|
||||
"parse_u64_value_result",
|
||||
"parse_f64_value_result",
|
||||
"parse_null_value_result",
|
||||
"parse_string_document_result",
|
||||
"parse_bool_document_result",
|
||||
"parse_i32_document_result",
|
||||
"parse_u32_document_result",
|
||||
"parse_i64_document_result",
|
||||
"parse_u64_document_result",
|
||||
"parse_f64_document_result",
|
||||
"parse_null_document_result",
|
||||
"field_string",
|
||||
"field_bool",
|
||||
"field_i32",
|
||||
"field_u32",
|
||||
"field_i64",
|
||||
"field_u64",
|
||||
"field_f64",
|
||||
"field_null",
|
||||
"array0",
|
||||
"array1",
|
||||
"array2",
|
||||
"array3",
|
||||
"object0",
|
||||
"object1",
|
||||
"object2",
|
||||
"object3",
|
||||
];
|
||||
|
||||
const STANDARD_JSON_RUNTIME_NAMES: &[&str] = &[
|
||||
"std.json.quote_string",
|
||||
"std.string.concat",
|
||||
"std.num.i32_to_string",
|
||||
"std.num.u32_to_string",
|
||||
"std.num.i64_to_string",
|
||||
"std.num.u64_to_string",
|
||||
"std.num.f64_to_string",
|
||||
"std.json.parse_string_value_result",
|
||||
"std.json.parse_bool_value_result",
|
||||
"std.json.parse_i32_value_result",
|
||||
"std.json.parse_u32_value_result",
|
||||
"std.json.parse_i64_value_result",
|
||||
"std.json.parse_u64_value_result",
|
||||
"std.json.parse_f64_value_result",
|
||||
];
|
||||
|
||||
const STANDARD_JSON_ALLOWED_STD_NAMES: &[&str] = &[
|
||||
"(import std.string (trim_ascii))",
|
||||
"std.json.quote_string",
|
||||
"std.string.concat",
|
||||
"std.num.i32_to_string",
|
||||
"std.num.u32_to_string",
|
||||
"std.num.i64_to_string",
|
||||
"std.num.u64_to_string",
|
||||
"std.num.f64_to_string",
|
||||
"std.json.parse_string_value_result",
|
||||
"std.json.parse_bool_value_result",
|
||||
"std.json.parse_i32_value_result",
|
||||
"std.json.parse_u32_value_result",
|
||||
"std.json.parse_i64_value_result",
|
||||
"std.json.parse_u64_value_result",
|
||||
"std.json.parse_f64_value_result",
|
||||
];
|
||||
|
||||
const STANDARD_JSON_DOCUMENT_SCALAR_BETA21: &[&str] = &[
|
||||
"parse_string_document_result",
|
||||
"parse_bool_document_result",
|
||||
"parse_i32_document_result",
|
||||
"parse_u32_document_result",
|
||||
"parse_i64_document_result",
|
||||
"parse_u64_document_result",
|
||||
"parse_f64_document_result",
|
||||
"parse_null_document_result",
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn standard_json_source_facade_project_checks_formats_and_tests() {
|
||||
let project =
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/projects/std-layout-local-json");
|
||||
|
||||
assert_local_json_fixture_is_source_authored(&project);
|
||||
|
||||
let fmt = run_glagol([
|
||||
OsStr::new("fmt"),
|
||||
OsStr::new("--check"),
|
||||
project.as_os_str(),
|
||||
]);
|
||||
assert_success("std layout local json fmt --check", &fmt);
|
||||
|
||||
let check = run_glagol([OsStr::new("check"), project.as_os_str()]);
|
||||
assert_success_stdout(check, "", "std layout local json check");
|
||||
|
||||
let test = run_glagol([OsStr::new("test"), project.as_os_str()]);
|
||||
assert_success_stdout(
|
||||
test,
|
||||
EXPECTED_LOCAL_TEST_OUTPUT,
|
||||
"std layout local json test output",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_json_std_import_project_checks_formats_and_tests() {
|
||||
let project =
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/projects/std-import-json");
|
||||
|
||||
assert_std_import_json_fixture_uses_repo_std(&project);
|
||||
|
||||
let fmt = run_glagol([
|
||||
OsStr::new("fmt"),
|
||||
OsStr::new("--check"),
|
||||
project.as_os_str(),
|
||||
]);
|
||||
assert_success("std import json fmt --check", &fmt);
|
||||
|
||||
let check = run_glagol([OsStr::new("check"), project.as_os_str()]);
|
||||
assert_success_stdout(check, "", "std import json check");
|
||||
|
||||
let test = run_glagol([OsStr::new("test"), project.as_os_str()]);
|
||||
assert_success_stdout(
|
||||
test,
|
||||
EXPECTED_STD_IMPORT_TEST_OUTPUT,
|
||||
"std import json test output",
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_local_json_fixture_is_source_authored(project: &Path) {
|
||||
let json = read(&project.join("src/json.slo"));
|
||||
let main = read(&project.join("src/main.slo"));
|
||||
|
||||
assert!(
|
||||
json.starts_with("(module json (export "),
|
||||
"json.slo must stay an explicit local module export"
|
||||
);
|
||||
assert!(
|
||||
main.starts_with("(module main)\n\n(import json ("),
|
||||
"main.slo must stay an explicit local json import"
|
||||
);
|
||||
assert!(
|
||||
!main.contains("(import std") && !main.contains("(import slovo.std"),
|
||||
"json fixture must not depend on automatic or package std imports"
|
||||
);
|
||||
assert_json_source_shape(&json, &main, "local json fixture");
|
||||
assert!(
|
||||
!main.contains("std."),
|
||||
"local json main fixture must use only local imports"
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_std_import_json_fixture_uses_repo_std(project: &Path) {
|
||||
let std_json = read(&Path::new(env!("CARGO_MANIFEST_DIR")).join("../lib/std/json.slo"));
|
||||
let main = read(&project.join("src/main.slo"));
|
||||
|
||||
assert!(
|
||||
!project.join("src/json.slo").exists(),
|
||||
"std import json fixture must use repo-root std/json.slo, not a local copy"
|
||||
);
|
||||
assert!(
|
||||
main.starts_with("(module main)\n\n(import std.json ("),
|
||||
"std import json fixture must use explicit `std.json` import syntax"
|
||||
);
|
||||
assert_json_source_shape(&std_json, &main, "repo std.json fixture");
|
||||
}
|
||||
|
||||
fn assert_json_source_shape(json: &str, main: &str, context: &str) {
|
||||
for runtime_name in STANDARD_JSON_RUNTIME_NAMES {
|
||||
assert!(
|
||||
json.contains(runtime_name),
|
||||
"{} must wrap or compose `{}`",
|
||||
context,
|
||||
runtime_name
|
||||
);
|
||||
}
|
||||
assert_std_only_contains(json, STANDARD_JSON_ALLOWED_STD_NAMES, context);
|
||||
assert_deferred_json_surface_absent(json, main, context);
|
||||
assert_json_document_scalar_helpers_are_source_authored(json, context);
|
||||
|
||||
for helper in STANDARD_JSON_SOURCE_FACADE_ALPHA {
|
||||
assert!(
|
||||
json.contains(&format!("(fn {} ", helper)),
|
||||
"{} is missing source facade `{}`",
|
||||
context,
|
||||
helper
|
||||
);
|
||||
assert!(
|
||||
main.contains(helper),
|
||||
"{} main fixture import/use is missing `{}`",
|
||||
context,
|
||||
helper
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_json_document_scalar_helpers_are_source_authored(json: &str, context: &str) {
|
||||
for helper in STANDARD_JSON_DOCUMENT_SCALAR_BETA21 {
|
||||
assert!(
|
||||
!json.contains(&format!("std.json.{}", helper)),
|
||||
"{} must keep `{}` source-authored, not compiler-known",
|
||||
context,
|
||||
helper
|
||||
);
|
||||
}
|
||||
for private_prefix in [
|
||||
"__glagol_json_parse_string_document",
|
||||
"__glagol_json_parse_bool_document",
|
||||
"__glagol_json_parse_i32_document",
|
||||
"__glagol_json_parse_u32_document",
|
||||
"__glagol_json_parse_i64_document",
|
||||
"__glagol_json_parse_u64_document",
|
||||
"__glagol_json_parse_f64_document",
|
||||
"__glagol_json_parse_null_document",
|
||||
] {
|
||||
assert!(
|
||||
!json.contains(private_prefix),
|
||||
"{} must not introduce private JSON document runtime symbol `{}`",
|
||||
context,
|
||||
private_prefix
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_deferred_json_surface_absent(json: &str, main: &str, context: &str) {
|
||||
for deferred in [
|
||||
"parse_object",
|
||||
"parse_array",
|
||||
"parse_value",
|
||||
"tokenize",
|
||||
"tokenizer",
|
||||
"schema",
|
||||
"stream",
|
||||
"unicode",
|
||||
"map",
|
||||
] {
|
||||
assert!(
|
||||
!json.contains(deferred) && !main.contains(deferred),
|
||||
"{} must not claim deferred JSON `{}` policies",
|
||||
context,
|
||||
deferred
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_std_only_contains(source: &str, allowed: &[&str], context: &str) {
|
||||
let mut remaining = source.to_string();
|
||||
for name in allowed {
|
||||
remaining = remaining.replace(name, "");
|
||||
}
|
||||
assert!(
|
||||
!remaining.contains("std."),
|
||||
"{} introduced unexpected compiler-known std names",
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
fn run_glagol<I, S>(args: I) -> Output
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<std::ffi::OsStr>,
|
||||
{
|
||||
Command::new(env!("CARGO_BIN_EXE_glagol"))
|
||||
.args(args)
|
||||
.current_dir(Path::new(env!("CARGO_MANIFEST_DIR")))
|
||||
.output()
|
||||
.expect("run glagol")
|
||||
}
|
||||
|
||||
fn read(path: &Path) -> String {
|
||||
fs::read_to_string(path).unwrap_or_else(|err| panic!("read `{}`: {}", path.display(), err))
|
||||
}
|
||||
|
||||
fn assert_success(context: &str, output: &Output) {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{} failed\nstdout:\n{}\nstderr:\n{}",
|
||||
context,
|
||||
stdout,
|
||||
stderr
|
||||
);
|
||||
assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr);
|
||||
}
|
||||
|
||||
fn assert_success_stdout(output: Output, expected: &str, context: &str) {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{} failed\nstdout:\n{}\nstderr:\n{}",
|
||||
context,
|
||||
stdout,
|
||||
stderr
|
||||
);
|
||||
assert_eq!(stdout, expected, "{} stdout drifted", context);
|
||||
assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user