Compare commits

..

18 Commits

Author SHA1 Message Date
sanjin
573052fe00 Release 1.0.0-beta.25 user project conformance 2026-05-23 07:40:07 +02:00
sanjin
b241a8a812 Release 1.0.0-beta.24 package manifest discipline 2026-05-23 03:23:10 +02:00
sanjin
05ff5be5c5 Release 1.0.0-beta.23 stdlib stability tier catalog 2026-05-23 02:30:45 +02:00
sanjin
7f71beac4c Release 1.0.0-beta.22 run manifest execution report hardening 2026-05-23 02:15:44 +02:00
sanjin
87e627045e Release 1.0.0-beta.21 JSON document scalar parsing foundation 2026-05-23 01:40:34 +02:00
sanjin
c1231fdb5f Release 1.0.0-beta.20 string search and trim foundation 2026-05-23 01:22:39 +02:00
sanjin
98f81d2d59 Release 1.0.0-beta.19 test discovery foundation 2026-05-23 01:02:00 +02:00
sanjin
3b231b7f21 Release 1.0.0-beta.18 JSON string token parsing foundation 2026-05-23 00:40:38 +02:00
sanjin
1185a1fa18 Release 1.0.0-beta.17 JSON primitive scalar parsing foundation 2026-05-23 00:05:43 +02:00
sanjin
ddb8afd904 Release 1.0.0-beta.16 string scanning foundation 2026-05-22 23:00:07 +02:00
sanjin
436261730a Release 1.0.0-beta.15 collection boundary hardening 2026-05-22 22:22:00 +02:00
sanjin
d3e628553f Release 1.0.0-beta.14 benchmark suite catalog 2026-05-22 21:55:36 +02:00
sanjin
acbe58f70e Release 1.0.0-beta.13 diagnostic catalog and schema policy 2026-05-22 21:11:54 +02:00
sanjin
dd5302507d Release 1.0.0-beta.12 concrete vector query and prefix parity 2026-05-22 20:43:53 +02:00
sanjin
87f90ba264 Release 1.0.0-beta.11 local package api documentation 2026-05-22 20:26:00 +02:00
sanjin
f8f0862ee3 Release 1.0.0-beta.10 developer experience api discovery 2026-05-22 20:01:35 +02:00
sanjin
5a3ed0c41e Release 1.0.0-beta.9 collection generic reservation 2026-05-22 19:36:37 +02:00
sanjin
4f52a54bea Release 1.0.0-beta.8 concrete type aliases 2026-05-22 18:59:10 +02:00
254 changed files with 21046 additions and 2088 deletions

View 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`

View 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`

View 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`

View 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.

View 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.

View File

@ -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.

View File

@ -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

View 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.

View 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.

View 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
```

View 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
```

View 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
```

View 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
```

View File

@ -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
```

View File

@ -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
```

View File

@ -0,0 +1,95 @@
# 1.0.0-beta.25 User Project Conformance Matrix
## Scope
`1.0.0-beta.25` is a tooling/conformance evidence slice for ordinary
project and workspace usage. It adds a deterministic user-project conformance
matrix over the existing checked examples under `examples/projects/` and
`examples/workspaces/`.
The matrix is stable-readiness evidence for the beta toolchain. It records
which existing user-shaped example projects and workspaces are expected to
pass `glagol check`, `glagol test --list`, and stable `glagol test` execution
through ordinary Glagol entry points.
At release time the matrix covers all 43 top-level fixture roots with
`slovo.toml` under those inventories and 655 discovered tests. Environment
fixtures run with deterministic in-test environment values so host shell state
does not decide the result.
This release changes tooling evidence only. It does not change the Slovo
source language, typed core, runtime behavior, standard-library helper
surface, compiler-known runtime names, package-manager behavior, package graph
semantics, registry behavior, lockfile behavior, semver behavior, ABI/layout
policy, stable schema policy, or performance policy.
## Contract
The conformance matrix is deterministic and repository-local:
- inputs are every top-level `slovo.toml` fixture under the existing
`examples/projects/` and `examples/workspaces/` directories
- entries are sorted by stable repository-relative path
- each entry names the example kind, path, and ordinary project/workspace
test count covered by the matrix
- fixture inventory drift must fail the focused matrix test until the matrix
and release evidence are updated deliberately
- generated evidence must not depend on wall-clock time, host-specific
absolute paths, random ordering, or network access
- matrix output is beta readiness evidence, not a frozen public schema
The matrix is intended to answer whether normal beta users can exercise the
current example projects and workspaces through the documented toolchain. It
does not promote new language forms, new standard-library helpers, new runtime
capabilities, new package manager behavior, or new compatibility guarantees.
## Non-Scope
This scope does not add:
- source-language syntax or semantics
- typed-core changes
- standard-library helpers or stdlib behavior changes
- compiler-known `std.*` runtime names
- runtime behavior or runtime C capabilities
- package manager behavior
- remote registry behavior
- lockfiles
- semantic-version solving
- package publishing
- optional, dev, target, or feature-gated dependencies
- stable package ABI/layout
- stable artifact-manifest, Markdown, JSON, or conformance-matrix schema
guarantees
- performance thresholds, performance claims, or timing publication
- LSP/watch, SARIF, daemon, coverage, retry, tag/group, or event-stream
protocols
## Acceptance Criteria
- README names `1.0.0-beta.25` as the current release and describes the
user-project conformance matrix as tooling/readiness evidence only.
- Language and compiler release notes describe beta25 as a
tooling/conformance evidence slice over existing `examples/projects/` and
`examples/workspaces/`.
- Language and compiler roadmaps record beta25 as the current
stable-readiness evidence slice while keeping language, stdlib, runtime,
package-manager, stable-schema, and performance work deferred.
- The post-beta roadmap records beta25 under tooling/release hardening.
- Glagol is versioned as `1.0.0-beta.25`.
- `compiler/tests/user_project_conformance_beta25.rs` covers all 43 top-level
project/workspace fixture roots and 655 discovered tests.
- Fixture inventory drift is checked against discovered top-level
`slovo.toml` roots.
- `scripts/release-gate.sh` runs the focused beta25 matrix test.
- No compiler, runtime, standard-library, package-manager, registry, lockfile,
semver, ABI/layout, stable-schema, or performance claim is introduced by
the documentation.
## Suggested Gates
```bash
git diff --check
cargo fmt --check
cargo test --test user_project_conformance_beta25
```

View 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.

View File

@ -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.

View File

@ -24,15 +24,20 @@ implementation scope.
networking (released in `1.0.0-beta.2` with read-only text file networking (released in `1.0.0-beta.2` with read-only text file
handles plus narrow filesystem status and mutation calls). handles plus narrow filesystem status and mutation calls).
3. Stabilize `lib/std` module boundaries and document beta-vs-stable APIs. 3. Stabilize `lib/std` module boundaries and document beta-vs-stable APIs.
4. Improve language usability around entry points, `match`, aliases, and 4. Improve language usability around entry points, `match`, concrete aliases,
concrete numeric completeness. and concrete numeric completeness.
5. Expand package/workspace discipline before remote registry work. 5. Expand package/workspace discipline before remote registry work.
6. Add networking only after resource/error policy is coherent. 6. Add networking only after resource/error policy is coherent.
7. Add serialization/data-interchange helpers before richer network libraries 7. Add serialization/data-interchange helpers before richer network libraries
(released in `1.0.0-beta.7` with compact JSON text construction and JSON (released in `1.0.0-beta.7` with compact JSON text construction and JSON
string quoting). string quoting).
8. Design generics and collection unification from real stdlib duplication 8. Promote concrete type aliases before generics so long concrete vector,
pressure. option/result, array, JSON, and resource-handle signatures can be named
9. Add editor-facing diagnostics, watch mode, and generated documentation 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. improvements.
10. Freeze migration/deprecation policy and cut stable only after beta feedback. 11. Freeze migration/deprecation policy and cut stable only after beta feedback.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View File

@ -0,0 +1,64 @@
# Beta25 Release Review
Verdict: PASS.
## Findings
No blocking or non-blocking findings in the re-reviewed scope.
The prior blocking finding is resolved. `compiler/tests/user_project_conformance_beta25.rs` now covers all 43 top-level fixture roots currently discovered under `examples/projects/` and `examples/workspaces/`: 41 project fixtures plus 2 workspace fixtures. The test also asserts the matrix length, total discovered-test count, sorted path order, and exact inventory completeness against discovered top-level `slovo.toml` fixture roots.
## Acceptance Checklist
- PASS: The conformance matrix covers every current top-level `slovo.toml` fixture root under `examples/projects/` and `examples/workspaces/`.
- PASS: `assert_matrix_inventory()` asserts the 43-root count, 655 discovered tests, sorted repository-relative path order, exact discovered fixture inventory, and the full expected path/test/run matrix.
- PASS: The test runs `glagol check`, `glagol test --list`, and stable `glagol test` for every matrix entry.
- PASS: Deterministic env fixture handling is wired through `configure_conformance_env()`: expected env variables are set to stable values and intentionally-missing env variables are removed before each Glagol command.
- PASS: `scripts/release-gate.sh` still runs `cargo test --test user_project_conformance_beta25`.
- PASS: `compiler/Cargo.toml` and `compiler/Cargo.lock` both version `glagol` as `1.0.0-beta.25`.
- PASS: README, release notes, roadmaps, and the beta25 matrix doc continue to frame the release as tooling/conformance evidence without claiming language, runtime, stdlib, package-manager, stable-schema, ABI/layout, or performance changes.
## Verification Commands
Ran:
```bash
git status --short
git diff --stat
sed -n '1,620p' compiler/tests/user_project_conformance_beta25.rs
find examples/projects -mindepth 2 -maxdepth 2 -name slovo.toml -printf '%h\n'
find examples/workspaces -mindepth 2 -maxdepth 2 -name slovo.toml -printf '%h\n'
find examples/projects -mindepth 2 -maxdepth 2 -name slovo.toml -printf '.' | wc -c
find examples/workspaces -mindepth 2 -maxdepth 2 -name slovo.toml -printf '.' | wc -c
rg -n "assert_eq!\(MATRIX\.len\(\), 43|discover_fixture_paths|configure_conformance_env|GLAGOL_STD_IMPORT_ENV_ALPHA_PRESENT|GLAGOL_STD_LAYOUT_LOCAL_ENV_ALPHA_PRESENT" compiler/tests/user_project_conformance_beta25.rs
git diff --check
cargo fmt --check
cargo test --test user_project_conformance_beta25
```
Results:
- `git diff --check`: PASS.
- `cargo fmt --check`: PASS.
- `cargo test --test user_project_conformance_beta25`: PASS, 1 test passed.
- Fixture inventory counts: 41 top-level project roots and 2 top-level workspace roots.
- Matrix inventory assertion: covers 43 roots and 655 discovered tests.
## Residual Risks
No release-blocking residual risks remain for this slice.
## Final Gate Disposition
The controller reran the full release gate after staging the generated
`docs/language/STDLIB_API.md` beta25 catalog update:
```bash
./scripts/release-gate.sh
```
Result: PASS. The gate reported:
```text
release gate passed: docs, stdlib API catalog, fmt, tests, promotion, binary, and LLVM smoke checks completed
```

View 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.

View 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.

439
README.md
View File

@ -6,7 +6,7 @@ This repository is the canonical public monorepo for the language design,
standard library source, compiler, runtime, examples, benchmarks, and technical standard library source, compiler, runtime, examples, benchmarks, and technical
documents. documents.
Current release: `1.0.0-beta.7`. Current release: `1.0.0-beta.25`.
## Repository Layout ## Repository Layout
@ -24,29 +24,215 @@ scripts/ local release and document tooling
## Beta Scope ## Beta Scope
`1.0.0-beta.7` keeps the `1.0.0-beta` language baseline, includes the `1.0.0-beta.25` 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` `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 runtime/resource foundation bundle, the `1.0.0-beta.3` standard-library
stabilization bundle, the `1.0.0-beta.4` language-usability diagnostics 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 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` `1.0.0-beta.6` loopback networking foundation, plus the `1.0.0-beta.7`
serialization/data-interchange foundation. The language baseline supports serialization/data-interchange foundation and the `1.0.0-beta.8` concrete type
practical local command-line, file, and loopback-network programs with: 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, plus the `1.0.0-beta.25` user-project
conformance matrix evidence slice.
The language baseline supports practical local command-line, file, and
loopback-network programs with:
- modules, explicit imports, packages, and local workspaces - 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` - `i32`, `i64`, `u32`, `u64`, `f64`, `bool`, `string`, and internal `unit`
- structs, enums, fixed arrays, concrete vectors, option/result families, and - structs, enums, fixed arrays, concrete vectors, option/result families, and
current `match` current `match`
- module-local transparent concrete type aliases
- explicit `std/*.slo` imports from `lib/std`, installed `share/slovo/std`, or - explicit `std/*.slo` imports from `lib/std`, installed `share/slovo/std`, or
`SLOVO_STD_PATH` `SLOVO_STD_PATH`
- beta-scoped loopback TCP handles through `std.net` - beta-scoped loopback TCP handles through `std.net`
- JSON string quoting and compact JSON text construction through `std.json` - 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` - hosted native builds through LLVM IR, Clang, and `runtime/runtime.c`
Still deferred before stable: generics, maps/sets, broad package registry The generated standard-library API catalog is a beta discovery aid: it lists
semantics, DNS/TLS/async networking, LSP/watch/debug-adapter guarantees, exported helper signatures from `lib/std/*.slo`, normalizes module-local
stable ABI and layout, and a stable standard-library compatibility freeze. 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.
The `1.0.0-beta.25` user-project conformance matrix slice adds deterministic
tooling/readiness evidence over the existing `examples/projects/` and
`examples/workspaces/` inventories. It currently covers all 43 top-level
fixture roots and 655 discovered tests through ordinary `check`,
`test --list`, and stable `test` entry points. The matrix is for ordinary
project and workspace usage evidence only: it does not add source-language
syntax or semantics, standard-library helpers, runtime behavior, package
manager or registry behavior, lockfile behavior, semantic-version solving,
stable schema guarantees, or performance claims.
Still deferred before stable: executable generics, generic aliases, maps/sets,
broad package registry semantics, stable artifact-manifest schema, stable
Markdown schema, stable conformance-matrix 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 ## Build And Test
@ -166,6 +352,31 @@ package/dependency summary, new workspace templates declare
local-package rules. Remote registries, lockfiles, semantic-version solving, local-package rules. Remote registries, lockfiles, semantic-version solving,
package publishing, and stable package ABI/layout remain deferred. 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.25 User Project Conformance Matrix
The `1.0.0-beta.25` release adds deterministic tooling evidence for the
existing user-shaped examples under `examples/projects/` and
`examples/workspaces/`. The conformance matrix is sorted by repository path and
records ordinary `check`, `test --list`, and stable `test` behavior for all
43 top-level fixture roots and 655 discovered tests.
This scope is stable-readiness evidence only. It adds no source-language
change, standard-library helper change, runtime behavior change, package
manager or registry behavior, lockfile behavior, semantic-version solving,
stable schema freeze, or performance claim.
## 1.0.0-beta.6 Networking Foundation ## 1.0.0-beta.6 Networking Foundation
The `1.0.0-beta.6` release adds a narrow blocking loopback TCP foundation: The `1.0.0-beta.6` release adds a narrow blocking loopback TCP foundation:
@ -190,16 +401,220 @@ The `1.0.0-beta.7` release adds a narrow JSON text-construction foundation:
- explicit std/local JSON example projects and a `json-quote-loop` benchmark - explicit std/local JSON example projects and a `json-quote-loop` benchmark
scaffold scaffold
This is not a complete JSON library. Full parsing, recursive JSON values, This is not a complete JSON library. Full parsing beyond primitive scalar
maps/sets, streaming encoders, schema validation, Unicode normalization, and a tokens and the ASCII JSON string-token helper, object/array parsing,
stable data-interchange API freeze remain deferred. 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 ## Documentation
- [Language Manifest](docs/language/MANIFEST.md) - [Language Manifest](docs/language/MANIFEST.md)
- [Language Specification](docs/language/SPEC-v1.md) - [Language Specification](docs/language/SPEC-v1.md)
- [Diagnostics Policy](docs/language/DIAGNOSTICS.md)
- [Local Package And Workspace Guide](docs/language/PACKAGES.md) - [Local Package And Workspace Guide](docs/language/PACKAGES.md)
- [Standard Library API Catalog](docs/language/STDLIB_API.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) - [Compiler Manifest](docs/compiler/GLAGOL_COMPILER_MANIFEST.md)
- [Post-Beta Roadmap](docs/POST_BETA_ROADMAP.md) - [Post-Beta Roadmap](docs/POST_BETA_ROADMAP.md)
- [Slovo Whitepaper](docs/papers/SLOVO_WHITEPAPER.md) - [Slovo Whitepaper](docs/papers/SLOVO_WHITEPAPER.md)

83
benchmarks/README.md Normal file
View 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.

View File

@ -18,6 +18,12 @@ from typing import Callable
TIMING_SCOPE = "local-machine comparison only" TIMING_SCOPE = "local-machine comparison only"
TIMING_MODES = ["cold-process", "hot-loop"] 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) @dataclass(frozen=True)
@ -52,6 +58,14 @@ class Implementation:
def main(root: Path, argv: list[str]) -> int: 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) spec = read_spec(root)
implementations = available_implementations(root, spec) implementations = available_implementations(root, spec)
parser = argparse.ArgumentParser(description=f"Run local {spec.name} timing comparisons.") 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 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: def read_spec(root: Path) -> BenchmarkSpec:
data = json.loads((root / "benchmark.json").read_text(encoding="utf-8")) data = json.loads((root / "benchmark.json").read_text(encoding="utf-8"))
loop_count = int(data["loop_count"]) 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] 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: def emit_list(root: Path, spec: BenchmarkSpec, implementations: list[Implementation], as_json: bool) -> None:
metadata = { metadata = {
"benchmark": spec.name, "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): if value and all(char.isalnum() or char in "/._:-" for char in value):
return value return value
return "'" + value.replace("'", "'\"'\"'") + "'" return "'" + value.replace("'", "'\"'\"'") + "'"
if __name__ == "__main__":
raise SystemExit(main(Path(__file__).resolve().parent, sys.argv[1:]))

2
compiler/Cargo.lock generated
View File

@ -4,4 +4,4 @@ version = 3
[[package]] [[package]]
name = "glagol" name = "glagol"
version = "1.0.0-beta.7" version = "1.0.0-beta.25"

View File

@ -1,6 +1,6 @@
[package] [package]
name = "glagol" name = "glagol"
version = "1.0.0-beta.7" version = "1.0.0-beta.25"
edition = "2021" edition = "2021"
description = "Glagol, the first compiler for the Slovo language" description = "Glagol, the first compiler for the Slovo language"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"

View File

@ -3,6 +3,7 @@ use crate::{token::Span, types::Type};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Program { pub struct Program {
pub module: String, pub module: String,
pub type_aliases: Vec<TypeAliasDecl>,
pub enums: Vec<EnumDecl>, pub enums: Vec<EnumDecl>,
pub structs: Vec<StructDecl>, pub structs: Vec<StructDecl>,
pub c_imports: Vec<CImportDecl>, pub c_imports: Vec<CImportDecl>,
@ -10,6 +11,15 @@ pub struct Program {
pub tests: Vec<Test>, 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)] #[derive(Debug, Clone)]
pub struct EnumDecl { pub struct EnumDecl {
pub name: String, pub name: String,

View File

@ -1,12 +1,14 @@
use std::collections::{hash_map::Entry, HashMap, HashSet}; use std::collections::{hash_map::Entry, BTreeSet, HashMap, HashSet};
use crate::{ use crate::{
ast::{ ast::{
BinaryOp, EnumDecl, Expr, ExprKind, Function, MatchArm, MatchPatternKind, Program, BinaryOp, EnumDecl, Expr, ExprKind, Function, MatchArm, MatchPatternKind, Program,
StructDecl, StructInitField, Test, StructDecl, StructInitField, Test, TypeAliasDecl,
}, },
diag::Diagnostic, diag::Diagnostic,
lower, std_runtime, lower,
reserved::{is_generic_type_parameter_name, unsupported_generic_type_parameter},
std_runtime,
token::Span, token::Span,
types::Type, types::Type,
unsafe_ops, unsafe_ops,
@ -294,13 +296,23 @@ pub fn check_program_with_imports(
fn check_program_inner( fn check_program_inner(
file: &str, file: &str,
program: Program, mut program: Program,
external_functions: &[ExternalFunction], external_functions: &[ExternalFunction],
external_structs: &[ExternalStruct], external_structs: &[ExternalStruct],
external_enums: &[ExternalEnum], external_enums: &[ExternalEnum],
project_resolution: bool, project_resolution: bool,
) -> Result<CheckedProgram, Vec<Diagnostic>> { ) -> Result<CheckedProgram, Vec<Diagnostic>> {
let mut errors = Vec::new(); 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 functions = HashMap::new();
let mut structs = HashMap::new(); let mut structs = HashMap::new();
let mut enums = 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) 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(&param.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(&param.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( fn check_struct_decl(
file: &str, file: &str,
struct_decl: &StructDecl, struct_decl: &StructDecl,
@ -2008,6 +2659,10 @@ fn check_local_type(
} }
fn unknown_named_type(file: &str, span: Span, name: &str, context: &str) -> Diagnostic { 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( Diagnostic::new(
file, file,
"UnknownStructType", "UnknownStructType",
@ -4858,6 +5513,12 @@ fn check_vector_type(file: &str, ty: &Type, span: Span) -> Result<(), Diagnostic
return Ok(()); 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 if **inner != Type::I32
&& **inner != Type::I64 && **inner != Type::I64
&& **inner != Type::F64 && **inner != Type::F64

View File

@ -1,5 +1,8 @@
use crate::token::Span; 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)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Severity { pub enum Severity {
Error, Error,
@ -184,8 +187,8 @@ impl Diagnostic {
) -> String { ) -> String {
let source = source_for(&self.file).unwrap_or(""); let source = source_for(&self.file).unwrap_or("");
let mut parts = vec![ let mut parts = vec![
" (schema slovo.diagnostic)".to_string(), format!(" (schema {})", DIAGNOSTIC_SCHEMA_NAME),
" (version 1)".to_string(), format!(" (version {})", DIAGNOSTIC_SCHEMA_VERSION),
format!(" (severity {})", self.severity.as_str()), format!(" (severity {})", self.severity.as_str()),
format!(" (code {})", self.code), format!(" (code {})", self.code),
format!(" (message {})", render_string(&self.message)), format!(" (message {})", render_string(&self.message)),
@ -310,8 +313,8 @@ fn render_json_diagnostic<'a>(
related: impl Iterator<Item = JsonRelated<'a>>, related: impl Iterator<Item = JsonRelated<'a>>,
) -> String { ) -> String {
let mut fields = vec![ let mut fields = vec![
"\"schema\":\"slovo.diagnostic\"".to_string(), format!("\"schema\":{}", render_json_string(DIAGNOSTIC_SCHEMA_NAME)),
"\"version\":1".to_string(), format!("\"version\":{}", DIAGNOSTIC_SCHEMA_VERSION),
format!("\"severity\":{}", render_json_string(severity)), format!("\"severity\":{}", render_json_string(severity)),
format!("\"code\":{}", render_json_string(code)), format!("\"code\":{}", render_json_string(code)),
format!("\"message\":{}", render_json_string(message)), format!("\"message\":{}", render_json_string(message)),

View File

@ -1,4 +1,8 @@
use std::{fs, path::Path}; use std::{
collections::{BTreeMap, BTreeSet},
fs,
path::Path,
};
use crate::{ use crate::{
ast::Program, ast::Program,
@ -6,6 +10,7 @@ use crate::{
lexer, lower, lexer, lower,
project::{self, ProjectArtifact, SourceFile, ToolFailure, WorkspaceArtifact}, project::{self, ProjectArtifact, SourceFile, ToolFailure, WorkspaceArtifact},
sexpr::{Atom, SExpr, SExprKind}, sexpr::{Atom, SExpr, SExprKind},
types::Type,
}; };
pub fn generate(input: &str, output_dir: &str) -> Result<(), ToolFailure> { pub fn generate(input: &str, output_dir: &str) -> Result<(), ToolFailure> {
@ -56,16 +61,23 @@ fn render_project(
artifact: &ProjectArtifact, artifact: &ProjectArtifact,
sources: &[SourceFile], sources: &[SourceFile],
) -> Result<String, ToolFailure> { ) -> Result<String, ToolFailure> {
let modules = sources
.iter()
.map(document_source)
.collect::<Result<Vec<_>, _>>()?;
let mut out = String::new(); let mut out = String::new();
out.push_str("# Project "); out.push_str("# Project ");
out.push_str(&artifact.project_name); out.push_str(&artifact.project_name);
out.push_str("\n\n"); out.push_str("\n\n");
if let Some(workspace) = &artifact.workspace { if let Some(workspace) = &artifact.workspace {
render_workspace(&mut out, 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 source in sources { for module in &modules {
let module = document_source(source)?; render_module(&mut out, module);
render_module(&mut out, &module);
} }
Ok(out) Ok(out)
} }
@ -120,27 +132,58 @@ fn document_source(source: &SourceFile) -> Result<DocModule, ToolFailure> {
sources: vec![source.clone()], sources: vec![source.clone()],
artifact: None, 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())) Ok(module_from_forms(&source.path, &forms, program.as_ref()))
} }
struct DocModule { struct DocModule {
path: String,
title: String, title: String,
imports: Vec<String>, imports: Vec<String>,
exports: Vec<String>, exports: Vec<String>,
structs: Vec<String>, structs: Vec<String>,
functions: Vec<String>, functions: Vec<String>,
tests: 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 { fn module_from_forms(file: &str, forms: &[SExpr], program: Option<&Program>) -> DocModule {
let mut module = DocModule { let mut module = DocModule {
path: file.to_string(),
title: file.to_string(), title: file.to_string(),
imports: Vec::new(), imports: Vec::new(),
exports: Vec::new(), exports: Vec::new(),
structs: Vec::new(), structs: Vec::new(),
functions: Vec::new(), functions: Vec::new(),
tests: Vec::new(), tests: Vec::new(),
public_api: PublicApi::default(),
}; };
for form in forms { for form in forms {
@ -205,6 +248,7 @@ fn module_from_forms(file: &str, forms: &[SExpr], program: Option<&Program>) ->
}) })
.collect(); .collect();
module.tests = program.tests.iter().map(|test| test.name.clone()).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(); module.imports.sort();
@ -221,11 +265,120 @@ fn render_module(out: &mut String, module: &DocModule) {
out.push_str("\n\n"); out.push_str("\n\n");
render_list(out, "Imports", &module.imports); render_list(out, "Imports", &module.imports);
render_list(out, "Exports", &module.exports); render_list(out, "Exports", &module.exports);
render_public_api_section(out, "###", &module.public_api);
render_list(out, "Structs", &module.structs); render_list(out, "Structs", &module.structs);
render_list(out, "Functions", &module.functions); render_list(out, "Functions", &module.functions);
render_list(out, "Tests", &module.tests); 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]) { fn render_list(out: &mut String, title: &str, values: &[String]) {
out.push_str("### "); out.push_str("### ");
out.push_str(title); out.push_str(title);
@ -242,6 +395,182 @@ fn render_list(out: &mut String, title: &str, values: &[String]) {
out.push('\n'); 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(), &param.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]> { fn list(expr: &SExpr) -> Option<&[SExpr]> {
match &expr.kind { match &expr.kind {
SExprKind::List(items) => Some(items), SExprKind::List(items) => Some(items),

View File

@ -52,6 +52,16 @@ pub fn run_tests(
test_runner::run(file, &checked, filter) 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>> { fn checked_program(file: &str, source: &str) -> Result<check::CheckedProgram, Vec<Diagnostic>> {
let tokens = lexer::lex(file, source)?; let tokens = lexer::lex(file, source)?;
let forms = sexpr::parse(file, &tokens)?; let forms = sexpr::parse(file, &tokens)?;

View File

@ -2,6 +2,11 @@ use std::collections::{HashMap, HashSet};
use crate::{ use crate::{
diag::Diagnostic, 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}, sexpr::{Atom, SExpr, SExprKind},
std_runtime, std_runtime,
token::Span, 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), function_names: collect_function_names(forms),
struct_names: collect_struct_names(forms), struct_names: collect_struct_names(forms),
enum_names: collect_enum_names(forms), enum_names: collect_enum_names(forms),
type_alias_names: collect_type_alias_names(forms),
errors: comment_errors errors: comment_errors
.into_iter() .into_iter()
.map(|comment| unsupported_non_full_line_comment(file, comment.span)) .map(|comment| unsupported_non_full_line_comment(file, comment.span))
@ -42,6 +48,7 @@ struct Formatter<'a> {
function_names: HashSet<String>, function_names: HashSet<String>,
struct_names: HashSet<String>, struct_names: HashSet<String>,
enum_names: HashSet<String>, enum_names: HashSet<String>,
type_alias_names: HashSet<String>,
errors: Vec<Diagnostic>, errors: Vec<Diagnostic>,
} }
@ -64,6 +71,7 @@ impl Formatter<'_> {
Some("module") => self.write_module(form), Some("module") => self.write_module(form),
Some("import") => self.write_import(form), Some("import") => self.write_import(form),
Some("import_c") => self.write_c_import(form), Some("import_c") => self.write_c_import(form),
Some("type") => self.write_type_alias(form),
Some("enum") => self.write_enum(form), Some("enum") => self.write_enum(form),
Some("struct") => self.write_struct(form), Some("struct") => self.write_struct(form),
Some("fn") => self.write_function(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) { fn write_struct(&mut self, form: &SExpr) {
let Some(items) = expect_list(form) else { let Some(items) = expect_list(form) else {
self.errors.push( self.errors.push(
@ -512,17 +581,17 @@ impl Formatter<'_> {
} else if is_ident(&variant_items[1], "string") { } else if is_ident(&variant_items[1], "string") {
"string".to_string() "string".to_string()
} else if let Some(name) = expect_ident(&variant_items[1]) { } 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() name.to_string()
} else { } else {
self.errors.push( self.errors.push(
Diagnostic::new( Diagnostic::new(
self.file, self.file,
"UnsupportedFormatterForm", "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) .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; continue;
} }
@ -531,10 +600,10 @@ impl Formatter<'_> {
Diagnostic::new( Diagnostic::new(
self.file, self.file,
"UnsupportedFormatterForm", "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) .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; continue;
}; };
@ -601,6 +670,12 @@ impl Formatter<'_> {
return; 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 { let Some(name) = expect_ident(&items[1]) else {
self.errors.push( self.errors.push(
Diagnostic::new( Diagnostic::new(
@ -847,8 +922,13 @@ impl Formatter<'_> {
} else if is_ident(&pair[1], "string") { } else if is_ident(&pair[1], "string") {
"string".to_string() "string".to_string()
} else if let Some(name) = expect_ident(&pair[1]) { } 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() name.to_string()
} else if self.push_reserved_type_error(&pair[1]) {
continue;
} else { } else {
self.errors.push( self.errors.push(
Diagnostic::new( Diagnostic::new(
@ -861,14 +941,20 @@ impl Formatter<'_> {
continue; continue;
} }
} else if let Some(items) = expect_list(&pair[1]) { } 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 text
} else if let Some(text) = } else if let Some(text) = render_supported_array_type(
render_supported_array_type(items, &self.struct_names, &self.enum_names) 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 text
} else if let Some(text) = render_supported_vec_type(items) { } else if self.push_reserved_type_error(&pair[1]) {
text continue;
} else { } else {
self.errors.push( self.errors.push(
Diagnostic::new( Diagnostic::new(
@ -946,11 +1032,18 @@ impl Formatter<'_> {
} }
if let Some(name) = expect_ident(form) { 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()); return Some(name.to_string());
} }
} }
if self.push_reserved_type_error(form) {
return None;
}
let Some(items) = expect_list(form) else { let Some(items) = expect_list(form) else {
self.errors.push( self.errors.push(
Diagnostic::new( Diagnostic::new(
@ -963,19 +1056,27 @@ impl Formatter<'_> {
return None; 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); 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); 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); return Some(text);
} }
if self.push_reserved_type_error(form) {
return None;
}
self.errors.push( self.errors.push(
Diagnostic::new( Diagnostic::new(
self.file, self.file,
@ -987,6 +1088,66 @@ impl Formatter<'_> {
None 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> { fn render_c_import_return_type(&mut self, form: &SExpr) -> Option<String> {
if is_ident(form, "i32") { if is_ident(form, "i32") {
return Some("i32".to_string()); return Some("i32".to_string());
@ -1159,6 +1320,14 @@ impl Formatter<'_> {
Some(format!("({})", name)) 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) => { other if std_runtime::is_standard_path(other) => {
self.errors self.errors
.push(std_runtime::unsupported_standard_library_call( .push(std_runtime::unsupported_standard_library_call(
@ -1318,6 +1487,16 @@ impl Formatter<'_> {
is_array: false, 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 { let Some(items) = expect_list(form) else {
@ -1332,20 +1511,24 @@ impl Formatter<'_> {
return None; 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 { return Some(RenderedType {
text, text,
is_array: false, 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 { return Some(RenderedType {
text, text,
is_array: false, is_array: false,
}); });
} }
if self.push_reserved_type_error(form) {
return None;
}
if items.len() != 3 || !is_ident(&items[0], "array") { if items.len() != 3 || !is_ident(&items[0], "array") {
self.errors.push( self.errors.push(
Diagnostic::new( Diagnostic::new(
@ -1362,7 +1545,11 @@ impl Formatter<'_> {
&items[1], &items[1],
&self.struct_names, &self.struct_names,
&self.enum_names, &self.enum_names,
&self.type_alias_names,
) else { ) else {
if self.push_reserved_type_error(&items[1]) {
return None;
}
self.errors.push( self.errors.push(
Diagnostic::new( Diagnostic::new(
self.file, self.file,
@ -1422,11 +1609,18 @@ impl Formatter<'_> {
} }
if let Some(name) = expect_ident(form) { 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()); return Some(name.to_string());
} }
} }
if self.push_reserved_type_error(form) {
return None;
}
let Some(items) = expect_list(form) else { let Some(items) = expect_list(form) else {
self.errors.push( self.errors.push(
Diagnostic::new( Diagnostic::new(
@ -1439,19 +1633,27 @@ impl Formatter<'_> {
return None; 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); 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); 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); return Some(text);
} }
if self.push_reserved_type_error(form) {
return None;
}
self.errors.push( self.errors.push(
Diagnostic::new( Diagnostic::new(
self.file, self.file,
@ -2051,6 +2253,7 @@ impl Formatter<'_> {
&items[1], &items[1],
&self.struct_names, &self.struct_names,
&self.enum_names, &self.enum_names,
&self.type_alias_names,
) else { ) else {
self.errors.push( self.errors.push(
Diagnostic::new( Diagnostic::new(
@ -2113,21 +2316,10 @@ impl Formatter<'_> {
return None; return None;
} }
let payload_type = if is_ident(&items[1], "i32") { let Some(payload_type) = render_payload_type_atom(&items[1], &self.type_alias_names) else {
"i32" if self.push_reserved_type_error(&items[1]) {
} else if is_ident(&items[1], "i64") { return None;
"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 {
self.errors.push( self.errors.push(
Diagnostic::new( Diagnostic::new(
self.file, self.file,
@ -2169,21 +2361,24 @@ impl Formatter<'_> {
return None; return None;
} }
let result_type = if is_ident(&items[1], "i32") && is_ident(&items[2], "i32") { let Some(ok_ty) = render_payload_type_atom(&items[1], &self.type_alias_names) else {
"i32 i32" if self.push_reserved_type_error(&items[1]) {
} else if is_ident(&items[1], "i64") && is_ident(&items[2], "i32") { return None;
"i64 i32" }
} else if is_ident(&items[1], "u32") && is_ident(&items[2], "i32") { self.errors.push(
"u32 i32" Diagnostic::new(
} else if is_ident(&items[1], "u64") && is_ident(&items[2], "i32") { self.file,
"u64 i32" "UnsupportedFormatterForm",
} else if is_ident(&items[1], "f64") && is_ident(&items[2], "i32") { "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",
"f64 i32" )
} else if is_ident(&items[1], "bool") && is_ident(&items[2], "i32") { .with_span(span),
"bool i32" );
} else if is_ident(&items[1], "string") && is_ident(&items[2], "i32") { return None;
"string i32" };
} else { 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( self.errors.push(
Diagnostic::new( Diagnostic::new(
self.file, self.file,
@ -2196,7 +2391,7 @@ impl Formatter<'_> {
}; };
let value = self.render_expr(&items[3], env)?; 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( fn render_call(
@ -2234,6 +2429,15 @@ impl Formatter<'_> {
Some(output) 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) { fn write_indented_rendered(&mut self, indent: &str, rendered: &str) {
push_indented(&mut self.output, indent, rendered); push_indented(&mut self.output, indent, rendered);
} }
@ -2355,6 +2559,19 @@ fn collect_enum_names(forms: &[SExpr]) -> HashSet<String> {
.collect() .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)> { fn qualified_enum_name(name: &str) -> Option<(&str, &str)> {
let (enum_name, variant) = name.split_once('.')?; let (enum_name, variant) = name.split_once('.')?;
if enum_name.is_empty() || variant.is_empty() || variant.contains('.') { if enum_name.is_empty() || variant.is_empty() || variant.contains('.') {
@ -2467,89 +2684,19 @@ fn list_head(form: &SExpr) -> Option<&str> {
expect_ident(first) expect_ident(first)
} }
fn render_option_result_type(items: &[SExpr]) -> Option<String> { fn render_option_result_type(
if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "i32") { items: &[SExpr],
return Some("(option i32)".to_string()); 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") { if items.len() == 3 && is_ident(&items[0], "result") {
return Some("(option i64)".to_string()); 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));
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());
} }
None None
@ -2559,42 +2706,27 @@ fn render_supported_array_constructor_type(
form: &SExpr, form: &SExpr,
struct_names: &HashSet<String>, struct_names: &HashSet<String>,
enum_names: &HashSet<String>, enum_names: &HashSet<String>,
type_alias_names: &HashSet<String>,
) -> Option<String> { ) -> Option<String> {
if is_ident(form, "i32") { render_direct_type_atom(form, struct_names, enum_names, type_alias_names)
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
}
} }
fn render_supported_array_type( fn render_supported_array_type(
items: &[SExpr], items: &[SExpr],
struct_names: &HashSet<String>, struct_names: &HashSet<String>,
enum_names: &HashSet<String>, enum_names: &HashSet<String>,
type_alias_names: &HashSet<String>,
) -> Option<String> { ) -> Option<String> {
if items.len() != 3 || !is_ident(&items[0], "array") { if items.len() != 3 || !is_ident(&items[0], "array") {
return None; 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])?; let len = expect_int(&items[2])?;
if len <= 0 { if len <= 0 {
return None; return None;
@ -2603,7 +2735,10 @@ fn render_supported_array_type(
Some(format!("(array {} {})", elem_ty, len)) 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 items.len() == 2 && is_ident(&items[0], "vec") {
if is_ident(&items[1], "i32") { if is_ident(&items[1], "i32") {
return Some("(vec i32)".to_string()); 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") { if is_ident(&items[1], "string") {
return Some("(vec string)".to_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 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]> { fn expect_list(form: &SExpr) -> Option<&[SExpr]> {
match &form.kind { match &form.kind {
SExprKind::List(items) => Some(items), SExprKind::List(items) => Some(items),

View File

@ -26,6 +26,10 @@ 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 void @print_bool(i1)\n\n");
out.push_str("declare i32 @string_len(ptr)\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 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 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 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 i64 @__glagol_string_parse_u32_result(ptr)\n\n");
@ -33,6 +37,13 @@ pub fn emit(_file: &str, program: &CheckedProgram) -> Result<String, Vec<Diagnos
out.push_str("declare i32 @__glagol_string_parse_f64_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 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_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_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_i64_to_string(i64)\n\n");
out.push_str("declare ptr @__glagol_num_u32_to_string(i32)\n\n"); out.push_str("declare ptr @__glagol_num_u32_to_string(i32)\n\n");
@ -1671,6 +1682,8 @@ impl FunctionGen<'_> {
| "__glagol_fs_read_open_text_result" | "__glagol_fs_read_open_text_result"
| "__glagol_net_tcp_read_all_result" | "__glagol_net_tcp_read_all_result"
| "__glagol_io_read_stdin_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); return self.emit_string_result_host_call(expr, callee, &arg_values);
} }
@ -1691,27 +1704,39 @@ impl FunctionGen<'_> {
|| callee == "__glagol_net_tcp_bound_port_result" || callee == "__glagol_net_tcp_bound_port_result"
|| callee == "__glagol_net_tcp_accept_result" || callee == "__glagol_net_tcp_accept_result"
|| callee == "__glagol_string_parse_i32_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); 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); 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); 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); 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); 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); return self.emit_bool_result_out_param_call(expr, callee, &arg_values);
} }

View File

@ -5,9 +5,14 @@ use crate::{
ast::{ ast::{
BinaryOp, CImportDecl, EnumDecl, EnumVariantDecl, Expr, ExprKind, Function, MatchArm, BinaryOp, CImportDecl, EnumDecl, EnumVariantDecl, Expr, ExprKind, Function, MatchArm,
MatchPattern, MatchPatternKind, Param, Program, StructDecl, StructField, StructInitField, MatchPattern, MatchPatternKind, Param, Program, StructDecl, StructField, StructInitField,
Test, Test, TypeAliasDecl,
}, },
diag::Diagnostic, 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}, sexpr::{Atom, SExpr, SExprKind},
token::Span, token::Span,
types::Type, types::Type,
@ -23,6 +28,8 @@ pub fn lower_program_with_imported_names(
imported_names: &[String], imported_names: &[String],
) -> Result<Program, Vec<Diagnostic>> { ) -> Result<Program, Vec<Diagnostic>> {
let mut module = None; let mut module = None;
let mut type_aliases = Vec::new();
let mut alias_names = HashMap::new();
let mut enums = Vec::new(); let mut enums = Vec::new();
let mut enum_names = imported_names.iter().cloned().collect::<HashSet<_>>(); let mut enum_names = imported_names.iter().cloned().collect::<HashSet<_>>();
let mut structs = Vec::new(); let mut structs = Vec::new();
@ -49,10 +56,67 @@ pub fn lower_program_with_imported_names(
} }
Err(mut errs) => errors.append(&mut errs), 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 { for form in forms {
match list_head(form) { match list_head(form) {
Some("module") => match lower_module(file, form) { Some("module") => match lower_module(file, form) {
@ -61,6 +125,7 @@ pub fn lower_program_with_imported_names(
}, },
Some("enum") => {} Some("enum") => {}
Some("struct") => {} Some("struct") => {}
Some("type") => {}
Some("import_c") => match lower_c_import(file, form) { Some("import_c") => match lower_c_import(file, form) {
Ok(import) => c_imports.push(import), Ok(import) => c_imports.push(import),
Err(mut errs) => errors.append(&mut errs), Err(mut errs) => errors.append(&mut errs),
@ -106,6 +171,7 @@ pub fn lower_program_with_imported_names(
if errors.is_empty() { if errors.is_empty() {
Ok(Program { Ok(Program {
module: module.unwrap_or_else(|| "main".to_string()), module: module.unwrap_or_else(|| "main".to_string()),
type_aliases,
enums, enums,
structs, structs,
c_imports, c_imports,
@ -124,6 +190,14 @@ pub fn print_program(program: &Program) -> String {
output.push_str(&program.module); output.push_str(&program.module);
output.push('\n'); 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 { for enum_decl in &program.enums {
output.push_str(" enum "); output.push_str(" enum ");
output.push_str(&enum_decl.name); 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>> { fn lower_struct(file: &str, form: &SExpr) -> Result<StructDecl, Vec<Diagnostic>> {
let mut errors = Vec::new(); let mut errors = Vec::new();
let Some(items) = expect_list(form) else { 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 { let Some(ty) = lower_type(&pair[1]) else {
errors.push( errors.push(invalid_type_diagnostic(
Diagnostic::new(file, "InvalidStructFieldType", "invalid struct field type") file,
.with_span(pair[1].span), &pair[1],
); "InvalidStructFieldType",
"invalid struct field type",
None,
None,
));
continue; 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 { let Some(payload_ty) = lower_type(&variant_items[1]) else {
errors.push( errors.push(invalid_type_diagnostic(
Diagnostic::new( file,
file, &variant_items[1],
"InvalidEnumVariant", "InvalidEnumVariant",
"enum variant payload type is invalid", "enum variant payload type is invalid",
) Some("i32"),
.with_span(variant_items[1].span) None,
.expected("i32"), ));
);
continue; continue;
}; };
@ -784,6 +932,10 @@ fn lower_function(
.hint("expected `(fn name ((arg Type) ...) -> ReturnType body...)`")]); .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]) { let name = match expect_ident(&items[1]) {
Some(name) => name.to_string(), Some(name) => name.to_string(),
None => { None => {
@ -821,10 +973,14 @@ fn lower_function(
} }
Some(ty) => ty, Some(ty) => ty,
None => { None => {
errors.push( errors.push(invalid_type_diagnostic(
Diagnostic::new(file, "InvalidReturnType", "invalid return type") file,
.with_span(items[4].span), &items[4],
); "InvalidReturnType",
"invalid return type",
None,
None,
));
Type::Unit 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]) { let return_type = match lower_type(&items[4]) {
Some(ty) => ty, Some(ty) => ty,
None => { None => {
errors.push( errors.push(invalid_type_diagnostic(
Diagnostic::new(file, "MalformedCImport", "invalid C import return type") file,
.with_span(items[4].span), &items[4],
); "MalformedCImport",
"invalid C import return type",
None,
None,
));
Type::Unit 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 { let Some(ty) = lower_type(&pair[1]) else {
errors.push( errors.push(invalid_type_diagnostic(
Diagnostic::new(file, "MalformedCImport", "invalid C import parameter type") file,
.with_span(pair[1].span), &pair[1],
); "MalformedCImport",
"invalid C import parameter type",
None,
None,
));
continue; continue;
}; };
params.push(Param { 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 { let Some(ty) = lower_type(&pair[1]) else {
errors.push( errors.push(invalid_type_diagnostic(
Diagnostic::new(file, "InvalidParamType", "invalid parameter type") file,
.with_span(pair[1].span), &pair[1],
); "InvalidParamType",
"invalid parameter type",
None,
None,
));
continue; 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") .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> { fn lower_type(form: &SExpr) -> Option<Type> {
match &form.kind { match &form.kind {
SExprKind::Atom(Atom::Ident(name)) => match name.as_str() { SExprKind::Atom(Atom::Ident(name)) => match name.as_str() {
@ -1556,6 +1746,9 @@ fn lower_expr(
span: form.span, span: form.span,
}) })
} }
name if is_unsupported_generic_standard_library_call(name) => Err(
unsupported_generic_standard_library_call(file, items[0].span, name),
),
name => { name => {
let mut args = Vec::new(); let mut args = Vec::new();
for item in &items[1..] { for item in &items[1..] {
@ -1930,13 +2123,14 @@ fn lower_array_init(
} }
let Some(elem_ty) = lower_type(&items[1]) else { let Some(elem_ty) = lower_type(&items[1]) else {
return Err(Diagnostic::new( return Err(invalid_type_diagnostic(
file, file,
&items[1],
"InvalidArrayElementType", "InvalidArrayElementType",
"array constructor element type is invalid", "array constructor element type is invalid",
) None,
.with_span(items[1].span) Some("fixed arrays use direct scalar `i32`, `i64`, `f64`, `bool`, `string`, known enum, or known non-recursive struct elements"),
.hint("fixed arrays use direct scalar `i32`, `i64`, `f64`, `bool`, `string`, known enum, or known non-recursive struct elements")); ));
}; };
let mut elements = Vec::new(); let mut elements = Vec::new();
@ -1972,13 +2166,14 @@ fn lower_option_some(
} }
let Some(payload_ty) = lower_type(&items[1]) else { let Some(payload_ty) = lower_type(&items[1]) else {
return Err(Diagnostic::new( return Err(invalid_type_diagnostic(
file, file,
&items[1],
"InvalidOptionPayloadType", "InvalidOptionPayloadType",
"option constructor payload type is invalid", "option constructor payload type is invalid",
) None,
.with_span(items[1].span) Some("first-pass options use `i32` payloads"),
.hint("first-pass options use `i32` payloads")); ));
}; };
Ok(Expr { 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 { let Some(payload_ty) = lower_type(&items[1]) else {
return Err(Diagnostic::new( return Err(invalid_type_diagnostic(
file, file,
&items[1],
"InvalidOptionPayloadType", "InvalidOptionPayloadType",
"option constructor payload type is invalid", "option constructor payload type is invalid",
) None,
.with_span(items[1].span) Some("first-pass options use `i32` payloads"),
.hint("first-pass options use `i32` payloads")); ));
}; };
Ok(Expr { Ok(Expr {
@ -2039,22 +2235,24 @@ fn lower_result_ok(
} }
let Some(ok_ty) = lower_type(&items[1]) else { let Some(ok_ty) = lower_type(&items[1]) else {
return Err(Diagnostic::new( return Err(invalid_type_diagnostic(
file, file,
&items[1],
"InvalidResultPayloadType", "InvalidResultPayloadType",
"result constructor ok type is invalid", "result constructor ok type is invalid",
) None,
.with_span(items[1].span) Some("first-pass results use `i32` payloads"),
.hint("first-pass results use `i32` payloads")); ));
}; };
let Some(err_ty) = lower_type(&items[2]) else { let Some(err_ty) = lower_type(&items[2]) else {
return Err(Diagnostic::new( return Err(invalid_type_diagnostic(
file, file,
&items[2],
"InvalidResultPayloadType", "InvalidResultPayloadType",
"result constructor err type is invalid", "result constructor err type is invalid",
) None,
.with_span(items[2].span) Some("first-pass results use `i32` payloads"),
.hint("first-pass results use `i32` payloads")); ));
}; };
Ok(Expr { Ok(Expr {
@ -2087,22 +2285,24 @@ fn lower_result_err(
} }
let Some(ok_ty) = lower_type(&items[1]) else { let Some(ok_ty) = lower_type(&items[1]) else {
return Err(Diagnostic::new( return Err(invalid_type_diagnostic(
file, file,
&items[1],
"InvalidResultPayloadType", "InvalidResultPayloadType",
"result constructor ok type is invalid", "result constructor ok type is invalid",
) None,
.with_span(items[1].span) Some("first-pass results use `i32` payloads"),
.hint("first-pass results use `i32` payloads")); ));
}; };
let Some(err_ty) = lower_type(&items[2]) else { let Some(err_ty) = lower_type(&items[2]) else {
return Err(Diagnostic::new( return Err(invalid_type_diagnostic(
file, file,
&items[2],
"InvalidResultPayloadType", "InvalidResultPayloadType",
"result constructor err type is invalid", "result constructor err type is invalid",
) None,
.with_span(items[2].span) Some("first-pass results use `i32` payloads"),
.hint("first-pass results use `i32` payloads")); ));
}; };
Ok(Expr { Ok(Expr {
@ -2224,10 +2424,14 @@ fn lower_local(
}; };
let Some(ty) = lower_type(&items[2]) else { let Some(ty) = lower_type(&items[2]) else {
return Err( return Err(invalid_type_diagnostic(
Diagnostic::new(file, "InvalidLocalType", "invalid local type") file,
.with_span(items[2].span), &items[2],
); "InvalidLocalType",
"invalid local type",
None,
None,
));
}; };
Ok(Expr { 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> { fn list_head(form: &SExpr) -> Option<&str> {
let items = expect_list(form)?; let items = expect_list(form)?;
let first = items.first()?; let first = items.first()?;

View File

@ -8,9 +8,11 @@ mod lexer;
mod llvm; mod llvm;
mod lower; mod lower;
mod project; mod project;
mod reserved;
mod scaffold; mod scaffold;
mod sexpr; mod sexpr;
mod std_runtime; mod std_runtime;
mod symbols;
mod test_runner; mod test_runner;
mod token; mod token;
mod types; mod types;
@ -22,8 +24,11 @@ use std::{
panic::{self, AssertUnwindSafe}, panic::{self, AssertUnwindSafe},
path::{Path, PathBuf}, path::{Path, PathBuf},
process::{self, Command as ProcessCommand}, process::{self, Command as ProcessCommand},
thread,
}; };
const TEST_RUNNER_THREAD_STACK_SIZE: usize = 16 * 1024 * 1024;
fn main() { fn main() {
let raw_args = env::args().collect::<Vec<_>>(); let raw_args = env::args().collect::<Vec<_>>();
let command_line = raw_args.join(" "); let command_line = raw_args.join(" ");
@ -49,9 +54,29 @@ fn run_invocation_guarded(invocation: Invocation) -> ! {
panic::set_hook(Box::new(|_| {})); panic::set_hook(Box::new(|_| {}));
let run_invocation = invocation.clone(); let run_invocation = invocation.clone();
let result = panic::catch_unwind(AssertUnwindSafe(move || { let result = if invocation.mode == Mode::RunTests {
run_invocation_inner(run_invocation); 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); panic::set_hook(previous_hook);
@ -94,19 +119,26 @@ fn run_invocation_inner(invocation: Invocation) -> ! {
if invocation.mode == Mode::Doc { if invocation.mode == Mode::Doc {
run_doc(invocation); 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 { if invocation.mode == Mode::Format && invocation.fmt_action != FmtAction::Stdout {
run_fmt_action(invocation); run_fmt_action(invocation);
} }
let project_capable_mode = matches!(invocation.mode, Mode::Check | Mode::Build | Mode::Run) let project_capable_mode = matches!(
|| (invocation.mode == Mode::RunTests && invocation.manifest_mode_name == "test"); 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) { if project_capable_mode && project::is_project_input(&invocation.path) {
match invocation.mode { match invocation.mode {
Mode::Check => run_project_check(invocation), Mode::Check => run_project_check(invocation),
Mode::RunTests => run_project_test(invocation), Mode::RunTests => run_project_test(invocation),
Mode::Build => run_project_build(invocation), Mode::Build => run_project_build(invocation),
Mode::Run => run_project_run(invocation), Mode::Run => run_project_run(invocation),
_ => unreachable!("project mode is selected only for check/test/build/run"), Mode::Symbols => run_project_symbols(invocation),
_ => unreachable!("project mode is selected only for check/test/build/run/symbols"),
} }
} }
@ -152,7 +184,13 @@ fn run_project_check(invocation: Invocation) -> ! {
} }
fn run_project_test(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) => { Ok(success) => {
let output = success.output; let output = success.output;
let primary_output = if let Some(output_path) = invocation.output_path.as_deref() { let primary_output = if let Some(output_path) = invocation.output_path.as_deref() {
@ -211,6 +249,25 @@ fn run_project_run(invocation: Invocation) -> ! {
run_native_from_llvm(invocation, output.text, Some(output.artifact), Vec::new()); 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) -> ! { fn exit_project_failure(invocation: Invocation, failure: project::ProjectTestFailure) -> ! {
let rendered = render_source_diagnostics_multi( let rendered = render_source_diagnostics_multi(
&failure.diagnostics, &failure.diagnostics,
@ -233,11 +290,52 @@ fn exit_project_failure(invocation: Invocation, failure: project::ProjectTestFai
process::exit(ExitCode::SourceFailure.code()); 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) -> ! { fn run_text_mode(invocation: Invocation, mode: Mode, source: &str) -> ! {
if mode == Mode::RunTests { if mode == Mode::RunTests {
run_test_mode(invocation, source); 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 foreign_imports = c_imports_for_manifest(&invocation.path, source);
let result = match mode { let result = match mode {
Mode::EmitLlvm => driver::compile_to_llvm(&invocation.path, source), Mode::EmitLlvm => driver::compile_to_llvm(&invocation.path, source),
@ -247,6 +345,7 @@ fn run_text_mode(invocation: Invocation, mode: Mode, source: &str) -> ! {
Mode::InspectLoweringSurface => driver::inspect_lowering_surface(&invocation.path, source), Mode::InspectLoweringSurface => driver::inspect_lowering_surface(&invocation.path, source),
Mode::InspectLoweringChecked => driver::inspect_lowering_checked(&invocation.path, source), Mode::InspectLoweringChecked => driver::inspect_lowering_checked(&invocation.path, source),
Mode::CheckTests => driver::check_tests(&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::RunTests => unreachable!("test mode is handled separately"),
Mode::Build => unreachable!("build is handled separately"), Mode::Build => unreachable!("build is handled separately"),
Mode::Run => unreachable!("run is handled separately"), Mode::Run => unreachable!("run is handled separately"),
@ -315,6 +414,48 @@ 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) -> ! { fn run_new(invocation: Invocation) -> ! {
match scaffold::create_project( match scaffold::create_project(
&invocation.path, &invocation.path,
@ -544,7 +685,13 @@ fn finish_formatted_source(
fn run_test_mode(invocation: Invocation, source: &str) -> ! { fn run_test_mode(invocation: Invocation, source: &str) -> ! {
let foreign_imports = c_imports_for_manifest(&invocation.path, source); 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) => { Ok(result) => {
let output = result.output; let output = result.output;
let primary_output = if let Some(output_path) = invocation.output_path.as_deref() { let primary_output = if let Some(output_path) = invocation.output_path.as_deref() {
@ -745,14 +892,15 @@ fn run_native_from_llvm(
let _ = io::stdout().write_all(&run_output.stdout); let _ = io::stdout().write_all(&run_output.stdout);
let _ = io::stderr().write_all(&run_output.stderr); let _ = io::stderr().write_all(&run_output.stderr);
let stdout = String::from_utf8_lossy(&run_output.stdout).to_string(); let stdout = String::from_utf8_lossy(&run_output.stdout).to_string();
write_manifest_if_requested_with_foreign_imports( 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, &invocation,
run_output.status.success(), run_output.status.success(),
PrimaryOutput::Stdout { PrimaryOutput::Stdout {
kind: Mode::Run.output_kind(), kind: Mode::Run.output_kind(),
text: &stdout, text: &stdout,
}, },
None,
Some(BuildInfo { Some(BuildInfo {
clang: &native.clang, clang: &native.clang,
runtime: &native.runtime, runtime: &native.runtime,
@ -760,8 +908,14 @@ fn run_native_from_llvm(
}), }),
&foreign_imports, &foreign_imports,
project_artifact.as_ref(), project_artifact.as_ref(),
RunReport {
exit_status,
stdout: &stdout,
stderr: &stderr,
args: &invocation.run_args,
},
); );
process::exit(run_output.status.code().unwrap_or(1)); process::exit(exit_status.unwrap_or(1));
} }
struct NativeBuild { struct NativeBuild {
@ -966,6 +1120,7 @@ struct Invocation {
project_template: scaffold::ProjectTemplate, project_template: scaffold::ProjectTemplate,
link_c_paths: Vec<String>, link_c_paths: Vec<String>,
test_filter: Option<String>, test_filter: Option<String>,
test_list: bool,
run_args: Vec<String>, run_args: Vec<String>,
} }
@ -1002,6 +1157,7 @@ fn parse_args(raw_args: &[String]) -> Result<Args, ParseError> {
let mut project_template = scaffold::ProjectTemplate::Binary; let mut project_template = scaffold::ProjectTemplate::Binary;
let mut link_c_paths = Vec::new(); let mut link_c_paths = Vec::new();
let mut test_filter = None; let mut test_filter = None;
let mut test_list = false;
let mut run_args = Vec::new(); let mut run_args = Vec::new();
let mut no_color = false; let mut no_color = false;
let command_line = raw_args.join(" "); let command_line = raw_args.join(" ");
@ -1210,7 +1366,18 @@ fn parse_args(raw_args: &[String]) -> Result<Args, ParseError> {
command_line: command_line.clone(), command_line: command_line.clone(),
})?); })?);
} }
"check" | "fmt" | "test" | "build" | "run" | "clean" | "new" | "doc" "--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() => if path.is_none() =>
{ {
let next = match arg.as_str() { let next = match arg.as_str() {
@ -1222,6 +1389,7 @@ fn parse_args(raw_args: &[String]) -> Result<Args, ParseError> {
"clean" => Mode::Clean, "clean" => Mode::Clean,
"new" => Mode::New, "new" => Mode::New,
"doc" => Mode::Doc, "doc" => Mode::Doc,
"symbols" => Mode::Symbols,
_ => unreachable!(), _ => unreachable!(),
}; };
set_mode( set_mode(
@ -1328,6 +1496,15 @@ 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 { if !run_args.is_empty() && mode != Mode::Run {
return parse_error( return parse_error(
"`--` program arguments are only supported with `run`", "`--` program arguments are only supported with `run`",
@ -1379,6 +1556,7 @@ fn parse_args(raw_args: &[String]) -> Result<Args, ParseError> {
project_template, project_template,
link_c_paths, link_c_paths,
test_filter, test_filter,
test_list,
run_args, run_args,
})) }))
} }
@ -1452,6 +1630,7 @@ fn exit_parse_error(err: ParseError, command_line: &str) -> ! {
project_template: scaffold::ProjectTemplate::Binary, project_template: scaffold::ProjectTemplate::Binary,
link_c_paths: Vec::new(), link_c_paths: Vec::new(),
test_filter: None, test_filter: None,
test_list: false,
run_args: Vec::new(), run_args: Vec::new(),
}; };
write_manifest_or_exit( write_manifest_or_exit(
@ -1464,6 +1643,7 @@ fn exit_parse_error(err: ParseError, command_line: &str) -> ! {
PrimaryOutput::Diagnostics { text: &stderr }, PrimaryOutput::Diagnostics { text: &stderr },
None, None,
None, None,
None,
&[], &[],
None, None,
err.diagnostics, err.diagnostics,
@ -1489,6 +1669,7 @@ enum Mode {
Clean, Clean,
New, New,
Doc, Doc,
Symbols,
} }
impl Mode { impl Mode {
@ -1507,6 +1688,7 @@ impl Mode {
Self::Clean => "clean", Self::Clean => "clean",
Self::New => "new", Self::New => "new",
Self::Doc => "doc", Self::Doc => "doc",
Self::Symbols => "symbols",
} }
} }
@ -1523,6 +1705,7 @@ impl Mode {
Self::Clean => "no-output", Self::Clean => "no-output",
Self::New => "no-output", Self::New => "no-output",
Self::Doc => "documentation", Self::Doc => "documentation",
Self::Symbols => "symbols",
} }
} }
@ -1714,6 +1897,13 @@ struct TestSummary {
filter: Option<String>, 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 { fn test_summary_from_report(report: test_runner::TestReport) -> TestSummary {
TestSummary { TestSummary {
total_discovered: report.total_discovered, total_discovered: report.total_discovered,
@ -1816,6 +2006,34 @@ fn write_manifest_if_requested_with_foreign_imports(
success, success,
primary_output, primary_output,
test_summary, 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, build_info,
foreign_imports, foreign_imports,
project_info, project_info,
@ -1839,6 +2057,7 @@ fn render_manifest(
success: bool, success: bool,
primary_output: PrimaryOutput<'_>, primary_output: PrimaryOutput<'_>,
test_summary: Option<TestSummary>, test_summary: Option<TestSummary>,
run_report: Option<RunReport<'_>>,
build_info: Option<BuildInfo<'_>>, build_info: Option<BuildInfo<'_>>,
foreign_imports: &[project::ProjectArtifactCImport], foreign_imports: &[project::ProjectArtifactCImport],
project_info: Option<&project::ProjectArtifact>, project_info: Option<&project::ProjectArtifact>,
@ -1862,7 +2081,10 @@ fn render_manifest(
" (success {})\n", " (success {})\n",
if success { "true" } else { "false" } 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!( out.push_str(&format!(
" (diagnostics-encoding {})\n", " (diagnostics-encoding {})\n",
match diagnostics { match diagnostics {
@ -1937,6 +2159,34 @@ fn render_manifest(
out.push_str(" )"); 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 { if let Some(build) = build_info {
out.push('\n'); out.push('\n');
out.push_str(" (hosted-build\n"); out.push_str(" (hosted-build\n");
@ -2425,6 +2675,6 @@ fn normalized_output_path(path: &str) -> Option<PathBuf> {
fn print_usage() { fn print_usage() {
eprintln!( eprintln!(
"usage: glagol [check|fmt|test|build|run|clean] [--json-diagnostics] [--no-color] [--manifest <path>] [--link-c <path>] [-o <path>] [--filter <substring>] <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 [--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"
); );
} }

View File

@ -177,6 +177,7 @@ struct ModuleUnit {
local_functions: HashMap<String, DeclInfo>, local_functions: HashMap<String, DeclInfo>,
local_structs: HashMap<String, DeclInfo>, local_structs: HashMap<String, DeclInfo>,
local_enums: HashMap<String, DeclInfo>, local_enums: HashMap<String, DeclInfo>,
local_aliases: HashMap<String, DeclInfo>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -204,6 +205,7 @@ enum DeclKind {
CImport, CImport,
Struct, Struct,
Enum, Enum,
TypeAlias,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -450,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 { struct CheckedProject {
program: CheckedProgram, program: CheckedProgram,
sources: Vec<SourceFile>, sources: Vec<SourceFile>,
@ -1102,7 +1124,16 @@ fn parse_manifest(path: PathBuf, source: String) -> Result<Manifest, Vec<Diagnos
}; };
match key { 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( "source_root" => set_manifest_key(
&file, &file,
&mut errors, &mut errors,
@ -1110,8 +1141,19 @@ fn parse_manifest(path: PathBuf, source: String) -> Result<Manifest, Vec<Diagnos
parsed, parsed,
line.span, line.span,
"source_root", "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( _ => errors.push(
Diagnostic::new( Diagnostic::new(
&file, &file,
@ -1390,6 +1432,7 @@ fn parse_package_manifest(
let mut source_root = None::<String>; let mut source_root = None::<String>;
let mut entry = None::<String>; let mut entry = None::<String>;
let mut dependencies = Vec::new(); let mut dependencies = Vec::new();
let mut dependency_keys = BTreeSet::<String>::new();
for line in manifest_lines(&source) { for line in manifest_lines(&source) {
let trimmed = line.text.trim(); let trimmed = line.text.trim();
@ -1467,6 +1510,8 @@ fn parse_package_manifest(
parsed, parsed,
line.span, line.span,
"name", "name",
"PackageManifestInvalid",
"package",
), ),
"version" => set_manifest_key( "version" => set_manifest_key(
&file, &file,
@ -1475,6 +1520,8 @@ fn parse_package_manifest(
parsed, parsed,
line.span, line.span,
"version", "version",
"PackageManifestInvalid",
"package",
), ),
"source_root" => set_manifest_key( "source_root" => set_manifest_key(
&file, &file,
@ -1483,6 +1530,8 @@ fn parse_package_manifest(
parsed, parsed,
line.span, line.span,
"source_root", "source_root",
"PackageManifestInvalid",
"package",
), ),
"entry" => set_manifest_key( "entry" => set_manifest_key(
&file, &file,
@ -1491,6 +1540,8 @@ fn parse_package_manifest(
parsed, parsed,
line.span, line.span,
"entry", "entry",
"PackageManifestInvalid",
"package",
), ),
other => errors.push( other => errors.push(
Diagnostic::new( Diagnostic::new(
@ -1502,21 +1553,54 @@ fn parse_package_manifest(
), ),
} }
} }
"dependencies" => match parse_dependency_path(value) { "dependencies" => {
Some(path) => dependencies.push(PackageDependency { let valid_key = if is_project_name(key) {
key: key.to_string(), true
path, } else {
span: line.span, errors.push(
}), Diagnostic::new(
None => errors.push( &file,
Diagnostic::new( "InvalidPackageDependencyName",
&file, format!(
"UnsupportedDependency", "package dependency name `{}` must start with `a-z` and contain only `a-z`, `0-9`, and `-`",
"workspace dependencies must use `{ path = \"...\" }` local path records only", key
) ),
.with_span(line.span), )
), .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( "" | "project" => errors.push(
Diagnostic::new( Diagnostic::new(
&file, &file,
@ -1672,13 +1756,15 @@ fn set_manifest_key(
value: String, value: String,
span: Span, span: Span,
key: &str, key: &str,
code: &'static str,
manifest_kind: &str,
) { ) {
if slot.replace(value).is_some() { if slot.replace(value).is_some() {
errors.push( errors.push(
Diagnostic::new( Diagnostic::new(
file, file,
"ProjectManifestInvalid", code,
format!("duplicate project manifest key `{}`", key), format!("duplicate {} manifest key `{}`", manifest_kind, key),
) )
.with_span(span), .with_span(span),
); );
@ -2304,6 +2390,7 @@ fn parse_module(path: &Path, file: String, source: String) -> Result<ModuleUnit,
errors.append(&mut errs); errors.append(&mut errs);
Program { Program {
module: name.clone(), module: name.clone(),
type_aliases: Vec::new(),
enums: Vec::new(), enums: Vec::new(),
structs: Vec::new(), structs: Vec::new(),
c_imports: Vec::new(), c_imports: Vec::new(),
@ -2313,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); local_declarations(&file, &program);
errors.append(&mut duplicate_errors); errors.append(&mut duplicate_errors);
@ -2327,6 +2414,7 @@ fn parse_module(path: &Path, file: String, source: String) -> Result<ModuleUnit,
local_functions, local_functions,
local_structs, local_structs,
local_enums, local_enums,
local_aliases,
}) })
} else { } else {
Err(errors) Err(errors)
@ -2621,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( fn local_declarations(
file: &str, file: &str,
program: &Program, program: &Program,
@ -2628,14 +2754,44 @@ fn local_declarations(
HashMap<String, DeclInfo>, HashMap<String, DeclInfo>,
HashMap<String, DeclInfo>, HashMap<String, DeclInfo>,
HashMap<String, DeclInfo>, HashMap<String, DeclInfo>,
HashMap<String, DeclInfo>,
Vec<Diagnostic>, Vec<Diagnostic>,
) { ) {
let mut all = HashMap::<String, DeclInfo>::new(); let mut all = HashMap::<String, DeclInfo>::new();
let mut functions = HashMap::new(); let mut functions = HashMap::new();
let mut structs = HashMap::new(); let mut structs = HashMap::new();
let mut enums = HashMap::new(); let mut enums = HashMap::new();
let mut aliases = HashMap::new();
let mut errors = Vec::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 { for function in &program.functions {
let decl = DeclInfo { let decl = DeclInfo {
span: function.span, span: function.span,
@ -2767,7 +2923,7 @@ fn local_declarations(
enums.insert(enum_decl.name.clone(), decl); enums.insert(enum_decl.name.clone(), decl);
} }
(functions, structs, enums, errors) (functions, structs, enums, aliases, errors)
} }
fn validate_workspace_packages( fn validate_workspace_packages(
@ -3028,9 +3184,20 @@ fn resolve_and_check_workspace(
let mut external_enums = Vec::new(); let mut external_enums = Vec::new();
for (name, binding) in imports { for (name, binding) in imports {
let provider = &packages[binding.provider_package].modules[binding.provider_module]; 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 { match binding.kind {
DeclKind::Function => { 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) provider.program.functions.iter().find(|f| f.name == *name)
{ {
external_functions.push(ExternalFunction { external_functions.push(ExternalFunction {
@ -3042,7 +3209,16 @@ fn resolve_and_check_workspace(
} }
} }
DeclKind::CImport => { 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) provider.program.c_imports.iter().find(|f| f.name == *name)
{ {
external_functions.push(ExternalFunction { external_functions.push(ExternalFunction {
@ -3054,7 +3230,15 @@ fn resolve_and_check_workspace(
} }
} }
DeclKind::Struct => { 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) provider.program.structs.iter().find(|s| s.name == *name)
{ {
external_structs.push(ExternalStruct { external_structs.push(ExternalStruct {
@ -3069,10 +3253,16 @@ fn resolve_and_check_workspace(
} }
} }
DeclKind::Enum => { 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); external_enums.push(enum_decl);
} }
} }
DeclKind::TypeAlias => {}
} }
} }
@ -3243,9 +3433,19 @@ fn resolve_and_check(
let mut external_enums = Vec::new(); let mut external_enums = Vec::new();
for (name, binding) in imports { for (name, binding) in imports {
let provider = &modules[by_name[&binding.provider]]; let provider = &modules[by_name[&binding.provider]];
let checked_provider = checked_by_module.get(&binding.provider);
match binding.kind { match binding.kind {
DeclKind::Function => { 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) provider.program.functions.iter().find(|f| f.name == *name)
{ {
external_functions.push(ExternalFunction { external_functions.push(ExternalFunction {
@ -3257,7 +3457,16 @@ fn resolve_and_check(
} }
} }
DeclKind::CImport => { 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) provider.program.c_imports.iter().find(|f| f.name == *name)
{ {
external_functions.push(ExternalFunction { external_functions.push(ExternalFunction {
@ -3269,7 +3478,15 @@ fn resolve_and_check(
} }
} }
DeclKind::Struct => { 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) provider.program.structs.iter().find(|s| s.name == *name)
{ {
external_structs.push(ExternalStruct { external_structs.push(ExternalStruct {
@ -3284,10 +3501,16 @@ fn resolve_and_check(
} }
} }
DeclKind::Enum => { 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); external_enums.push(enum_decl);
} }
} }
DeclKind::TypeAlias => {}
} }
} }
@ -3381,6 +3604,22 @@ fn build_export_maps(
Some(kind) => { Some(kind) => {
exports.insert(export.name.clone(), 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( None => diagnostics.push(
Diagnostic::new( Diagnostic::new(
&module.file, &module.file,
@ -3428,6 +3667,7 @@ fn resolve_imports(
.get(&name.name) .get(&name.name)
.or_else(|| module.local_structs.get(&name.name)) .or_else(|| module.local_structs.get(&name.name))
.or_else(|| module.local_enums.get(&name.name)) .or_else(|| module.local_enums.get(&name.name))
.or_else(|| module.local_aliases.get(&name.name))
{ {
diagnostics.push(duplicate_name( diagnostics.push(duplicate_name(
&module.file, &module.file,
@ -3470,7 +3710,8 @@ fn resolve_imports(
.local_functions .local_functions
.get(&name.name) .get(&name.name)
.or_else(|| provider.local_structs.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(); let exported = export_maps[provider_index].get(&name.name).copied();
match (provider_local, exported) { match (provider_local, exported) {
(_, Some(kind)) => { (_, Some(kind)) => {
@ -3483,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( (Some(decl), None) => diagnostics.push(
Diagnostic::new( Diagnostic::new(
&module.file, &module.file,
@ -3557,13 +3815,14 @@ fn resolve_workspace_imports(
&packages[provider_package_index].modules[provider_module_index]; &packages[provider_package_index].modules[provider_module_index];
for name in &import.names { for name in &import.names {
if let Some(local) = module if let Some(local) = module
.local_functions .local_functions
.get(&name.name) .get(&name.name)
.or_else(|| module.local_structs.get(&name.name)) .or_else(|| module.local_structs.get(&name.name))
.or_else(|| module.local_enums.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, diagnostics.push(duplicate_name(
&module.file,
&name.name, &name.name,
name.span, name.span,
local.span, local.span,
@ -3608,7 +3867,8 @@ fn resolve_workspace_imports(
.local_functions .local_functions
.get(&name.name) .get(&name.name)
.or_else(|| provider.local_structs.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] let exported = export_maps[provider_package_index]
[provider_module_index] [provider_module_index]
.get(&name.name) .get(&name.name)
@ -3625,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( (Some(decl), None) => diagnostics.push(
Diagnostic::new( Diagnostic::new(
&module.file, &module.file,

163
compiler/src/reserved.rs Normal file
View 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(" "))
}
}
}

View File

@ -68,6 +68,9 @@ const F64_PARAM: &[RuntimeType] = &[RuntimeType::F64];
const BOOL_PARAM: &[RuntimeType] = &[RuntimeType::Bool]; const BOOL_PARAM: &[RuntimeType] = &[RuntimeType::Bool];
const STRING_PARAM: &[RuntimeType] = &[RuntimeType::String]; const STRING_PARAM: &[RuntimeType] = &[RuntimeType::String];
const STRING_STRING_PARAMS: &[RuntimeType] = &[RuntimeType::String, 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 I32_STRING_PARAMS: &[RuntimeType] = &[RuntimeType::I32, RuntimeType::String];
const VEC_I32_PARAM: &[RuntimeType] = &[RuntimeType::VecI32]; const VEC_I32_PARAM: &[RuntimeType] = &[RuntimeType::VecI32];
const VEC_I32_I32_PARAMS: &[RuntimeType] = &[RuntimeType::VecI32, RuntimeType::I32]; const VEC_I32_I32_PARAMS: &[RuntimeType] = &[RuntimeType::VecI32, RuntimeType::I32];
@ -176,6 +179,34 @@ pub const FUNCTIONS: &[RuntimeFunction] = &[
return_type: RuntimeType::String, return_type: RuntimeType::String,
promoted: true, 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 { RuntimeFunction {
source_name: "std.string.parse_i32_result", source_name: "std.string.parse_i32_result",
runtime_symbol: "__glagol_string_parse_i32_result", runtime_symbol: "__glagol_string_parse_i32_result",
@ -225,6 +256,55 @@ pub const FUNCTIONS: &[RuntimeFunction] = &[
return_type: RuntimeType::String, return_type: RuntimeType::String,
promoted: true, 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 { RuntimeFunction {
source_name: "std.io.eprint", source_name: "std.io.eprint",
runtime_symbol: "__glagol_io_eprint", runtime_symbol: "__glagol_io_eprint",
@ -747,7 +827,18 @@ const RESERVED_HELPER_SYMBOLS: &[&str] = &[
"__glagol_string_parse_u64_result", "__glagol_string_parse_u64_result",
"__glagol_string_parse_f64_result", "__glagol_string_parse_f64_result",
"__glagol_string_parse_bool_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_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_u32_to_string",
"__glagol_num_u64_to_string", "__glagol_num_u64_to_string",
"__glagol_num_f64_to_string", "__glagol_num_f64_to_string",
@ -789,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), format!("standard library call `{}` is not supported", source_name),
) )
.with_span(span) .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.json.quote_string, 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") .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) .found(source_name)
.hint("use a promoted standard-runtime name or a legacy intrinsic alias") .hint("use a promoted standard-runtime name or a legacy intrinsic alias")
} }

667
compiler/src/symbols.rs Normal file
View 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(&param.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,
}

View File

@ -173,6 +173,39 @@ pub fn run(
} }
} }
pub fn list(program: &CheckedProgram, filter: Option<&str>) -> TestRunSuccess {
let mut output = String::new();
let mut report = TestReport {
total_discovered: program.tests.len(),
selected: 0,
passed: 0,
failed: 0,
skipped: 0,
filter: filter.map(str::to_string),
};
for test in &program.tests {
output.push_str("test ");
write_test_name(&test.name, &mut output);
if let Some(filter) = filter {
if !test.name.contains(filter) {
report.skipped += 1;
output.push_str(" ... skipped\n");
continue;
}
}
report.selected += 1;
output.push_str(" ... selected\n");
}
output.push_str(&format!("{} test(s) selected", report.selected));
write_report_suffix(&report, &mut output);
output.push('\n');
TestRunSuccess { output, report }
}
fn write_report_suffix(report: &TestReport, output: &mut String) { fn write_report_suffix(report: &TestReport, output: &mut String) {
output.push_str(&format!( output.push_str(&format!(
" (total_discovered {}, selected {}, passed {}, failed {}, skipped {}", " (total_discovered {}, selected {}, passed {}, failed {}, skipped {}",
@ -181,6 +214,8 @@ fn write_report_suffix(report: &TestReport, output: &mut String) {
if let Some(filter) = report.filter.as_deref() { if let Some(filter) = report.filter.as_deref() {
output.push_str(", filter "); output.push_str(", filter ");
write_test_name(filter, output); write_test_name(filter, output);
} else {
output.push_str(", filter none");
} }
output.push(')'); output.push(')');
} }
@ -686,6 +721,208 @@ fn parse_bool_result_value(value: &str) -> Value {
} }
} }
fn parse_json_i32_result_value(value: &str) -> Value {
if json_integer_token(value.as_bytes(), true) {
parse_i32_result_value(value)
} else {
Value::ResultI32 {
is_ok: false,
payload: 1,
}
}
}
fn parse_json_i64_result_value(value: &str) -> Value {
if json_integer_token(value.as_bytes(), true) {
parse_i64_result_value(value)
} else {
Value::ResultI64I32 {
is_ok: false,
ok_payload: 0,
err_payload: 1,
}
}
}
fn parse_json_u32_result_value(value: &str) -> Value {
if json_integer_token(value.as_bytes(), false) {
parse_u32_result_value(value)
} else {
Value::ResultU32I32 {
is_ok: false,
payload: 1,
}
}
}
fn parse_json_u64_result_value(value: &str) -> Value {
if json_integer_token(value.as_bytes(), false) {
parse_u64_result_value(value)
} else {
Value::ResultU64I32 {
is_ok: false,
ok_payload: 0,
err_payload: 1,
}
}
}
fn parse_json_f64_result_value(value: &str) -> Value {
match parse_json_f64(value) {
Some(payload) => Value::ResultF64I32 {
is_ok: true,
ok_payload: payload,
err_payload: 0,
},
None => Value::ResultF64I32 {
is_ok: false,
ok_payload: 0.0,
err_payload: 1,
},
}
}
fn parse_json_string_result_value(value: &str) -> Value {
match decode_json_ascii_string_token(value.as_bytes()) {
Some(payload) => Value::ResultStringI32 {
is_ok: true,
ok_payload: payload,
err_payload: 0,
},
None => Value::ResultStringI32 {
is_ok: false,
ok_payload: String::new(),
err_payload: 1,
},
}
}
fn decode_json_ascii_string_token(bytes: &[u8]) -> Option<String> {
if bytes.len() < 2 || bytes.first() != Some(&b'"') || bytes.last() != Some(&b'"') {
return None;
}
let mut decoded = String::with_capacity(bytes.len().saturating_sub(2));
let mut index = 1;
let end = bytes.len() - 1;
while index < end {
let byte = bytes[index];
index += 1;
if byte < 0x20 || byte >= 0x80 || byte == b'"' {
return None;
}
if byte != b'\\' {
decoded.push(byte as char);
continue;
}
if index >= end {
return None;
}
let escaped = bytes[index];
index += 1;
match escaped {
b'"' => decoded.push('"'),
b'\\' => decoded.push('\\'),
b'/' => decoded.push('/'),
b'b' => decoded.push('\u{0008}'),
b'f' => decoded.push('\u{000c}'),
b'n' => decoded.push('\n'),
b'r' => decoded.push('\r'),
b't' => decoded.push('\t'),
_ => return None,
}
}
Some(decoded)
}
fn json_integer_token(bytes: &[u8], allow_negative: bool) -> bool {
if bytes.is_empty() {
return false;
}
let mut index = 0;
if bytes[index] == b'-' {
if !allow_negative {
return false;
}
index += 1;
if index == bytes.len() {
return false;
}
}
if !bytes[index].is_ascii_digit() {
return false;
}
if bytes[index] == b'0' && index + 1 != bytes.len() {
return false;
}
index += 1;
while index < bytes.len() {
if !bytes[index].is_ascii_digit() {
return false;
}
index += 1;
}
true
}
fn parse_json_f64(text: &str) -> Option<f64> {
if !json_number_token(text.as_bytes()) {
return None;
}
text.parse::<f64>().ok().filter(|value| value.is_finite())
}
fn json_number_token(bytes: &[u8]) -> bool {
if bytes.is_empty() {
return false;
}
let mut index = 0;
if bytes[index] == b'-' {
index += 1;
if index == bytes.len() {
return false;
}
}
if bytes[index] == b'0' {
index += 1;
} else if bytes[index].is_ascii_digit() && bytes[index] != b'0' {
consume_ascii_digits(bytes, &mut index);
} else {
return false;
}
if index < bytes.len() && bytes[index] == b'.' {
index += 1;
if consume_ascii_digits(bytes, &mut index) == 0 {
return false;
}
}
if index < bytes.len() && (bytes[index] == b'e' || bytes[index] == b'E') {
index += 1;
if index < bytes.len() && (bytes[index] == b'+' || bytes[index] == b'-') {
index += 1;
}
if consume_ascii_digits(bytes, &mut index) == 0 {
return false;
}
}
index == bytes.len()
}
fn parse_f64_strict_ascii(text: &str) -> Option<f64> { fn parse_f64_strict_ascii(text: &str) -> Option<f64> {
let bytes = text.as_bytes(); let bytes = text.as_bytes();
if !is_ascii_decimal_f64(bytes) { if !is_ascii_decimal_f64(bytes) {
@ -759,6 +996,232 @@ fn quote_json_string_value(value: &str) -> String {
quoted quoted
} }
fn eval_string_byte_at_result_call(
file: &str,
expr: &TExpr,
args: &[TExpr],
locals: &mut HashMap<String, Value>,
functions: &HashMap<&str, &CheckedFunction>,
foreign_imports: &HashSet<&str>,
depth: usize,
) -> Result<Value, Diagnostic> {
let Some(value) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.string.byte_at_result` calls",
));
};
let Some(index) = args.get(1) else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.string.byte_at_result` calls",
));
};
let value = eval_expr(file, value, locals, functions, foreign_imports, depth)?;
let index = eval_expr(file, index, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.string.byte_at_result` on non-string values",
));
};
let Some(index) = index.as_i32() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.string.byte_at_result` with non-i32 index",
));
};
let payload = usize::try_from(index)
.ok()
.and_then(|index| value.as_bytes().get(index))
.map(|byte| i32::from(*byte));
Ok(match payload {
Some(payload) => Value::ResultI32 {
is_ok: true,
payload,
},
None => Value::ResultI32 {
is_ok: false,
payload: 1,
},
})
}
fn eval_string_slice_result_call(
file: &str,
expr: &TExpr,
args: &[TExpr],
locals: &mut HashMap<String, Value>,
functions: &HashMap<&str, &CheckedFunction>,
foreign_imports: &HashSet<&str>,
depth: usize,
) -> Result<Value, Diagnostic> {
let Some(value) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.string.slice_result` calls",
));
};
let Some(start) = args.get(1) else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.string.slice_result` calls",
));
};
let Some(count) = args.get(2) else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.string.slice_result` calls",
));
};
let value = eval_expr(file, value, locals, functions, foreign_imports, depth)?;
let start = eval_expr(file, start, locals, functions, foreign_imports, depth)?;
let count = eval_expr(file, count, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.string.slice_result` on non-string values",
));
};
let Some(start) = start.as_i32() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.string.slice_result` with non-i32 start",
));
};
let Some(count) = count.as_i32() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.string.slice_result` with non-i32 count",
));
};
let bytes = value.as_bytes();
let slice = usize::try_from(start)
.ok()
.and_then(|start| {
usize::try_from(count)
.ok()
.and_then(|count| start.checked_add(count).map(|end| (start, end)))
})
.and_then(|(start, end)| {
(start <= bytes.len() && end <= bytes.len()).then(|| &bytes[start..end])
});
let Some(slice) = slice else {
return Ok(Value::ResultStringI32 {
is_ok: false,
ok_payload: String::new(),
err_payload: 1,
});
};
let ok_payload = String::from_utf8(slice.to_vec()).map_err(|_| {
Diagnostic::new(
file,
"TestRuntimeError",
"`std.string.slice_result` produced non-UTF-8 bytes in the test runner",
)
.with_span(expr.span)
})?;
Ok(Value::ResultStringI32 {
is_ok: true,
ok_payload,
err_payload: 0,
})
}
fn eval_string_starts_with_call(
file: &str,
expr: &TExpr,
args: &[TExpr],
locals: &mut HashMap<String, Value>,
functions: &HashMap<&str, &CheckedFunction>,
foreign_imports: &HashSet<&str>,
depth: usize,
) -> Result<Value, Diagnostic> {
let Some(value) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.string.starts_with` calls",
));
};
let Some(prefix) = args.get(1) else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.string.starts_with` calls",
));
};
let value = eval_expr(file, value, locals, functions, foreign_imports, depth)?;
let prefix = eval_expr(file, prefix, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.string.starts_with` on non-string values",
));
};
let Some(prefix) = prefix.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.string.starts_with` with non-string prefix",
));
};
Ok(Value::Bool(value.as_bytes().starts_with(prefix.as_bytes())))
}
fn eval_string_ends_with_call(
file: &str,
expr: &TExpr,
args: &[TExpr],
locals: &mut HashMap<String, Value>,
functions: &HashMap<&str, &CheckedFunction>,
foreign_imports: &HashSet<&str>,
depth: usize,
) -> Result<Value, Diagnostic> {
let Some(value) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.string.ends_with` calls",
));
};
let Some(suffix) = args.get(1) else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.string.ends_with` calls",
));
};
let value = eval_expr(file, value, locals, functions, foreign_imports, depth)?;
let suffix = eval_expr(file, suffix, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.string.ends_with` on non-string values",
));
};
let Some(suffix) = suffix.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.string.ends_with` with non-string suffix",
));
};
Ok(Value::Bool(value.as_bytes().ends_with(suffix.as_bytes())))
}
fn eval_expr( fn eval_expr(
file: &str, file: &str,
expr: &TExpr, expr: &TExpr,
@ -2128,6 +2591,50 @@ fn eval_expr(
}; };
return Ok(Value::String(format!("{}{}", left, right))); return Ok(Value::String(format!("{}{}", left, right)));
} }
if runtime_symbol == "__glagol_string_byte_at_result" {
return eval_string_byte_at_result_call(
file,
expr,
args,
locals,
functions,
foreign_imports,
depth,
);
}
if runtime_symbol == "__glagol_string_slice_result" {
return eval_string_slice_result_call(
file,
expr,
args,
locals,
functions,
foreign_imports,
depth,
);
}
if runtime_symbol == "__glagol_string_starts_with" {
return eval_string_starts_with_call(
file,
expr,
args,
locals,
functions,
foreign_imports,
depth,
);
}
if runtime_symbol == "__glagol_string_ends_with" {
return eval_string_ends_with_call(
file,
expr,
args,
locals,
functions,
foreign_imports,
depth,
);
}
if runtime_symbol == "__glagol_json_quote_string" { if runtime_symbol == "__glagol_json_quote_string" {
let Some(arg) = args.first() else { let Some(arg) = args.first() else {
return Err(unsupported_test_expr( return Err(unsupported_test_expr(
@ -2254,6 +2761,132 @@ fn eval_expr(
}; };
return Ok(parse_bool_result_value(value)); return Ok(parse_bool_result_value(value));
} }
if runtime_symbol == "__glagol_json_parse_bool_value_result" {
let Some(arg) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.json.parse_bool_value_result` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.json.parse_bool_value_result` on non-string values",
));
};
return Ok(parse_bool_result_value(value));
}
if runtime_symbol == "__glagol_json_parse_string_value_result" {
let Some(arg) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.json.parse_string_value_result` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.json.parse_string_value_result` on non-string values",
));
};
return Ok(parse_json_string_result_value(value));
}
if runtime_symbol == "__glagol_json_parse_i32_value_result" {
let Some(arg) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.json.parse_i32_value_result` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.json.parse_i32_value_result` on non-string values",
));
};
return Ok(parse_json_i32_result_value(value));
}
if runtime_symbol == "__glagol_json_parse_u32_value_result" {
let Some(arg) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.json.parse_u32_value_result` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.json.parse_u32_value_result` on non-string values",
));
};
return Ok(parse_json_u32_result_value(value));
}
if runtime_symbol == "__glagol_json_parse_i64_value_result" {
let Some(arg) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.json.parse_i64_value_result` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.json.parse_i64_value_result` on non-string values",
));
};
return Ok(parse_json_i64_result_value(value));
}
if runtime_symbol == "__glagol_json_parse_u64_value_result" {
let Some(arg) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.json.parse_u64_value_result` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.json.parse_u64_value_result` on non-string values",
));
};
return Ok(parse_json_u64_result_value(value));
}
if runtime_symbol == "__glagol_json_parse_f64_value_result" {
let Some(arg) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.json.parse_f64_value_result` calls",
));
};
let value = eval_expr(file, arg, locals, functions, foreign_imports, depth)?;
let Some(value) = value.as_string() else {
return Err(unsupported_test_expr(
file,
expr,
"`std.json.parse_f64_value_result` on non-string values",
));
};
return Ok(parse_json_f64_result_value(value));
}
if runtime_symbol == "__glagol_process_argc" { if runtime_symbol == "__glagol_process_argc" {
let argc = i32::try_from(env::args().count()).map_err(|_| { let argc = i32::try_from(env::args().count()).map_err(|_| {
Diagnostic::new( Diagnostic::new(

View 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
);
}

View 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);
}

View File

@ -83,6 +83,148 @@ const CASES: &[DiagnosticCase] = &[
"#, "#,
snapshot: "../tests/unknown-top-level-form.diag", 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 { DiagnosticCase {
name: "enum-empty", name: "enum-empty",
source: r#" source: r#"
@ -894,6 +1036,250 @@ const CASES: &[DiagnosticCase] = &[
"#, "#,
snapshot: "../tests/std-string-concat-unsupported-string-container.diag", 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 { DiagnosticCase {
name: "std-string-parse-i32-result-arity", name: "std-string-parse-i32-result-arity",
source: r#" source: r#"
@ -1388,6 +1774,16 @@ const CASES: &[DiagnosticCase] = &[
"#, "#,
snapshot: "../tests/std-string-index-unsupported.diag", 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 { DiagnosticCase {
name: "std-string-slice-unsupported", name: "std-string-slice-unsupported",
source: r#" source: r#"
@ -1399,6 +1795,39 @@ const CASES: &[DiagnosticCase] = &[
"#, "#,
snapshot: "../tests/std-string-slice-unsupported.diag", 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 { DiagnosticCase {
name: "std-string-tokenize-unsupported", name: "std-string-tokenize-unsupported",
source: r#" source: r#"
@ -2051,11 +2480,22 @@ const CASES: &[DiagnosticCase] = &[
(module main) (module main)
(fn main () -> i32 (fn main () -> i32
(std.result.map (ok string i32 "a")) (std.result.map (ok string i32 "a") mapper)
0) 0)
"#, "#,
snapshot: "../tests/std-result-map-unsupported.diag", 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 { DiagnosticCase {
name: "std-package-load-unsupported", name: "std-package-load-unsupported",
source: r#" source: r#"

View 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);
}

View 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)
);
}

View File

@ -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] #[test]
fn formatter_preserves_full_line_comments_inside_function_bodies() { fn formatter_preserves_full_line_comments_inside_function_bodies() {
let compiler = env!("CARGO_BIN_EXE_glagol"); let compiler = env!("CARGO_BIN_EXE_glagol");

View File

@ -13,6 +13,12 @@ const LOWERING_FIXTURES: &[LoweringFixture] = &[
surface_snapshot: "../tests/top-level-test.surface.lower", surface_snapshot: "../tests/top-level-test.surface.lower",
checked_snapshot: "../tests/top-level-test.checked.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 { LoweringFixture {
name: "comments", name: "comments",
source: "../tests/comments.slo", source: "../tests/comments.slo",

View 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)
)
}

View File

@ -1279,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] #[test]
fn project_diagnostic_families_have_json_golden_coverage() { fn project_diagnostic_families_have_json_golden_coverage() {
let duplicate = write_project( let duplicate = write_project(

View File

@ -13,11 +13,13 @@ const DIAGNOSTIC_SNAPSHOTS: &[&str] = &[
"array-return-type-mismatch.diag", "array-return-type-mismatch.diag",
"arity-mismatch.diag", "arity-mismatch.diag",
"cyclic-struct-fields.diag", "cyclic-struct-fields.diag",
"cyclic-type-alias.diag",
"duplicate-local.diag", "duplicate-local.diag",
"duplicate-match-arm.diag", "duplicate-match-arm.diag",
"duplicate-struct-constructor-field.diag", "duplicate-struct-constructor-field.diag",
"duplicate-struct-field.diag", "duplicate-struct-field.diag",
"duplicate-struct.diag", "duplicate-struct.diag",
"duplicate-type-alias.diag",
"empty-array.diag", "empty-array.diag",
"enum-constructor-arity.diag", "enum-constructor-arity.diag",
"enum-container-values.diag", "enum-container-values.diag",
@ -65,6 +67,7 @@ const DIAGNOSTIC_SNAPSHOTS: &[&str] = &[
"malformed-result-err-unwrap.diag", "malformed-result-err-unwrap.diag",
"malformed-result-ok-unwrap.diag", "malformed-result-ok-unwrap.diag",
"malformed-result-constructor.diag", "malformed-result-constructor.diag",
"malformed-type-alias.diag",
"malformed-unsafe-form.diag", "malformed-unsafe-form.diag",
"malformed-while.diag", "malformed-while.diag",
"field-access-on-non-struct.diag", "field-access-on-non-struct.diag",
@ -82,6 +85,7 @@ const DIAGNOSTIC_SNAPSHOTS: &[&str] = &[
"set-immutable-local.diag", "set-immutable-local.diag",
"set-parameter.diag", "set-parameter.diag",
"single-file-main-i64-return.diag", "single-file-main-i64-return.diag",
"self-type-alias.diag",
"set-type-mismatch.diag", "set-type-mismatch.diag",
"set-unknown-local.diag", "set-unknown-local.diag",
"std-abi-layout-unsupported.diag", "std-abi-layout-unsupported.diag",
@ -162,11 +166,26 @@ const DIAGNOSTIC_SNAPSHOTS: &[&str] = &[
"std-random-string-unsupported.diag", "std-random-string-unsupported.diag",
"std-random-uuid-unsupported.diag", "std-random-uuid-unsupported.diag",
"std-result-map-unsupported.diag", "std-result-map-unsupported.diag",
"std-vec-empty-generic-unsupported.diag",
"std-string-byte-at-result-arity.diag",
"std-string-byte-at-result-bool-context.diag",
"std-string-byte-at-result-context.diag",
"std-string-byte-at-result-helper-shadow.diag",
"std-string-byte-at-result-name-shadow.diag",
"std-string-byte-at-result-type.diag",
"std-string-byte-at-unsupported.diag",
"std-string-concat-arity.diag", "std-string-concat-arity.diag",
"std-string-concat-helper-shadow.diag", "std-string-concat-helper-shadow.diag",
"std-string-concat-result-context.diag", "std-string-concat-result-context.diag",
"std-string-concat-type.diag", "std-string-concat-type.diag",
"std-string-concat-unsupported-string-container.diag", "std-string-concat-unsupported-string-container.diag",
"std-string-contains-unsupported.diag",
"std-string-ends-with-arity.diag",
"std-string-ends-with-context.diag",
"std-string-ends-with-helper-shadow.diag",
"std-string-ends-with-name-shadow.diag",
"std-string-ends-with-type.diag",
"std-string-find-result-unsupported.diag",
"std-string-from-i64-unsupported.diag", "std-string-from-i64-unsupported.diag",
"std-string-index-unsupported.diag", "std-string-index-unsupported.diag",
"std-string-len-type.diag", "std-string-len-type.diag",
@ -215,7 +234,19 @@ const DIAGNOSTIC_SNAPSHOTS: &[&str] = &[
"std-string-parse-i32-whitespace-unsupported.diag", "std-string-parse-i32-whitespace-unsupported.diag",
"std-string-parse-string-unsupported.diag", "std-string-parse-string-unsupported.diag",
"std-string-scan-unsupported.diag", "std-string-scan-unsupported.diag",
"std-string-slice-result-arity.diag",
"std-string-slice-result-bool-context.diag",
"std-string-slice-result-context.diag",
"std-string-slice-result-helper-shadow.diag",
"std-string-slice-result-name-shadow.diag",
"std-string-slice-result-type.diag",
"std-string-slice-unsupported.diag", "std-string-slice-unsupported.diag",
"std-string-split-unsupported.diag",
"std-string-starts-with-arity.diag",
"std-string-starts-with-context.diag",
"std-string-starts-with-helper-shadow.diag",
"std-string-starts-with-name-shadow.diag",
"std-string-starts-with-type.diag",
"std-string-tokenize-unsupported.diag", "std-string-tokenize-unsupported.diag",
"std-terminal-clear-unsupported.diag", "std-terminal-clear-unsupported.diag",
"std-terminal-echo-unsupported.diag", "std-terminal-echo-unsupported.diag",
@ -280,7 +311,11 @@ const DIAGNOSTIC_SNAPSHOTS: &[&str] = &[
"test-invalid-name.diag", "test-invalid-name.diag",
"test-non-bool.diag", "test-non-bool.diag",
"type-mismatch.diag", "type-mismatch.diag",
"type-alias-name-conflict.diag",
"type-alias-value-name-conflict.diag",
"unclosed-list.diag", "unclosed-list.diag",
"unknown-type-alias-target.diag",
"unsupported-type-alias-target.diag",
"unknown-function.diag", "unknown-function.diag",
"unknown-struct-local.diag", "unknown-struct-local.diag",
"unknown-struct-parameter.diag", "unknown-struct-parameter.diag",
@ -316,8 +351,13 @@ const DIAGNOSTIC_SNAPSHOTS: &[&str] = &[
"unsupported-array-print.diag", "unsupported-array-print.diag",
"unsupported-array-signature-element-type.diag", "unsupported-array-signature-element-type.diag",
"unsupported-float-literal.diag", "unsupported-float-literal.diag",
"unsupported-generic-function.diag",
"unsupported-generic-type-alias.diag",
"unsupported-generic-type-parameter.diag",
"unsupported-generic-vec-type.diag", "unsupported-generic-vec-type.diag",
"unsupported-map-type.diag",
"unsupported-signature-type.diag", "unsupported-signature-type.diag",
"unsupported-set-type.diag",
"unsupported-result-parameter-payload-type.diag", "unsupported-result-parameter-payload-type.diag",
"unsupported-result-return-payload-type.diag", "unsupported-result-return-payload-type.diag",
"unsupported-unit-parameter-signature.diag", "unsupported-unit-parameter-signature.diag",
@ -465,6 +505,8 @@ const LOWERING_INSPECTOR_FIXTURES: &[&str] = &[
"time-sleep.surface.lower", "time-sleep.surface.lower",
"top-level-test.checked.lower", "top-level-test.checked.lower",
"top-level-test.surface.lower", "top-level-test.surface.lower",
"type-aliases.checked.lower",
"type-aliases.surface.lower",
"u32-numeric-primitive.checked.lower", "u32-numeric-primitive.checked.lower",
"u32-numeric-primitive.surface.lower", "u32-numeric-primitive.surface.lower",
"u64-numeric-primitive.checked.lower", "u64-numeric-primitive.checked.lower",
@ -490,6 +532,11 @@ const LOWERING_INSPECTOR_CASES: &[LoweringInspectorCase] = &[
surface_snapshot: "top-level-test.surface.lower", surface_snapshot: "top-level-test.surface.lower",
checked_snapshot: "top-level-test.checked.lower", checked_snapshot: "top-level-test.checked.lower",
}, },
LoweringInspectorCase {
source: "tests/type-aliases.slo",
surface_snapshot: "type-aliases.surface.lower",
checked_snapshot: "type-aliases.checked.lower",
},
LoweringInspectorCase { LoweringInspectorCase {
source: "tests/comments.slo", source: "tests/comments.slo",
surface_snapshot: "comments.surface.lower", surface_snapshot: "comments.surface.lower",
@ -853,6 +900,7 @@ const GLAGOL_TEST_FIXTURES: &[&str] = &[
"time-sleep.slo", "time-sleep.slo",
"top-level-test.fmt", "top-level-test.fmt",
"top-level-test.slo", "top-level-test.slo",
"type-aliases.slo",
"u32-numeric-primitive.slo", "u32-numeric-primitive.slo",
"u64-numeric-primitive.slo", "u64-numeric-primitive.slo",
"unsafe.slo", "unsafe.slo",
@ -920,6 +968,7 @@ const SLOVO_FORMATTER_FIXTURES: &[&str] = &[
"struct.slo", "struct.slo",
"time-sleep.slo", "time-sleep.slo",
"top-level-test.slo", "top-level-test.slo",
"type-aliases.slo",
"u32-numeric-primitive.slo", "u32-numeric-primitive.slo",
"u64-numeric-primitive.slo", "u64-numeric-primitive.slo",
"unsafe.slo", "unsafe.slo",
@ -1172,6 +1221,22 @@ const STANDARD_JSON_SOURCE_FACADE_ALPHA: &[&str] = &[
"i64_value", "i64_value",
"u64_value", "u64_value",
"f64_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_string",
"field_bool", "field_bool",
"field_i32", "field_i32",
@ -1198,6 +1263,42 @@ const STANDARD_JSON_RUNTIME_NAMES: &[&str] = &[
"std.num.i64_to_string", "std.num.i64_to_string",
"std.num.u64_to_string", "std.num.u64_to_string",
"std.num.f64_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",
]; ];
const STANDARD_PROCESS_SOURCE_FACADE_ALPHA: &[&str] = &[ const STANDARD_PROCESS_SOURCE_FACADE_ALPHA: &[&str] = &[
@ -1237,6 +1338,16 @@ const STANDARD_PROCESS_SOURCE_FACADE_ALPHA: &[&str] = &[
const STANDARD_STRING_SOURCE_FACADE_ALPHA: &[&str] = &[ const STANDARD_STRING_SOURCE_FACADE_ALPHA: &[&str] = &[
"len", "len",
"concat", "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_result",
"parse_i32_option", "parse_i32_option",
"parse_u32_result", "parse_u32_result",
@ -1445,9 +1556,14 @@ const STANDARD_VEC_I64_SOURCE_FACADE_ALPHA: &[&str] = &[
"index_of_option", "index_of_option",
"last_index_of_option", "last_index_of_option",
"contains", "contains",
"count_of",
"sum", "sum",
"concat", "concat",
"take", "take",
"starts_with",
"without_prefix",
"ends_with",
"without_suffix",
"drop", "drop",
"reverse", "reverse",
"subvec", "subvec",
@ -1479,9 +1595,14 @@ const STANDARD_VEC_F64_SOURCE_FACADE_ALPHA: &[&str] = &[
"index_of_option", "index_of_option",
"last_index_of_option", "last_index_of_option",
"contains", "contains",
"count_of",
"sum", "sum",
"concat", "concat",
"take", "take",
"starts_with",
"without_prefix",
"ends_with",
"without_suffix",
"drop", "drop",
"reverse", "reverse",
"subvec", "subvec",
@ -3584,6 +3705,10 @@ fn assert_slovo_std_source_layout_alpha(repo: &Path, std_dir: &Path) {
"std.string.parse_i64_result", "std.string.parse_i64_result",
"std.string.parse_u32_result", "std.string.parse_u32_result",
"std.string.parse_i32_result", "std.string.parse_i32_result",
"std.string.byte_at_result",
"std.string.slice_result",
"std.string.starts_with",
"std.string.ends_with",
"std.string.concat", "std.string.concat",
"std.string.len", "std.string.len",
], ],
@ -4343,24 +4468,17 @@ fn assert_slovo_std_source_layout_alpha(repo: &Path, std_dir: &Path) {
} }
assert_std_only_contains( assert_std_only_contains(
&slovo_json, &slovo_json,
STANDARD_JSON_RUNTIME_NAMES, STANDARD_JSON_ALLOWED_STD_NAMES,
"Slovo std/json.slo must not introduce other compiler-known std names", "Slovo std/json.slo must not introduce other compiler-known std names",
); );
assert_std_only_contains( assert_std_only_contains(
&glagol_json, &glagol_json,
STANDARD_JSON_RUNTIME_NAMES, STANDARD_JSON_ALLOWED_STD_NAMES,
"Glagol local json fixture must not introduce other compiler-known std names", "Glagol local json fixture must not introduce other compiler-known std names",
); );
for source in [&slovo_json, &glagol_json] { for source in [&slovo_json, &glagol_json] {
assert!( assert_deferred_json_surface_absent(source, "standard json facade");
!source.contains("parse") assert_json_document_scalar_helpers_are_source_authored(source, "standard json facade");
&& !source.contains("token")
&& !source.contains("map")
&& !source.contains("unicode")
&& !source.contains("schema")
&& !source.contains("stream"),
"standard json facade must not claim deferred parser or richer data policies"
);
} }
for helper in STANDARD_JSON_SOURCE_FACADE_ALPHA { for helper in STANDARD_JSON_SOURCE_FACADE_ALPHA {
assert!( assert!(
@ -7215,10 +7333,17 @@ fn assert_project_std_import_json_tooling_matches_fixture(project: &Path) {
concat!( concat!(
"test \"explicit std json quote escapes facade\" ... ok\n", "test \"explicit std json quote escapes facade\" ... ok\n",
"test \"explicit std json scalar values 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 fields facade\" ... ok\n",
"test \"explicit std json arrays objects facade\" ... ok\n", "test \"explicit std json arrays objects facade\" ... ok\n",
"test \"explicit std json facade all\" ... ok\n", "test \"explicit std json facade all\" ... ok\n",
"5 test(s) passed\n", "12 test(s) passed\n",
), ),
); );
} }
@ -7271,13 +7396,18 @@ fn assert_project_std_import_string_tooling_matches_fixture(project: &Path) {
STANDARD_STRING_SOURCE_FACADE_ALPHA, STANDARD_STRING_SOURCE_FACADE_ALPHA,
concat!( concat!(
"test \"explicit std string len concat\" ... ok\n", "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 result wrappers\" ... ok\n",
"test \"explicit std string parse option 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 integer fallbacks\" ... ok\n",
"test \"explicit std string parse float bool 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 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", "test \"explicit std string helpers all\" ... ok\n",
"7 test(s) passed\n", "12 test(s) passed\n",
), ),
); );
} }
@ -7559,6 +7689,11 @@ fn assert_project_std_import_vec_i64_tooling_matches_fixture(project: &Path) {
"test \"explicit std vec_i64 builder helpers\" ... ok\n", "test \"explicit std vec_i64 builder helpers\" ... ok\n",
"test \"explicit std vec_i64 query helpers\" ... ok\n", "test \"explicit std vec_i64 query helpers\" ... ok\n",
"test \"explicit std vec_i64 option query helpers\" ... ok\n", "test \"explicit std vec_i64 option query helpers\" ... ok\n",
"test \"explicit std vec_i64 count_of helper\" ... ok\n",
"test \"explicit std vec_i64 starts_with helper\" ... ok\n",
"test \"explicit std vec_i64 ends_with helper\" ... ok\n",
"test \"explicit std vec_i64 without_suffix helper\" ... ok\n",
"test \"explicit std vec_i64 without_prefix helper\" ... ok\n",
"test \"explicit std vec_i64 transform helpers\" ... ok\n", "test \"explicit std vec_i64 transform helpers\" ... ok\n",
"test \"explicit std vec_i64 subvec helper\" ... ok\n", "test \"explicit std vec_i64 subvec helper\" ... ok\n",
"test \"explicit std vec_i64 insert helper\" ... ok\n", "test \"explicit std vec_i64 insert helper\" ... ok\n",
@ -7569,7 +7704,7 @@ fn assert_project_std_import_vec_i64_tooling_matches_fixture(project: &Path) {
"test \"explicit std vec_i64 remove range helper\" ... ok\n", "test \"explicit std vec_i64 remove range helper\" ... ok\n",
"test \"explicit std vec_i64 real program helpers\" ... ok\n", "test \"explicit std vec_i64 real program helpers\" ... ok\n",
"test \"explicit std vec_i64 helpers all\" ... ok\n", "test \"explicit std vec_i64 helpers all\" ... ok\n",
"15 test(s) passed\n", "20 test(s) passed\n",
), ),
"std import vec_i64 project test", "std import vec_i64 project test",
); );
@ -7653,6 +7788,7 @@ fn assert_project_std_import_vec_f64_tooling_matches_fixture(project: &Path) {
"test \"explicit std vec_f64 builder helpers\" ... ok\n", "test \"explicit std vec_f64 builder helpers\" ... ok\n",
"test \"explicit std vec_f64 query helpers\" ... ok\n", "test \"explicit std vec_f64 query helpers\" ... ok\n",
"test \"explicit std vec_f64 option query helpers\" ... ok\n", "test \"explicit std vec_f64 option query helpers\" ... ok\n",
"test \"explicit std vec_f64 count_of helper\" ... ok\n",
"test \"explicit std vec_f64 starts_with helper\" ... ok\n", "test \"explicit std vec_f64 starts_with helper\" ... ok\n",
"test \"explicit std vec_f64 ends_with helper\" ... ok\n", "test \"explicit std vec_f64 ends_with helper\" ... ok\n",
"test \"explicit std vec_f64 without_suffix helper\" ... ok\n", "test \"explicit std vec_f64 without_suffix helper\" ... ok\n",
@ -7667,7 +7803,7 @@ fn assert_project_std_import_vec_f64_tooling_matches_fixture(project: &Path) {
"test \"explicit std vec_f64 remove range helper\" ... ok\n", "test \"explicit std vec_f64 remove range helper\" ... ok\n",
"test \"explicit std vec_f64 real program helpers\" ... ok\n", "test \"explicit std vec_f64 real program helpers\" ... ok\n",
"test \"explicit std vec_f64 helpers all\" ... ok\n", "test \"explicit std vec_f64 helpers all\" ... ok\n",
"19 test(s) passed\n", "20 test(s) passed\n",
), ),
"std import vec_f64 project test", "std import vec_f64 project test",
); );
@ -8742,13 +8878,18 @@ fn assert_project_std_layout_local_string_tooling_matches_fixture(project: &Path
test, test,
concat!( concat!(
"test \"explicit local string len concat\" ... ok\n", "test \"explicit local string len concat\" ... ok\n",
"test \"explicit local string byte_at_result wrapper\" ... ok\n",
"test \"explicit local string slice_result wrapper\" ... ok\n",
"test \"explicit local string boundary wrappers\" ... ok\n",
"test \"explicit local string parse result wrappers\" ... ok\n", "test \"explicit local string parse result wrappers\" ... ok\n",
"test \"explicit local string parse option wrappers\" ... ok\n", "test \"explicit local string parse option wrappers\" ... ok\n",
"test \"explicit local string parse integer fallbacks\" ... ok\n", "test \"explicit local string parse integer fallbacks\" ... ok\n",
"test \"explicit local string parse float bool fallbacks\" ... ok\n", "test \"explicit local string parse float bool fallbacks\" ... ok\n",
"test \"explicit local string parse custom fallbacks\" ... ok\n", "test \"explicit local string parse custom fallbacks\" ... ok\n",
"test \"explicit local string search helpers\" ... ok\n",
"test \"explicit local string ascii trim helpers\" ... ok\n",
"test \"explicit local string helpers all\" ... ok\n", "test \"explicit local string helpers all\" ... ok\n",
"7 test(s) passed\n", "12 test(s) passed\n",
), ),
"std layout local string project test", "std layout local string project test",
); );
@ -8779,17 +8920,21 @@ fn assert_standard_string_source_fallback_helpers_alpha(project: &Path) {
"std.string.parse_i64_result", "std.string.parse_i64_result",
"std.string.parse_u32_result", "std.string.parse_u32_result",
"std.string.parse_i32_result", "std.string.parse_i32_result",
"std.string.byte_at_result",
"std.string.slice_result",
"std.string.starts_with",
"std.string.ends_with",
"std.string.concat", "std.string.concat",
"std.string.len", "std.string.len",
], ],
"standard string source helper fixture must use only existing std.string runtime names", "standard string source helper fixture must use only existing std.string runtime names",
); );
assert!( assert!(
!string.contains("trim") !string.contains("locale")
&& !string.contains("locale")
&& !string.contains("unicode") && !string.contains("unicode")
&& !string.contains("bytes") && !string.contains("bytes")
&& !string.contains("case_insensitive") && !string.contains("case_insensitive")
&& !string.contains("regex")
&& !string.contains("host_error"), && !string.contains("host_error"),
"standard string source helper fixture must not claim deferred parsing or richer error APIs" "standard string source helper fixture must not claim deferred parsing or richer error APIs"
); );
@ -9402,10 +9547,17 @@ fn assert_project_std_layout_local_json_tooling_matches_fixture(project: &Path)
concat!( concat!(
"test \"explicit local json quote escapes facade\" ... ok\n", "test \"explicit local json quote escapes facade\" ... ok\n",
"test \"explicit local json scalar values 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 fields facade\" ... ok\n",
"test \"explicit local json arrays objects facade\" ... ok\n", "test \"explicit local json arrays objects facade\" ... ok\n",
"test \"explicit local json facade all\" ... ok\n", "test \"explicit local json facade all\" ... ok\n",
"5 test(s) passed\n", "12 test(s) passed\n",
), ),
"std layout local json project test", "std layout local json project test",
); );
@ -9436,19 +9588,14 @@ fn assert_standard_json_source_facade_alpha(project: &Path) {
} }
assert_std_only_contains( assert_std_only_contains(
&json_source, &json_source,
STANDARD_JSON_RUNTIME_NAMES, STANDARD_JSON_ALLOWED_STD_NAMES,
"standard json source facade fixture must use only approved std runtime names directly", "standard json source facade fixture must use only approved std runtime names directly",
); );
assert!( assert!(
!main.contains("std.") !main.contains("std."),
&& !json_source.contains("parse") "standard json source facade fixture main must remain local"
&& !json_source.contains("token")
&& !json_source.contains("map")
&& !json_source.contains("unicode")
&& !json_source.contains("schema")
&& !json_source.contains("stream"),
"standard json source facade fixture must remain local and must not claim deferred JSON policies"
); );
assert_deferred_json_surface_absent(&json_source, "standard json source facade fixture");
for helper in STANDARD_JSON_SOURCE_FACADE_ALPHA { for helper in STANDARD_JSON_SOURCE_FACADE_ALPHA {
assert!( assert!(
json_source.contains(&format!("(fn {} ", helper)), json_source.contains(&format!("(fn {} ", helper)),
@ -9761,6 +9908,11 @@ fn assert_project_std_layout_local_vec_i64_tooling_matches_fixture(project: &Pat
"test \"explicit local vec_i64 builder helpers\" ... ok\n", "test \"explicit local vec_i64 builder helpers\" ... ok\n",
"test \"explicit local vec_i64 query helpers\" ... ok\n", "test \"explicit local vec_i64 query helpers\" ... ok\n",
"test \"explicit local vec_i64 option query helpers\" ... ok\n", "test \"explicit local vec_i64 option query helpers\" ... ok\n",
"test \"explicit local vec_i64 count_of helper\" ... ok\n",
"test \"explicit local vec_i64 starts_with helper\" ... ok\n",
"test \"explicit local vec_i64 ends_with helper\" ... ok\n",
"test \"explicit local vec_i64 without_suffix helper\" ... ok\n",
"test \"explicit local vec_i64 without_prefix helper\" ... ok\n",
"test \"explicit local vec_i64 transform helpers\" ... ok\n", "test \"explicit local vec_i64 transform helpers\" ... ok\n",
"test \"explicit local vec_i64 subvec helper\" ... ok\n", "test \"explicit local vec_i64 subvec helper\" ... ok\n",
"test \"explicit local vec_i64 insert helper\" ... ok\n", "test \"explicit local vec_i64 insert helper\" ... ok\n",
@ -9771,7 +9923,7 @@ fn assert_project_std_layout_local_vec_i64_tooling_matches_fixture(project: &Pat
"test \"explicit local vec_i64 remove range helper\" ... ok\n", "test \"explicit local vec_i64 remove range helper\" ... ok\n",
"test \"explicit local vec_i64 real program helpers\" ... ok\n", "test \"explicit local vec_i64 real program helpers\" ... ok\n",
"test \"explicit local vec_i64 helpers all\" ... ok\n", "test \"explicit local vec_i64 helpers all\" ... ok\n",
"15 test(s) passed\n", "20 test(s) passed\n",
), ),
"std layout local vec_i64 project test", "std layout local vec_i64 project test",
); );
@ -9795,6 +9947,7 @@ fn assert_project_std_layout_local_vec_f64_tooling_matches_fixture(project: &Pat
"test \"explicit local vec_f64 builder helpers\" ... ok\n", "test \"explicit local vec_f64 builder helpers\" ... ok\n",
"test \"explicit local vec_f64 query helpers\" ... ok\n", "test \"explicit local vec_f64 query helpers\" ... ok\n",
"test \"explicit local vec_f64 option query helpers\" ... ok\n", "test \"explicit local vec_f64 option query helpers\" ... ok\n",
"test \"explicit local vec_f64 count_of helper\" ... ok\n",
"test \"explicit local vec_f64 starts_with helper\" ... ok\n", "test \"explicit local vec_f64 starts_with helper\" ... ok\n",
"test \"explicit local vec_f64 ends_with helper\" ... ok\n", "test \"explicit local vec_f64 ends_with helper\" ... ok\n",
"test \"explicit local vec_f64 without_suffix helper\" ... ok\n", "test \"explicit local vec_f64 without_suffix helper\" ... ok\n",
@ -9809,7 +9962,7 @@ fn assert_project_std_layout_local_vec_f64_tooling_matches_fixture(project: &Pat
"test \"explicit local vec_f64 remove range helper\" ... ok\n", "test \"explicit local vec_f64 remove range helper\" ... ok\n",
"test \"explicit local vec_f64 real program helpers\" ... ok\n", "test \"explicit local vec_f64 real program helpers\" ... ok\n",
"test \"explicit local vec_f64 helpers all\" ... ok\n", "test \"explicit local vec_f64 helpers all\" ... ok\n",
"19 test(s) passed\n", "20 test(s) passed\n",
), ),
"std layout local vec_f64 project test", "std layout local vec_f64 project test",
); );
@ -10577,6 +10730,55 @@ fn assert_std_only_contains(source: &str, allowed: &[&str], context: &str) {
assert!(!remaining.contains("std."), "{}", context); assert!(!remaining.contains("std."), "{}", context);
} }
fn assert_deferred_json_surface_absent(source: &str, context: &str) {
for deferred in [
"parse_object",
"parse_array",
"parse_value",
"tokenize",
"tokenizer",
"schema",
"stream",
"unicode",
"map",
] {
assert!(
!source.contains(deferred),
"{} must not claim deferred JSON `{}` policies",
context,
deferred
);
}
}
fn assert_json_document_scalar_helpers_are_source_authored(source: &str, context: &str) {
for helper in STANDARD_JSON_DOCUMENT_SCALAR_BETA21 {
assert!(
!source.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!(
!source.contains(private_prefix),
"{} must not introduce private JSON document runtime symbol `{}`",
context,
private_prefix
);
}
}
fn repo_root() -> PathBuf { fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")) Path::new(env!("CARGO_MANIFEST_DIR"))
.parent() .parent()

View 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")
}

View File

@ -6,6 +6,7 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
process::{Command, Output, Stdio}, process::{Command, Output, Stdio},
sync::atomic::{AtomicUsize, Ordering}, sync::atomic::{AtomicUsize, Ordering},
time::{SystemTime, UNIX_EPOCH},
}; };
static NEXT_FIXTURE_ID: AtomicUsize = AtomicUsize::new(0); static NEXT_FIXTURE_ID: AtomicUsize = AtomicUsize::new(0);
@ -353,7 +354,7 @@ fn exp10_diagnostics_cover_promoted_and_deferred_boundaries() {
(std.result.map (ok string i32 "a")) (std.result.map (ok string i32 "a"))
0) 0)
"#, "#,
"UnsupportedStandardLibraryCall", "UnsupportedGenericStandardLibraryCall",
), ),
( (
"promoted-name-shadow", "promoted-name-shadow",
@ -951,11 +952,16 @@ fn write_fixture(name: &str, source: &str) -> PathBuf {
} }
fn temp_root(name: &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!( env::temp_dir().join(format!(
"glagol-exp10-host-result-{}-{}-{}", "glagol-exp10-host-result-{}-{}-{}-{}",
name, name,
std::process::id(), std::process::id(),
NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed) NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed),
nanos
)) ))
} }

View File

@ -94,7 +94,7 @@ fn result_helpers_alpha_rejects_deferred_and_misused_std_names() {
(std.result.map (ok i32 i32 1)) (std.result.map (ok i32 i32 1))
0) 0)
"#, "#,
"UnsupportedStandardLibraryCall", "UnsupportedGenericStandardLibraryCall",
), ),
( (
"unwrap-or-deferred", "unwrap-or-deferred",

View 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);
}

View File

@ -7,13 +7,18 @@ use std::{
const EXPECTED_STD_STRING_OUTPUT: &str = concat!( const EXPECTED_STD_STRING_OUTPUT: &str = concat!(
"test \"explicit std string len concat\" ... ok\n", "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 result wrappers\" ... ok\n",
"test \"explicit std string parse option 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 integer fallbacks\" ... ok\n",
"test \"explicit std string parse float bool 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 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", "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!( const EXPECTED_STD_NUM_OUTPUT: &str = concat!(
@ -32,6 +37,16 @@ fn explicit_std_string_import_loads_repo_root_standard_source() {
&[ &[
"len", "len",
"concat", "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_result",
"parse_i32_option", "parse_i32_option",
"parse_u32_result", "parse_u32_result",

View 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
);
}

View 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
);
}

View File

@ -8,19 +8,33 @@ use std::{
const EXPECTED_LOCAL_TEST_OUTPUT: &str = concat!( const EXPECTED_LOCAL_TEST_OUTPUT: &str = concat!(
"test \"explicit local json quote escapes facade\" ... ok\n", "test \"explicit local json quote escapes facade\" ... ok\n",
"test \"explicit local json scalar values 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 fields facade\" ... ok\n",
"test \"explicit local json arrays objects facade\" ... ok\n", "test \"explicit local json arrays objects facade\" ... ok\n",
"test \"explicit local json facade all\" ... ok\n", "test \"explicit local json facade all\" ... ok\n",
"5 test(s) passed\n", "12 test(s) passed\n",
); );
const EXPECTED_STD_IMPORT_TEST_OUTPUT: &str = concat!( const EXPECTED_STD_IMPORT_TEST_OUTPUT: &str = concat!(
"test \"explicit std json quote escapes facade\" ... ok\n", "test \"explicit std json quote escapes facade\" ... ok\n",
"test \"explicit std json scalar values 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 fields facade\" ... ok\n",
"test \"explicit std json arrays objects facade\" ... ok\n", "test \"explicit std json arrays objects facade\" ... ok\n",
"test \"explicit std json facade all\" ... ok\n", "test \"explicit std json facade all\" ... ok\n",
"5 test(s) passed\n", "12 test(s) passed\n",
); );
const STANDARD_JSON_SOURCE_FACADE_ALPHA: &[&str] = &[ const STANDARD_JSON_SOURCE_FACADE_ALPHA: &[&str] = &[
@ -32,6 +46,22 @@ const STANDARD_JSON_SOURCE_FACADE_ALPHA: &[&str] = &[
"i64_value", "i64_value",
"u64_value", "u64_value",
"f64_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_string",
"field_bool", "field_bool",
"field_i32", "field_i32",
@ -58,6 +88,42 @@ const STANDARD_JSON_RUNTIME_NAMES: &[&str] = &[
"std.num.i64_to_string", "std.num.i64_to_string",
"std.num.u64_to_string", "std.num.u64_to_string",
"std.num.f64_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] #[test]
@ -157,23 +223,9 @@ fn assert_json_source_shape(json: &str, main: &str, context: &str) {
runtime_name runtime_name
); );
} }
assert_std_only_contains(json, STANDARD_JSON_RUNTIME_NAMES, context); assert_std_only_contains(json, STANDARD_JSON_ALLOWED_STD_NAMES, context);
assert!( assert_deferred_json_surface_absent(json, main, context);
!json.contains("parse") assert_json_document_scalar_helpers_are_source_authored(json, context);
&& !json.contains("token")
&& !json.contains("map")
&& !json.contains("unicode")
&& !json.contains("schema")
&& !json.contains("stream")
&& !main.contains("parse")
&& !main.contains("token")
&& !main.contains("map")
&& !main.contains("unicode")
&& !main.contains("schema")
&& !main.contains("stream"),
"{} must not claim deferred JSON parsing or richer data APIs",
context
);
for helper in STANDARD_JSON_SOURCE_FACADE_ALPHA { for helper in STANDARD_JSON_SOURCE_FACADE_ALPHA {
assert!( assert!(
@ -191,6 +243,55 @@ fn assert_json_source_shape(json: &str, main: &str, context: &str) {
} }
} }
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) { fn assert_std_only_contains(source: &str, allowed: &[&str], context: &str) {
let mut remaining = source.to_string(); let mut remaining = source.to_string();
for name in allowed { for name in allowed {

View File

@ -0,0 +1,463 @@
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 json_string_parser_lowers_to_private_runtime_helper() {
let fixture = write_fixture(
"lowering",
r#"
(module main)
(fn main () -> i32
(std.result.unwrap_ok (std.json.parse_string_value_result "\"slovo\""))
0)
"#,
);
let output = run_glagol([fixture.as_os_str()]);
assert_success("compile json string parser lowering", &output);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("declare ptr @__glagol_json_parse_string_value_result(ptr)")
&& stdout.contains("call ptr @__glagol_json_parse_string_value_result(")
&& !stdout.contains("@std.json.parse_string_value_result"),
"JSON string parser LLVM shape drifted\nstdout:\n{}",
stdout
);
}
#[test]
fn test_runner_enforces_ascii_json_string_token_contract() {
let source = r#"
(module main)
(test "json string empty ok"
(= (std.result.unwrap_ok (std.json.parse_string_value_result "\"\"")) ""))
(test "json string plain ok"
(= (std.result.unwrap_ok (std.json.parse_string_value_result "\"slovo\"")) "slovo"))
(test "json string quote backslash slash ok"
(= (std.result.unwrap_ok (std.json.parse_string_value_result "\"slo\\\"vo\\\\path\\/leaf\"")) "slo\"vo\\path/leaf"))
(test "json string newline tab escapes ok"
(= (std.result.unwrap_ok (std.json.parse_string_value_result "\"line\\nnext\\tend\"")) "line\nnext\tend"))
(test "json string backspace formfeed roundtrip ok"
(= (std.json.quote_string (std.result.unwrap_ok (std.json.parse_string_value_result "\"a\\b\\f\""))) "\"a\\b\\f\""))
(test "json string missing quotes err"
(= (std.result.unwrap_err (std.json.parse_string_value_result "slovo")) 1))
(test "json string leading whitespace err"
(= (std.result.unwrap_err (std.json.parse_string_value_result " \"slovo\"")) 1))
(test "json string trailing whitespace err"
(= (std.result.unwrap_err (std.json.parse_string_value_result "\"slovo\" ")) 1))
(test "json string unterminated err"
(= (std.result.unwrap_err (std.json.parse_string_value_result "\"slovo")) 1))
(test "json string trailing bytes err"
(= (std.result.unwrap_err (std.json.parse_string_value_result "\"slovo\"x")) 1))
(test "json string raw quote err"
(= (std.result.unwrap_err (std.json.parse_string_value_result "\"slo\"vo\"")) 1))
(test "json string bad escape err"
(= (std.result.unwrap_err (std.json.parse_string_value_result "\"bad\\x\"")) 1))
(test "json string unicode escape deferred err"
(= (std.result.unwrap_err (std.json.parse_string_value_result "\"\\u0041\"")) 1))
(test "json string raw newline err"
(= (std.result.unwrap_err (std.json.parse_string_value_result "\"line\nnext\"")) 1))
"#;
let fixture = write_fixture("test-runner", source);
let output = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
assert_success("run json string parser tests", &output);
assert_eq!(
String::from_utf8_lossy(&output.stdout),
concat!(
"test \"json string empty ok\" ... ok\n",
"test \"json string plain ok\" ... ok\n",
"test \"json string quote backslash slash ok\" ... ok\n",
"test \"json string newline tab escapes ok\" ... ok\n",
"test \"json string backspace formfeed roundtrip ok\" ... ok\n",
"test \"json string missing quotes err\" ... ok\n",
"test \"json string leading whitespace err\" ... ok\n",
"test \"json string trailing whitespace err\" ... ok\n",
"test \"json string unterminated err\" ... ok\n",
"test \"json string trailing bytes err\" ... ok\n",
"test \"json string raw quote err\" ... ok\n",
"test \"json string bad escape err\" ... ok\n",
"test \"json string unicode escape deferred err\" ... ok\n",
"test \"json string raw newline err\" ... ok\n",
"14 test(s) passed\n",
),
"json string parser test runner stdout drifted"
);
}
#[test]
fn hosted_json_string_parser_smoke_when_toolchain_is_available() {
let fixture = write_fixture(
"runtime-smoke",
r#"
(module main)
(fn main () -> i32
(std.io.print_string (std.result.unwrap_ok (std.json.parse_string_value_result "\"slovo\"")))
(std.io.print_string (std.result.unwrap_ok (std.json.parse_string_value_result "\"slo\\\"vo\"")))
(std.io.print_string (std.result.unwrap_ok (std.json.parse_string_value_result "\"path\\/leaf\"")))
(std.result.unwrap_err (std.json.parse_string_value_result "\"\\u0041\"")))
"#,
);
let binary = unique_path("json-string-parsing-beta18-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 string 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 string 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),
"slovo\nslo\"vo\npath/leaf\n",
"json string parser binary stdout drifted"
);
assert!(
run.stderr.is_empty(),
"json string parser binary wrote stderr:\n{}",
String::from_utf8_lossy(&run.stderr)
);
}
#[test]
fn hosted_json_string_runtime_rejects_raw_non_ascii_when_clang_is_available() {
let Some(clang) = find_clang() else {
eprintln!("skipping beta18 raw non-ASCII runtime smoke: set GLAGOL_CLANG or install clang");
return;
};
let c_source = write_c_fixture(
"raw-non-ascii",
r#"
#include <stdio.h>
#include <stdlib.h>
char *__glagol_json_parse_string_value_result(const char *text);
int main(void) {
const char token[] = { '"', (char)0xc3, (char)0xa9, '"', '\0' };
char *value = __glagol_json_parse_string_value_result(token);
if (value != NULL) {
free(value);
fputs("accepted raw non-ascii JSON string token\n", stderr);
return 1;
}
puts("raw-non-ascii rejected");
return 0;
}
"#,
);
let runtime = Path::new(env!("CARGO_MANIFEST_DIR")).join("../runtime/runtime.c");
let binary = unique_path("json-string-raw-non-ascii-beta18-bin");
let mut compile = Command::new(&clang);
compile.arg(runtime).arg(c_source).arg("-o").arg(&binary);
configure_clang_runtime_env(&mut compile, &clang);
let compile = compile
.output()
.unwrap_or_else(|err| panic!("run `{}`: {}", clang.display(), err));
assert_success("clang beta18 raw non-ASCII runtime smoke", &compile);
let run = Command::new(&binary)
.output()
.unwrap_or_else(|err| panic!("run `{}`: {}", binary.display(), err));
assert_eq!(
run.status.code(),
Some(0),
"raw non-ASCII runtime smoke 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),
"raw-non-ascii rejected\n",
"raw non-ASCII runtime smoke stdout drifted"
);
assert!(
run.stderr.is_empty(),
"raw non-ASCII runtime smoke wrote stderr:\n{}",
String::from_utf8_lossy(&run.stderr)
);
}
#[test]
fn json_string_parser_diagnostics_cover_promoted_name_and_shadowing() {
let arity = write_fixture(
"arity",
r#"
(module main)
(fn main () -> (result string i32)
(std.json.parse_string_value_result))
"#,
);
assert_rejected(
"std.json.parse_string_value_result arity",
run_glagol([arity.as_os_str()]),
"wrong number of arguments",
);
let type_mismatch = write_fixture(
"type",
r#"
(module main)
(fn main () -> (result string i32)
(std.json.parse_string_value_result 1))
"#,
);
assert_rejected(
"std.json.parse_string_value_result type",
run_glagol([type_mismatch.as_os_str()]),
"cannot call `std.json.parse_string_value_result` with argument of wrong type",
);
let source_shadow = write_fixture(
"source-shadow",
r#"
(module main)
(fn std.json.parse_string_value_result ((text string)) -> (result string i32)
(err string i32 1))
(fn main () -> i32
0)
"#,
);
assert_rejected(
"std.json.parse_string_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_string_value_result ((text string)) -> (result string i32)
(err string i32 1))
(fn main () -> i32
0)
"#,
);
assert_rejected(
"__glagol_json_parse_string_value_result helper shadow",
run_glagol([helper_shadow.as_os_str()]),
"DuplicateFunction",
);
}
#[test]
fn deferred_json_parser_families_remain_unsupported_after_string_tokens() {
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_beta18_promoted_string_parser() {
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
);
assert!(
stderr.contains("std.json.parse_string_value_result"),
"unsupported std guidance omitted promoted beta18 name\nstderr:\n{}",
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-string-beta18-{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 write_c_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-string-beta18-c-{id}-{name}"));
fs::create_dir_all(&dir).unwrap_or_else(|err| panic!("create `{}`: {}", dir.display(), err));
let path = dir.join("main.c");
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 find_clang() -> Option<PathBuf> {
if let Some(path) = env::var_os("GLAGOL_CLANG").filter(|value| !value.is_empty()) {
return Some(PathBuf::from(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")?;
for dir in env::split_paths(&path) {
let candidate = dir.join(name);
if candidate.is_file() {
return Some(candidate);
}
}
None
}
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 lib_dir = root.join("usr/lib");
let lib64_dir = root.join("usr/lib64");
let include_dir = root.join("usr/include");
command.env("CPATH", include_dir);
command.env(
"LIBRARY_PATH",
format!("{}:{}", lib_dir.display(), lib64_dir.display()),
);
}
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
);
}

View File

@ -0,0 +1,309 @@
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_string_scanning_beta16_lowering_shape_uses_runtime_symbols() {
let fixture = write_fixture(
"std-string-scanning-beta16-lowering",
r#"
(module main)
(fn byte_or_code ((text string) (index i32)) -> i32
(match (std.string.byte_at_result text index)
((ok value)
value)
((err code)
code)))
(fn slice_or_empty ((text string) (start i32) (count i32)) -> string
(match (std.string.slice_result text start count)
((ok value)
value)
((err code)
"")))
(fn has_edges ((text string)) -> bool
(if (std.string.starts_with text "slo")
(std.string.ends_with text "vo")
false))
(fn main () -> i32
(if (has_edges (slice_or_empty "slovo" 0 5))
(byte_or_code "slovo" 3)
1))
"#,
);
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 rejected beta16 string scanning fixture\nstdout:\n{}\nstderr:\n{}",
stdout,
stderr
);
assert!(
stdout.contains("declare i64 @__glagol_string_byte_at_result(ptr, i32)")
&& stdout.contains("declare ptr @__glagol_string_slice_result(ptr, i32, i32)")
&& stdout.contains("declare i1 @__glagol_string_starts_with(ptr, ptr)")
&& stdout.contains("declare i1 @__glagol_string_ends_with(ptr, ptr)")
&& stdout.contains("call i64 @__glagol_string_byte_at_result(ptr %text, i32 %index)")
&& stdout.contains(
"call ptr @__glagol_string_slice_result(ptr %text, i32 %start, i32 %count)"
)
&& stdout.contains("call i1 @__glagol_string_starts_with(ptr %text, ptr @.str.")
&& stdout.contains("call i1 @__glagol_string_ends_with(ptr %text, ptr @.str.")
&& stdout.contains("icmp ne ptr %")
&& stdout.contains("insertvalue { i1, ptr, i32 }")
&& !stdout.contains("@std.string.byte_at_result")
&& !stdout.contains("@std.string.slice_result")
&& !stdout.contains("@std.string.starts_with")
&& !stdout.contains("@std.string.ends_with"),
"LLVM output did not contain expected beta16 string runtime shape\nstdout:\n{}",
stdout
);
assert!(stderr.is_empty(), "compiler wrote stderr:\n{}", stderr);
}
#[test]
fn test_runner_executes_standard_string_scanning_beta16_boundaries() {
let fixture = write_fixture(
"std-string-scanning-beta16-test-runner",
r#"
(module main)
(test "byte first ok"
(= (unwrap_ok (std.string.byte_at_result "slovo" 0)) 115))
(test "byte last ok"
(= (unwrap_ok (std.string.byte_at_result "slovo" 4)) 111))
(test "byte empty err"
(= (unwrap_err (std.string.byte_at_result "" 0)) 1))
(test "byte negative err"
(= (unwrap_err (std.string.byte_at_result "slovo" -1)) 1))
(test "byte end err"
(= (unwrap_err (std.string.byte_at_result "slovo" 5)) 1))
(test "slice middle ok"
(= (unwrap_ok (std.string.slice_result "slovo" 1 3)) "lov"))
(test "slice zero count ok"
(= (unwrap_ok (std.string.slice_result "slovo" 2 0)) ""))
(test "slice len zero ok"
(= (unwrap_ok (std.string.slice_result "slovo" 5 0)) ""))
(test "slice full ok"
(= (unwrap_ok (std.string.slice_result "slovo" 0 5)) "slovo"))
(test "slice negative start err"
(= (unwrap_err (std.string.slice_result "slovo" -1 1)) 1))
(test "slice negative count err"
(= (unwrap_err (std.string.slice_result "slovo" 1 -1)) 1))
(test "slice overrun err"
(= (unwrap_err (std.string.slice_result "slovo" 4 2)) 1))
(test "slice start past end err"
(= (unwrap_err (std.string.slice_result "slovo" 6 0)) 1))
(test "starts true empty"
(if (std.string.starts_with "slovo" "slo")
(std.string.starts_with "slovo" "")
false))
(test "starts false middle"
(= (std.string.starts_with "slovo" "ovo") false))
(test "ends true empty"
(if (std.string.ends_with "slovo" "ovo")
(std.string.ends_with "slovo" "")
false))
(test "ends false prefix"
(= (std.string.ends_with "slovo" "slo") false))
"#,
);
let output = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
assert_success("run beta16 string scanning tests", &output);
assert_eq!(
String::from_utf8_lossy(&output.stdout),
concat!(
"test \"byte first ok\" ... ok\n",
"test \"byte last ok\" ... ok\n",
"test \"byte empty err\" ... ok\n",
"test \"byte negative err\" ... ok\n",
"test \"byte end err\" ... ok\n",
"test \"slice middle ok\" ... ok\n",
"test \"slice zero count ok\" ... ok\n",
"test \"slice len zero ok\" ... ok\n",
"test \"slice full ok\" ... ok\n",
"test \"slice negative start err\" ... ok\n",
"test \"slice negative count err\" ... ok\n",
"test \"slice overrun err\" ... ok\n",
"test \"slice start past end err\" ... ok\n",
"test \"starts true empty\" ... ok\n",
"test \"starts false middle\" ... ok\n",
"test \"ends true empty\" ... ok\n",
"test \"ends false prefix\" ... ok\n",
"17 test(s) passed\n",
),
"beta16 string scanning test runner stdout drifted"
);
}
#[test]
fn standard_string_scanning_beta16_runtime_smoke_when_clang_is_available() {
let Some(clang) = find_clang() else {
eprintln!(
"skipping beta16 string scanning runtime smoke: set GLAGOL_CLANG or install clang"
);
return;
};
let fixture = write_fixture(
"std-string-scanning-beta16-runtime-smoke",
r#"
(module main)
(fn main () -> i32
(std.io.print_i32 (unwrap_ok (std.string.byte_at_result "slovo" 3)))
(std.io.print_string (unwrap_ok (std.string.slice_result "slovo" 1 3)))
(std.io.print_bool (std.string.starts_with "slovo" "slo"))
(std.io.print_bool (std.string.ends_with "slovo" "ovo"))
(std.io.print_i32 (unwrap_err (std.string.byte_at_result "slovo" 5)))
(std.io.print_i32 (unwrap_err (std.string.slice_result "slovo" 4 2)))
0)
"#,
);
let compile = run_glagol([fixture.as_os_str()]);
assert_success("compile beta16 string scanning runtime smoke", &compile);
let run = compile_and_run_with_runtime(&clang, "std-string-scanning-beta16", &compile.stdout);
assert_success("run beta16 string scanning runtime smoke", &run);
assert_eq!(
String::from_utf8_lossy(&run.stdout),
"118\nlov\ntrue\ntrue\n1\n1\n",
"beta16 string scanning runtime stdout drifted"
);
}
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_fixture(name: &str, source: &str) -> PathBuf {
let mut path = env::temp_dir();
path.push(format!(
"glagol-standard-string-scanning-beta16-{}-{}-{}.slo",
name,
std::process::id(),
NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed)
));
fs::write(&path, source).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err));
path
}
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
);
}
fn compile_and_run_with_runtime(clang: &Path, name: &str, ir: &[u8]) -> Output {
let manifest = Path::new(env!("CARGO_MANIFEST_DIR"));
let temp_dir = env::temp_dir().join(format!(
"glagol-standard-string-scanning-beta16-{}-{}",
std::process::id(),
NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed)
));
fs::create_dir_all(&temp_dir)
.unwrap_or_else(|err| panic!("create `{}`: {}", temp_dir.display(), err));
let ir_path = temp_dir.join(format!("{}.ll", name));
let exe_path = temp_dir.join(name);
fs::write(&ir_path, ir).unwrap_or_else(|err| panic!("write `{}`: {}", ir_path.display(), err));
let runtime = manifest.join("../runtime/runtime.c");
let mut clang_command = Command::new(clang);
clang_command
.arg(&runtime)
.arg(&ir_path)
.arg("-o")
.arg(&exe_path)
.current_dir(manifest);
configure_clang_runtime_env(&mut clang_command, clang);
let clang_output = clang_command
.output()
.unwrap_or_else(|err| panic!("run `{}`: {}", clang.display(), err));
assert_success("clang beta16 string scanning runtime smoke", &clang_output);
Command::new(&exe_path)
.output()
.unwrap_or_else(|err| panic!("run `{}`: {}", exe_path.display(), err))
}
fn find_clang() -> Option<PathBuf> {
if let Some(path) = env::var_os("GLAGOL_CLANG").filter(|value| !value.is_empty()) {
return Some(PathBuf::from(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(program: &str) -> Option<PathBuf> {
let path = env::var_os("PATH")?;
env::split_paths(&path)
.map(|dir| dir.join(program))
.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 mut paths = vec![lib64, lib];
if let Some(existing) = env::var_os("LD_LIBRARY_PATH") {
paths.extend(env::split_paths(&existing));
}
let joined = env::join_paths(paths).expect("join LD_LIBRARY_PATH");
command.env("LD_LIBRARY_PATH", joined);
}

View File

@ -0,0 +1,478 @@
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 string contains\" ... ok\n",
"test \"explicit std string index_of_option\" ... ok\n",
"test \"explicit std string last_index_of_option\" ... ok\n",
"test \"explicit std string ascii trim\" ... ok\n",
"test \"explicit std string search trim composition\" ... ok\n",
"test \"explicit std string search trim all\" ... ok\n",
"6 test(s) passed\n",
);
const STANDARD_STRING_SEARCH_TRIM_BETA20: &[&str] = &[
"contains",
"index_of_option",
"last_index_of_option",
"trim_ascii_start",
"trim_ascii_end",
"trim_ascii",
];
const ALLOWED_STD_REFERENCES: &[&str] = &[
"std.result",
"std.string.parse_bool_result",
"std.string.parse_f64_result",
"std.string.parse_i64_result",
"std.string.parse_u64_result",
"std.string.parse_i32_result",
"std.string.parse_u32_result",
"std.string.byte_at_result",
"std.string.slice_result",
"std.string.starts_with",
"std.string.ends_with",
"std.string.concat",
"std.string.len",
];
#[test]
fn explicit_std_string_search_and_ascii_trim_helpers_check_and_test() {
let project = write_project(
"std-string-search-trim-beta20",
r#"
(module main)
(import std.string (contains index_of_option last_index_of_option trim_ascii_start trim_ascii_end trim_ascii))
(fn option_i32_eq ((maybe (option i32)) (expected i32)) -> bool
(match maybe
((some value)
(= value expected))
((none)
false)))
(fn option_i32_none ((maybe (option i32))) -> bool
(match maybe
((some value)
false)
((none)
true)))
(fn imported_string_contains_ok () -> bool
(if (contains "slovo compiler" "slo")
(if (contains "slovo compiler" "compiler")
(= (contains "slovo compiler" "missing") false)
false)
false))
(fn imported_string_index_of_ok () -> bool
(if (option_i32_eq (index_of_option "bananana" "ana") 1)
(if (option_i32_eq (index_of_option "slovo" "s") 0)
(if (option_i32_eq (index_of_option "slovo" "vo") 3)
(option_i32_none (index_of_option "slovo" "compiler"))
false)
false)
false))
(fn imported_string_last_index_of_ok () -> bool
(if (option_i32_eq (last_index_of_option "bananana" "ana") 5)
(if (option_i32_eq (last_index_of_option "slovo" "o") 4)
(if (option_i32_eq (last_index_of_option "slovo" "s") 0)
(option_i32_none (last_index_of_option "slovo" "compiler"))
false)
false)
false))
(fn imported_string_ascii_trim_ok () -> bool
(if (= (trim_ascii_start "\n\t slovo \t") "slovo \t")
(if (= (trim_ascii_end "\n\t slovo \t") "\n\t slovo")
(if (= (trim_ascii "\n\t slovo \t") "slovo")
(if (= (trim_ascii "slovo") "slovo")
(= (trim_ascii " ") "")
false)
false)
false)
false))
(fn imported_string_search_trim_composes_ok () -> bool
(if (= (trim_ascii " slovo compiler ") "slovo compiler")
(if (contains (trim_ascii " slovo compiler ") "compiler")
(if (option_i32_eq (index_of_option (trim_ascii_start "\t\tprefix-core") "core") 7)
(option_i32_eq (last_index_of_option (trim_ascii_end "core-core\n") "core") 5)
false)
false)
false))
(fn imported_string_search_trim_all_ok () -> bool
(if (imported_string_contains_ok)
(if (imported_string_index_of_ok)
(if (imported_string_last_index_of_ok)
(if (imported_string_ascii_trim_ok)
(imported_string_search_trim_composes_ok)
false)
false)
false)
false))
(fn main () -> i32
(if (imported_string_search_trim_all_ok)
42
1))
(test "explicit std string contains"
(imported_string_contains_ok))
(test "explicit std string index_of_option"
(imported_string_index_of_ok))
(test "explicit std string last_index_of_option"
(imported_string_last_index_of_ok))
(test "explicit std string ascii trim"
(imported_string_ascii_trim_ok))
(test "explicit std string search trim composition"
(imported_string_search_trim_composes_ok))
(test "explicit std string search trim all"
(= (main) 42))
"#,
);
let source = read(&project.join("src/main.slo"));
let std_string = read(&std_string_path());
assert!(
!project.join("src/string.slo").exists(),
"beta20 fixture must exercise repo-root std.string, not a local module copy"
);
assert!(
source.starts_with("(module main)\n\n(import std.string ("),
"beta20 fixture must use an explicit std.string import"
);
assert_std_string_search_trim_facades(&std_string);
let fmt = run_glagol([
OsStr::new("fmt"),
OsStr::new("--check"),
project.as_os_str(),
]);
assert_success("std string search trim fmt --check", &fmt);
let check = run_glagol([OsStr::new("check"), project.as_os_str()]);
assert_success_stdout(check, "", "std string search trim check");
let test = run_glagol([OsStr::new("test"), project.as_os_str()]);
assert_success_stdout(test, EXPECTED_TEST_OUTPUT, "std string search trim test");
}
#[test]
fn string_search_and_ascii_trim_helpers_are_not_compiler_known_runtime_calls() {
let std_string = read(&std_string_path());
assert_std_string_search_trim_facades(&std_string);
for helper in STANDARD_STRING_SEARCH_TRIM_BETA20 {
assert!(
!std_string.contains(&format!("std.string.{}", helper)),
"std.string.{} must remain source-authored, not a compiler-known runtime call",
helper
);
assert!(
!std_string.contains(&format!("__glagol_string_{}", helper)),
"std.string.{} must not introduce a private runtime symbol",
helper
);
}
let cases = [
UnsupportedRuntimeCase {
name: "contains",
symbol: "std.string.contains",
source: r#"
(module main)
(fn main () -> i32
(if (std.string.contains "slovo" "ovo")
0
1))
"#,
},
UnsupportedRuntimeCase {
name: "index-of-option",
symbol: "std.string.index_of_option",
source: r#"
(module main)
(fn main () -> i32
(match (std.string.index_of_option "slovo" "o")
((some value)
value)
((none)
0)))
"#,
},
UnsupportedRuntimeCase {
name: "last-index-of-option",
symbol: "std.string.last_index_of_option",
source: r#"
(module main)
(fn main () -> i32
(match (std.string.last_index_of_option "slovo" "o")
((some value)
value)
((none)
0)))
"#,
},
UnsupportedRuntimeCase {
name: "trim-ascii-start",
symbol: "std.string.trim_ascii_start",
source: r#"
(module main)
(fn main () -> i32
(std.string.len (std.string.trim_ascii_start " slovo")))
"#,
},
UnsupportedRuntimeCase {
name: "trim-ascii-end",
symbol: "std.string.trim_ascii_end",
source: r#"
(module main)
(fn main () -> i32
(std.string.len (std.string.trim_ascii_end "slovo ")))
"#,
},
UnsupportedRuntimeCase {
name: "trim-ascii",
symbol: "std.string.trim_ascii",
source: r#"
(module main)
(fn main () -> i32
(std.string.len (std.string.trim_ascii " slovo ")))
"#,
},
];
for case in cases {
let fixture = write_fixture(case.name, case.source);
let output = run_glagol([fixture.as_os_str()]);
assert_failure_stderr_contains(
&format!("direct {} runtime call", case.symbol),
&output,
&format!("standard library call `{}` is not supported", case.symbol),
);
}
}
fn assert_std_string_search_trim_facades(std_string: &str) {
assert!(
std_string.starts_with("(module string (export "),
"lib/std/string.slo must stay a source-authored module export"
);
let mut non_allowed_std = std_string.to_owned();
for allowed in ALLOWED_STD_REFERENCES {
non_allowed_std = non_allowed_std.replace(allowed, "");
}
assert!(
!non_allowed_std.contains("std."),
"std.string beta20 helpers must use only existing std.result bridges and promoted beta16-or-earlier std.string primitives"
);
for helper in STANDARD_STRING_SEARCH_TRIM_BETA20 {
assert!(
std_string.contains(&format!("(fn {} ", helper)),
"lib/std/string.slo is missing source facade `{}`",
helper
);
}
let search_trim_source = search_trim_source_region(std_string);
for primitive in [
("len", ["std.string.len", "(len "]),
(
"byte_at_result",
["std.string.byte_at_result", "(byte_at_result "],
),
(
"slice_result",
["std.string.slice_result", "(slice_result "],
),
("starts_with", ["std.string.starts_with", "(starts_with "]),
] {
assert!(
primitive
.1
.iter()
.any(|needle| search_trim_source.contains(needle)),
"beta20 search/trim facades must compose over existing beta16 string primitive `{}`",
primitive.0
);
}
assert!(
!std_string.contains("unicode")
&& !std_string.contains("grapheme")
&& !std_string.contains("locale")
&& !std_string.contains("case_insensitive")
&& !std_string.contains("regex"),
"beta20 string helpers must not claim deferred Unicode, locale, case-folding, or regex APIs"
);
}
fn search_trim_source_region(source: &str) -> &str {
let ends_with_end = function_range(source, "ends_with").1;
let parse_start = function_range(source, "parse_i32_result").0;
&source[ends_with_end..parse_start]
}
fn function_range(source: &str, name: &str) -> (usize, usize) {
let needle = format!("(fn {} ", name);
let start = source
.find(&needle)
.unwrap_or_else(|| panic!("missing function `{}`", name));
let mut depth = 0usize;
for (offset, byte) in source.as_bytes()[start..].iter().enumerate() {
match byte {
b'(' => depth += 1,
b')' => {
depth = depth
.checked_sub(1)
.unwrap_or_else(|| panic!("unbalanced function `{}`", name));
if depth == 0 {
return (start, start + offset + 1);
}
}
_ => {}
}
}
panic!("unterminated function `{}`", 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)
.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-string-search-trim-beta20-{}-{}-{}.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-string-search-trim-beta20-{}-{}-{}",
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_string_path() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../lib/std/string.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
);
}
struct UnsupportedRuntimeCase {
name: &'static str,
symbol: &'static str,
source: &'static str,
}

View File

@ -7,18 +7,33 @@ use std::{
const EXPECTED_TEST_OUTPUT: &str = concat!( const EXPECTED_TEST_OUTPUT: &str = concat!(
"test \"explicit local string len concat\" ... ok\n", "test \"explicit local string len concat\" ... ok\n",
"test \"explicit local string byte_at_result wrapper\" ... ok\n",
"test \"explicit local string slice_result wrapper\" ... ok\n",
"test \"explicit local string boundary wrappers\" ... ok\n",
"test \"explicit local string parse result wrappers\" ... ok\n", "test \"explicit local string parse result wrappers\" ... ok\n",
"test \"explicit local string parse option wrappers\" ... ok\n", "test \"explicit local string parse option wrappers\" ... ok\n",
"test \"explicit local string parse integer fallbacks\" ... ok\n", "test \"explicit local string parse integer fallbacks\" ... ok\n",
"test \"explicit local string parse float bool fallbacks\" ... ok\n", "test \"explicit local string parse float bool fallbacks\" ... ok\n",
"test \"explicit local string parse custom fallbacks\" ... ok\n", "test \"explicit local string parse custom fallbacks\" ... ok\n",
"test \"explicit local string search helpers\" ... ok\n",
"test \"explicit local string ascii trim helpers\" ... ok\n",
"test \"explicit local string helpers all\" ... ok\n", "test \"explicit local string helpers all\" ... ok\n",
"7 test(s) passed\n", "12 test(s) passed\n",
); );
const STANDARD_STRING_SOURCE_FALLBACK_HELPERS_ALPHA: &[&str] = &[ const STANDARD_STRING_SOURCE_FALLBACK_HELPERS_ALPHA: &[&str] = &[
"len", "len",
"concat", "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_result",
"parse_i32_option", "parse_i32_option",
"parse_u32_result", "parse_u32_result",
@ -100,6 +115,10 @@ fn assert_local_string_fixture_is_source_authored(project: &Path) {
"std.string.parse_u64_result", "std.string.parse_u64_result",
"std.string.parse_i32_result", "std.string.parse_i32_result",
"std.string.parse_u32_result", "std.string.parse_u32_result",
"std.string.byte_at_result",
"std.string.slice_result",
"std.string.starts_with",
"std.string.ends_with",
"std.string.concat", "std.string.concat",
"std.string.len", "std.string.len",
] { ] {
@ -110,11 +129,11 @@ fn assert_local_string_fixture_is_source_authored(project: &Path) {
"string fixture must use only the existing promoted std.string runtime names" "string fixture must use only the existing promoted std.string runtime names"
); );
assert!( assert!(
!string.contains("trim") !string.contains("locale")
&& !string.contains("locale")
&& !string.contains("unicode") && !string.contains("unicode")
&& !string.contains("bytes") && !string.contains("bytes")
&& !string.contains("case_insensitive") && !string.contains("case_insensitive")
&& !string.contains("regex")
&& !string.contains("host_error"), && !string.contains("host_error"),
"string fixture must not claim deferred parsing or richer error APIs" "string fixture must not claim deferred parsing or richer error APIs"
); );

View File

@ -11,6 +11,7 @@ const EXPECTED_TEST_OUTPUT: &str = concat!(
"test \"explicit local vec_f64 builder helpers\" ... ok\n", "test \"explicit local vec_f64 builder helpers\" ... ok\n",
"test \"explicit local vec_f64 query helpers\" ... ok\n", "test \"explicit local vec_f64 query helpers\" ... ok\n",
"test \"explicit local vec_f64 option query helpers\" ... ok\n", "test \"explicit local vec_f64 option query helpers\" ... ok\n",
"test \"explicit local vec_f64 count_of helper\" ... ok\n",
"test \"explicit local vec_f64 starts_with helper\" ... ok\n", "test \"explicit local vec_f64 starts_with helper\" ... ok\n",
"test \"explicit local vec_f64 ends_with helper\" ... ok\n", "test \"explicit local vec_f64 ends_with helper\" ... ok\n",
"test \"explicit local vec_f64 without_suffix helper\" ... ok\n", "test \"explicit local vec_f64 without_suffix helper\" ... ok\n",
@ -25,7 +26,7 @@ const EXPECTED_TEST_OUTPUT: &str = concat!(
"test \"explicit local vec_f64 remove range helper\" ... ok\n", "test \"explicit local vec_f64 remove range helper\" ... ok\n",
"test \"explicit local vec_f64 real program helpers\" ... ok\n", "test \"explicit local vec_f64 real program helpers\" ... ok\n",
"test \"explicit local vec_f64 helpers all\" ... ok\n", "test \"explicit local vec_f64 helpers all\" ... ok\n",
"19 test(s) passed\n", "20 test(s) passed\n",
); );
const STANDARD_VEC_F64_SOURCE_FACADE_ALPHA: &[&str] = &[ const STANDARD_VEC_F64_SOURCE_FACADE_ALPHA: &[&str] = &[
@ -48,6 +49,7 @@ const STANDARD_VEC_F64_SOURCE_FACADE_ALPHA: &[&str] = &[
"index_of_option", "index_of_option",
"last_index_of_option", "last_index_of_option",
"contains", "contains",
"count_of",
"sum", "sum",
"concat", "concat",
"take", "take",
@ -184,6 +186,7 @@ fn assert_local_vec_f64_fixture_is_source_authored(project: &Path) {
"index_of_option_loop", "index_of_option_loop",
"last_index_of_option_loop", "last_index_of_option_loop",
"contains_loop", "contains_loop",
"count_of_loop",
"sum_loop", "sum_loop",
"concat_loop", "concat_loop",
"take_loop", "take_loop",
@ -196,6 +199,43 @@ fn assert_local_vec_f64_fixture_is_source_authored(project: &Path) {
helper helper
); );
} }
assert_count_of_cases_are_exercised(&main);
assert_prefix_suffix_cases_are_exercised(&main);
}
fn assert_count_of_cases_are_exercised(main: &str) {
assert!(
main.contains("(count_of (empty)"),
"main.slo must exercise count_of on an empty vec_f64"
);
assert!(
main.matches("(count_of").count() >= 4,
"main.slo must exercise repeated, singleton, and absent count_of cases"
);
}
fn assert_prefix_suffix_cases_are_exercised(main: &str) {
for case in [
"(starts_with values (empty))",
"(starts_with values values)",
"(ends_with values (empty))",
"(ends_with values values)",
"(without_prefix values (empty))",
"(without_prefix values values)",
"(without_suffix values (empty))",
"(without_suffix values values)",
"mismatched_prefix",
"mismatched_suffix",
"longer_prefix",
"longer_suffix",
] {
assert!(
main.contains(case),
"main.slo must exercise vec_f64 prefix/suffix case `{}`",
case
);
}
} }
fn run_glagol<I, S>(args: I) -> Output fn run_glagol<I, S>(args: I) -> Output

View File

@ -11,6 +11,7 @@ const EXPECTED_STD_VEC_F64_OUTPUT: &str = concat!(
"test \"explicit std vec_f64 builder helpers\" ... ok\n", "test \"explicit std vec_f64 builder helpers\" ... ok\n",
"test \"explicit std vec_f64 query helpers\" ... ok\n", "test \"explicit std vec_f64 query helpers\" ... ok\n",
"test \"explicit std vec_f64 option query helpers\" ... ok\n", "test \"explicit std vec_f64 option query helpers\" ... ok\n",
"test \"explicit std vec_f64 count_of helper\" ... ok\n",
"test \"explicit std vec_f64 starts_with helper\" ... ok\n", "test \"explicit std vec_f64 starts_with helper\" ... ok\n",
"test \"explicit std vec_f64 ends_with helper\" ... ok\n", "test \"explicit std vec_f64 ends_with helper\" ... ok\n",
"test \"explicit std vec_f64 without_suffix helper\" ... ok\n", "test \"explicit std vec_f64 without_suffix helper\" ... ok\n",
@ -25,7 +26,7 @@ const EXPECTED_STD_VEC_F64_OUTPUT: &str = concat!(
"test \"explicit std vec_f64 remove range helper\" ... ok\n", "test \"explicit std vec_f64 remove range helper\" ... ok\n",
"test \"explicit std vec_f64 real program helpers\" ... ok\n", "test \"explicit std vec_f64 real program helpers\" ... ok\n",
"test \"explicit std vec_f64 helpers all\" ... ok\n", "test \"explicit std vec_f64 helpers all\" ... ok\n",
"19 test(s) passed\n", "20 test(s) passed\n",
); );
const STANDARD_VEC_F64_SOURCE_FACADE_ALPHA: &[&str] = &[ const STANDARD_VEC_F64_SOURCE_FACADE_ALPHA: &[&str] = &[
@ -48,6 +49,7 @@ const STANDARD_VEC_F64_SOURCE_FACADE_ALPHA: &[&str] = &[
"index_of_option", "index_of_option",
"last_index_of_option", "last_index_of_option",
"contains", "contains",
"count_of",
"sum", "sum",
"concat", "concat",
"take", "take",

View File

@ -11,6 +11,11 @@ const EXPECTED_TEST_OUTPUT: &str = concat!(
"test \"explicit local vec_i64 builder helpers\" ... ok\n", "test \"explicit local vec_i64 builder helpers\" ... ok\n",
"test \"explicit local vec_i64 query helpers\" ... ok\n", "test \"explicit local vec_i64 query helpers\" ... ok\n",
"test \"explicit local vec_i64 option query helpers\" ... ok\n", "test \"explicit local vec_i64 option query helpers\" ... ok\n",
"test \"explicit local vec_i64 count_of helper\" ... ok\n",
"test \"explicit local vec_i64 starts_with helper\" ... ok\n",
"test \"explicit local vec_i64 ends_with helper\" ... ok\n",
"test \"explicit local vec_i64 without_suffix helper\" ... ok\n",
"test \"explicit local vec_i64 without_prefix helper\" ... ok\n",
"test \"explicit local vec_i64 transform helpers\" ... ok\n", "test \"explicit local vec_i64 transform helpers\" ... ok\n",
"test \"explicit local vec_i64 subvec helper\" ... ok\n", "test \"explicit local vec_i64 subvec helper\" ... ok\n",
"test \"explicit local vec_i64 insert helper\" ... ok\n", "test \"explicit local vec_i64 insert helper\" ... ok\n",
@ -21,7 +26,7 @@ const EXPECTED_TEST_OUTPUT: &str = concat!(
"test \"explicit local vec_i64 remove range helper\" ... ok\n", "test \"explicit local vec_i64 remove range helper\" ... ok\n",
"test \"explicit local vec_i64 real program helpers\" ... ok\n", "test \"explicit local vec_i64 real program helpers\" ... ok\n",
"test \"explicit local vec_i64 helpers all\" ... ok\n", "test \"explicit local vec_i64 helpers all\" ... ok\n",
"15 test(s) passed\n", "20 test(s) passed\n",
); );
const STANDARD_VEC_I64_SOURCE_FACADE_ALPHA: &[&str] = &[ const STANDARD_VEC_I64_SOURCE_FACADE_ALPHA: &[&str] = &[
@ -44,9 +49,14 @@ const STANDARD_VEC_I64_SOURCE_FACADE_ALPHA: &[&str] = &[
"index_of_option", "index_of_option",
"last_index_of_option", "last_index_of_option",
"contains", "contains",
"count_of",
"sum", "sum",
"concat", "concat",
"take", "take",
"starts_with",
"without_prefix",
"ends_with",
"without_suffix",
"drop", "drop",
"reverse", "reverse",
"subvec", "subvec",
@ -176,6 +186,7 @@ fn assert_local_vec_i64_fixture_is_source_authored(project: &Path) {
"index_of_option_loop", "index_of_option_loop",
"last_index_of_option_loop", "last_index_of_option_loop",
"contains_loop", "contains_loop",
"count_of_loop",
"sum_loop", "sum_loop",
"concat_loop", "concat_loop",
"take_loop", "take_loop",
@ -188,6 +199,43 @@ fn assert_local_vec_i64_fixture_is_source_authored(project: &Path) {
helper helper
); );
} }
assert_count_of_cases_are_exercised(&main);
assert_prefix_suffix_cases_are_exercised(&main);
}
fn assert_count_of_cases_are_exercised(main: &str) {
assert!(
main.contains("(count_of (empty)"),
"main.slo must exercise count_of on an empty vec_i64"
);
assert!(
main.matches("(count_of").count() >= 4,
"main.slo must exercise repeated, singleton, and absent count_of cases"
);
}
fn assert_prefix_suffix_cases_are_exercised(main: &str) {
for case in [
"(starts_with values (empty))",
"(starts_with values values)",
"(ends_with values (empty))",
"(ends_with values values)",
"(without_prefix values (empty))",
"(without_prefix values values)",
"(without_suffix values (empty))",
"(without_suffix values values)",
"mismatched_prefix",
"mismatched_suffix",
"longer_prefix",
"longer_suffix",
] {
assert!(
main.contains(case),
"main.slo must exercise vec_i64 prefix/suffix case `{}`",
case
);
}
} }
fn run_glagol<I, S>(args: I) -> Output fn run_glagol<I, S>(args: I) -> Output

View File

@ -11,6 +11,11 @@ const EXPECTED_STD_VEC_I64_OUTPUT: &str = concat!(
"test \"explicit std vec_i64 builder helpers\" ... ok\n", "test \"explicit std vec_i64 builder helpers\" ... ok\n",
"test \"explicit std vec_i64 query helpers\" ... ok\n", "test \"explicit std vec_i64 query helpers\" ... ok\n",
"test \"explicit std vec_i64 option query helpers\" ... ok\n", "test \"explicit std vec_i64 option query helpers\" ... ok\n",
"test \"explicit std vec_i64 count_of helper\" ... ok\n",
"test \"explicit std vec_i64 starts_with helper\" ... ok\n",
"test \"explicit std vec_i64 ends_with helper\" ... ok\n",
"test \"explicit std vec_i64 without_suffix helper\" ... ok\n",
"test \"explicit std vec_i64 without_prefix helper\" ... ok\n",
"test \"explicit std vec_i64 transform helpers\" ... ok\n", "test \"explicit std vec_i64 transform helpers\" ... ok\n",
"test \"explicit std vec_i64 subvec helper\" ... ok\n", "test \"explicit std vec_i64 subvec helper\" ... ok\n",
"test \"explicit std vec_i64 insert helper\" ... ok\n", "test \"explicit std vec_i64 insert helper\" ... ok\n",
@ -21,7 +26,7 @@ const EXPECTED_STD_VEC_I64_OUTPUT: &str = concat!(
"test \"explicit std vec_i64 remove range helper\" ... ok\n", "test \"explicit std vec_i64 remove range helper\" ... ok\n",
"test \"explicit std vec_i64 real program helpers\" ... ok\n", "test \"explicit std vec_i64 real program helpers\" ... ok\n",
"test \"explicit std vec_i64 helpers all\" ... ok\n", "test \"explicit std vec_i64 helpers all\" ... ok\n",
"15 test(s) passed\n", "20 test(s) passed\n",
); );
const STANDARD_VEC_I64_SOURCE_FACADE_ALPHA: &[&str] = &[ const STANDARD_VEC_I64_SOURCE_FACADE_ALPHA: &[&str] = &[
@ -44,9 +49,14 @@ const STANDARD_VEC_I64_SOURCE_FACADE_ALPHA: &[&str] = &[
"index_of_option", "index_of_option",
"last_index_of_option", "last_index_of_option",
"contains", "contains",
"count_of",
"sum", "sum",
"concat", "concat",
"take", "take",
"starts_with",
"without_prefix",
"ends_with",
"without_suffix",
"drop", "drop",
"reverse", "reverse",
"subvec", "subvec",

View File

@ -0,0 +1,184 @@
use std::{
fs,
path::PathBuf,
process::{Command, Output},
sync::atomic::{AtomicUsize, Ordering},
};
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
#[test]
fn symbols_single_file_reports_editor_facing_source_metadata() {
let source = r#"(module docs (export main))
(type Count i32)
(struct Point
(x i32)
(y i32))
(enum Status Ready (Blocked i32))
(fn main () -> i32
0)
(test "main returns zero"
(= (main) 0))
"#;
let file = write_file("symbols-file", source);
let output = run_glagol(["symbols".as_ref(), file.as_os_str()]);
assert_success("symbols file", &output);
assert!(output.stderr.is_empty(), "symbols wrote stderr");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.starts_with("(symbols\n"));
assert!(stdout.contains(" (schema slovo.symbols)\n"));
assert!(stdout.contains(" (version \"1.0.0-beta.10\")\n"));
assert!(stdout.contains(&format!(" (path \"{}\")\n", file.display())));
assert!(stdout.contains(" (name \"docs\")\n"));
assert!(stdout.contains(" (module_span (span "));
assert!(stdout.contains("(range (start (line 1) (column 1))"));
assert!(stdout.contains(" (name \"main\")\n"));
assert!(stdout.contains(" (name \"Count\")\n"));
assert!(stdout.contains(" (name \"Point\")\n"));
assert!(stdout.contains(" (name \"Status\")\n"));
assert!(stdout.contains(" (name \"Blocked\")\n"));
assert!(stdout.contains(" (name \"main returns zero\")\n"));
}
#[test]
fn symbols_project_writes_output_and_manifest_without_stdout() {
let project = write_project(
"symbols-project",
&[(
"math",
"(module math (export one))\n\n(fn one () -> i32\n 1)\n",
)],
"(module main)\n\n(import math (one))\n\n(fn main () -> i32\n (one))\n",
);
let output_path = unique_path("symbols-project-out").with_extension("sexpr");
let manifest_path = unique_path("symbols-project-manifest").with_extension("slo");
let output = run_glagol([
"symbols".as_ref(),
project.as_os_str(),
"-o".as_ref(),
output_path.as_os_str(),
"--manifest".as_ref(),
manifest_path.as_os_str(),
]);
assert_success("symbols project", &output);
assert!(output.stdout.is_empty(), "symbols -o wrote stdout");
assert!(output.stderr.is_empty(), "symbols project wrote stderr");
let symbols = fs::read_to_string(&output_path).expect("read symbols output");
assert!(symbols.contains(" (name \"main\")\n"));
assert!(symbols.contains(" (name \"math\")\n"));
assert!(symbols.contains(" (module \"math\")\n"));
assert!(symbols.contains(" (name \"one\")\n"));
assert!(symbols.contains(" (imports\n"));
assert!(symbols.contains(" (functions\n"));
let manifest = fs::read_to_string(&manifest_path).expect("read symbols manifest");
assert!(manifest.contains(" (mode symbols)\n"));
assert!(manifest.contains(" (kind symbols)\n"));
assert!(manifest.contains(&format!(" (path \"{}\")\n", output_path.display())));
}
#[test]
fn symbols_workspace_includes_package_names_deterministically() {
let workspace = unique_path("symbols-workspace");
let scaffold = run_glagol([
"new".as_ref(),
workspace.as_os_str(),
"--template".as_ref(),
"workspace".as_ref(),
]);
assert_success("workspace scaffold", &scaffold);
let first = run_glagol(["symbols".as_ref(), workspace.as_os_str()]);
let second = run_glagol(["symbols".as_ref(), workspace.as_os_str()]);
assert_success("symbols workspace first", &first);
assert_success("symbols workspace second", &second);
assert_eq!(
first.stdout, second.stdout,
"workspace symbols output was not deterministic"
);
let stdout = String::from_utf8_lossy(&first.stdout);
assert!(stdout.contains(" (package \"app\")\n"));
assert!(stdout.contains(" (package \"libutil\")\n"));
assert!(stdout.contains(" (module \"libutil.libutil\")\n"));
}
#[test]
fn symbols_usage_is_part_of_the_public_cli_surface() {
let output = run_glagol([std::ffi::OsStr::new("--help")]);
assert_success("glagol help", &output);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("glagol symbols <file.slo|project|workspace>"));
}
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_file(name: &str, source: &str) -> PathBuf {
let path = unique_path(name).with_extension("slo");
fs::write(&path, source).expect("write fixture");
path
}
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-symbols-beta10-{}-{}-{}-{}",
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(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)
);
}

View File

@ -0,0 +1,384 @@
use std::{
fs,
path::PathBuf,
process::{Command, Output},
sync::atomic::{AtomicUsize, Ordering},
time::{SystemTime, UNIX_EPOCH},
};
static NEXT_FIXTURE_ID: AtomicUsize = AtomicUsize::new(0);
#[test]
fn file_list_preserves_order_and_does_not_execute_bodies() {
let fixture = write_fixture(
"file-list",
r#"
(module main)
(test "alpha first"
true)
(test "beta would fail"
false)
"#,
);
let output = run_glagol(["test".as_ref(), "--list".as_ref(), fixture.as_os_str()]);
assert_success_stdout(
"file test list",
output,
"test \"alpha first\" ... selected\n\
test \"beta would fail\" ... selected\n\
2 test(s) selected (total_discovered 2, selected 2, passed 0, failed 0, skipped 0, filter none)\n",
);
}
#[test]
fn file_list_filter_marks_selected_and_skipped_in_order() {
let fixture = write_fixture(
"file-list-filter",
r#"
(module main)
(test "alpha first"
false)
(test "beta second"
false)
"#,
);
let output = run_glagol([
"test".as_ref(),
"--list".as_ref(),
fixture.as_os_str(),
"--filter".as_ref(),
"beta".as_ref(),
]);
assert_success_stdout(
"file filtered test list",
output,
"test \"alpha first\" ... skipped\n\
test \"beta second\" ... selected\n\
1 test(s) selected (total_discovered 2, selected 1, passed 0, failed 0, skipped 1, filter \"beta\")\n",
);
}
#[test]
fn legacy_run_tests_list_matches_test_subcommand_list() {
let fixture = write_fixture(
"legacy-list",
r#"
(module main)
(test "legacy first"
false)
(test "legacy second"
false)
"#,
);
let subcommand = run_glagol(["test".as_ref(), "--list".as_ref(), fixture.as_os_str()]);
let legacy = run_glagol([
"--run-tests".as_ref(),
"--list".as_ref(),
fixture.as_os_str(),
]);
assert_success("test --list", &subcommand);
assert_success("legacy --run-tests --list", &legacy);
assert_eq!(
legacy.stdout, subcommand.stdout,
"legacy list output differed from test subcommand"
);
assert!(legacy.stderr.is_empty(), "legacy list wrote stderr");
}
#[test]
fn project_list_preserves_topological_order_and_filter_counts() {
let project = write_project(
"project-list",
&[(
"provider",
"(module provider (export value))\n\n\
(fn value () -> i32\n 1)\n\n\
(test \"provider first\"\n false)\n",
)],
"(module main)\n\n\
(import provider (value))\n\n\
(fn main () -> i32\n (value))\n\n\
(test \"consumer second\"\n false)\n",
);
let output = run_glagol([
"test".as_ref(),
"--list".as_ref(),
project.as_os_str(),
"--filter".as_ref(),
"consumer".as_ref(),
]);
assert_success_stdout(
"project filtered list",
output,
"test \"provider first\" ... skipped\n\
test \"consumer second\" ... selected\n\
1 test(s) selected (total_discovered 2, selected 1, passed 0, failed 0, skipped 1, filter \"consumer\")\n",
);
}
#[test]
fn workspace_list_preserves_package_order_without_running_tests() {
let workspace = write_workspace(
"workspace-list",
"[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 41)\n\n\
(test \"util provider first\"\n false)\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\n\
(test \"app consumer second\"\n false)\n",
)],
},
],
);
let output = run_glagol(["test".as_ref(), "--list".as_ref(), workspace.as_os_str()]);
assert_success_stdout(
"workspace list",
output,
"test \"util provider first\" ... selected\n\
test \"app consumer second\" ... selected\n\
2 test(s) selected (total_discovered 2, selected 2, passed 0, failed 0, skipped 0, filter none)\n",
);
}
#[test]
fn ordinary_test_output_stays_byte_stable() {
let fixture = write_fixture(
"ordinary-test",
r#"
(module main)
(test "alpha first"
true)
(test "beta second"
true)
"#,
);
let subcommand = run_glagol(["test".as_ref(), fixture.as_os_str()]);
assert_success_stdout(
"ordinary test",
subcommand,
"test \"alpha first\" ... ok\n\
test \"beta second\" ... ok\n\
2 test(s) passed\n",
);
let legacy = run_glagol(["--run-tests".as_ref(), fixture.as_os_str()]);
assert_success_stdout(
"ordinary legacy run-tests",
legacy,
"test \"alpha first\" ... ok\n\
test \"beta second\" ... ok\n\
2 test(s) passed\n",
);
}
#[test]
fn list_mode_reuses_checked_diagnostics_for_invalid_tests() {
let fixture = write_fixture(
"invalid-list",
r#"
(module main)
(test "not bool"
1)
"#,
);
let output = run_glagol(["test".as_ref(), "--list".as_ref(), fixture.as_os_str()]);
assert_failure_stderr_contains(
"invalid list",
&output,
&[
"TestExpressionNotBool",
"Expected:",
"bool",
"Found:",
"i32",
],
);
assert!(
output.stdout.is_empty(),
"invalid list wrote stdout:\n{}",
String::from_utf8_lossy(&output.stdout)
);
}
#[test]
fn list_flag_is_rejected_outside_test_mode() {
let fixture = write_fixture(
"list-outside-test",
r#"
(module main)
(fn main () -> i32
0)
"#,
);
let output = run_glagol(["check".as_ref(), "--list".as_ref(), fixture.as_os_str()]);
assert_failure_stderr_contains(
"check --list",
&output,
&["`--list` is only supported with `test` and `--run-tests`"],
);
}
fn write_fixture(name: &str, source: &str) -> PathBuf {
let path = unique_path(name).with_extension("slo");
fs::write(&path, source).expect("write fixture");
path
}
fn write_project(name: &str, modules: &[(&str, &str)], main: &str) -> PathBuf {
let root = unique_path(name);
let src = root.join("src");
fs::create_dir_all(&src).expect("create project src");
fs::write(
root.join("slovo.toml"),
format!(
"[project]\nname = \"{}\"\nsource_root = \"src\"\nentry = \"main\"\n",
name
),
)
.expect("write project manifest");
fs::write(src.join("main.slo"), main).expect("write project main");
for (module, source) in modules {
fs::write(src.join(format!("{}.slo", module)), source).expect("write project module");
}
root
}
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_FIXTURE_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-test-discovery-beta19-{}-{}-{}-{}",
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(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)
);
}
fn assert_success_stdout(context: &str, output: Output, expected: &str) {
assert_success(context, &output);
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_failure_stderr_contains(context: &str, output: &Output, expected: &[&str]) {
assert!(
!output.status.success(),
"{} unexpectedly succeeded\nstdout:\n{}\nstderr:\n{}",
context,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let stderr = String::from_utf8_lossy(&output.stderr);
for needle in expected {
assert!(
stderr.contains(needle),
"{} stderr missing `{}`:\n{}",
context,
needle,
stderr
);
}
}

View File

@ -0,0 +1,539 @@
use std::{
ffi::OsStr,
fs,
path::{Path, PathBuf},
process::{Command, Output},
sync::atomic::{AtomicUsize, Ordering},
time::{SystemTime, UNIX_EPOCH},
};
static NEXT_TEMP_ID: AtomicUsize = AtomicUsize::new(0);
struct MatrixEntry {
path: &'static str,
expected_tests: usize,
run_tests: bool,
}
const MATRIX: &[MatrixEntry] = &[
MatrixEntry {
path: "examples/projects/basic",
expected_tests: 2,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/enum-imports",
expected_tests: 9,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-import-cli",
expected_tests: 17,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-import-env",
expected_tests: 23,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-import-fs",
expected_tests: 35,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-import-io",
expected_tests: 12,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-import-json",
expected_tests: 12,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-import-math",
expected_tests: 4,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-import-net",
expected_tests: 9,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-import-num",
expected_tests: 5,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-import-option",
expected_tests: 36,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-import-process",
expected_tests: 21,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-import-random",
expected_tests: 2,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-import-result",
expected_tests: 26,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-import-string",
expected_tests: 12,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-import-time",
expected_tests: 3,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-import-vec_bool",
expected_tests: 19,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-import-vec_f64",
expected_tests: 20,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-import-vec_i32",
expected_tests: 22,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-import-vec_i64",
expected_tests: 20,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-import-vec_string",
expected_tests: 19,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-layout-local-cli",
expected_tests: 17,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-layout-local-env",
expected_tests: 23,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-layout-local-fs",
expected_tests: 35,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-layout-local-io",
expected_tests: 12,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-layout-local-json",
expected_tests: 12,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-layout-local-math",
expected_tests: 7,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-layout-local-net",
expected_tests: 9,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-layout-local-num",
expected_tests: 5,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-layout-local-option",
expected_tests: 36,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-layout-local-process",
expected_tests: 21,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-layout-local-random",
expected_tests: 3,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-layout-local-result",
expected_tests: 26,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-layout-local-string",
expected_tests: 12,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-layout-local-time",
expected_tests: 3,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-layout-local-vec_bool",
expected_tests: 19,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-layout-local-vec_f64",
expected_tests: 20,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-layout-local-vec_i32",
expected_tests: 22,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-layout-local-vec_i64",
expected_tests: 20,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/std-layout-local-vec_string",
expected_tests: 19,
run_tests: true,
},
MatrixEntry {
path: "examples/projects/stdlib-composition",
expected_tests: 3,
run_tests: true,
},
MatrixEntry {
path: "examples/workspaces/exp-5-local",
expected_tests: 2,
run_tests: true,
},
MatrixEntry {
path: "examples/workspaces/std-import-option",
expected_tests: 1,
run_tests: true,
},
];
#[test]
fn user_project_conformance_matrix_checks_lists_and_runs_stable_tests() {
assert_matrix_inventory();
let repo_root = repo_root();
for entry in MATRIX {
let fixture = repo_root.join(entry.path);
assert!(
fixture.join("slovo.toml").is_file(),
"{} must remain an existing project or workspace fixture",
entry.path
);
let check = run_glagol(
[OsStr::new("check"), fixture.as_os_str()],
&temp_root(entry.path, "check"),
);
assert_success_no_stderr(entry.path, "check", &check);
assert_stdout_eq(entry.path, "check", &check, "");
let list = run_glagol(
[
OsStr::new("test"),
OsStr::new("--list"),
fixture.as_os_str(),
],
&temp_root(entry.path, "list"),
);
assert_success_no_stderr(entry.path, "test --list", &list);
assert_stdout_contains(
entry.path,
"test --list",
&list,
&format!(
"{} test(s) selected (total_discovered {}, selected {}, passed 0, failed 0, skipped 0, filter none)",
entry.expected_tests, entry.expected_tests, entry.expected_tests
),
);
if entry.run_tests {
let test = run_glagol(
[OsStr::new("test"), fixture.as_os_str()],
&temp_root(entry.path, "test"),
);
assert_success_no_stderr(entry.path, "test", &test);
assert_stdout_contains(
entry.path,
"test",
&test,
&format!("{} test(s) passed", entry.expected_tests),
);
}
}
}
fn assert_matrix_inventory() {
assert_eq!(MATRIX.len(), 43, "beta25 fixture-root count changed");
assert_eq!(
MATRIX
.iter()
.map(|entry| entry.expected_tests)
.sum::<usize>(),
655,
"beta25 discovered-test count changed"
);
let matrix_paths = MATRIX
.iter()
.map(|entry| entry.path.to_owned())
.collect::<Vec<_>>();
let mut sorted_matrix_paths = matrix_paths.clone();
sorted_matrix_paths.sort();
assert_eq!(
matrix_paths, sorted_matrix_paths,
"beta25 user-project conformance matrix must remain sorted by repository-relative path"
);
let discovered_paths = discover_fixture_paths(&repo_root());
assert_eq!(
discovered_paths, matrix_paths,
"beta25 user-project conformance matrix must cover every top-level example project and workspace fixture"
);
let inventory = MATRIX
.iter()
.map(|entry| {
format!(
"{}|tests={}|run_tests={}",
entry.path, entry.expected_tests, entry.run_tests
)
})
.collect::<Vec<_>>()
.join("\n");
assert_eq!(
inventory,
concat!(
"examples/projects/basic|tests=2|run_tests=true\n",
"examples/projects/enum-imports|tests=9|run_tests=true\n",
"examples/projects/std-import-cli|tests=17|run_tests=true\n",
"examples/projects/std-import-env|tests=23|run_tests=true\n",
"examples/projects/std-import-fs|tests=35|run_tests=true\n",
"examples/projects/std-import-io|tests=12|run_tests=true\n",
"examples/projects/std-import-json|tests=12|run_tests=true\n",
"examples/projects/std-import-math|tests=4|run_tests=true\n",
"examples/projects/std-import-net|tests=9|run_tests=true\n",
"examples/projects/std-import-num|tests=5|run_tests=true\n",
"examples/projects/std-import-option|tests=36|run_tests=true\n",
"examples/projects/std-import-process|tests=21|run_tests=true\n",
"examples/projects/std-import-random|tests=2|run_tests=true\n",
"examples/projects/std-import-result|tests=26|run_tests=true\n",
"examples/projects/std-import-string|tests=12|run_tests=true\n",
"examples/projects/std-import-time|tests=3|run_tests=true\n",
"examples/projects/std-import-vec_bool|tests=19|run_tests=true\n",
"examples/projects/std-import-vec_f64|tests=20|run_tests=true\n",
"examples/projects/std-import-vec_i32|tests=22|run_tests=true\n",
"examples/projects/std-import-vec_i64|tests=20|run_tests=true\n",
"examples/projects/std-import-vec_string|tests=19|run_tests=true\n",
"examples/projects/std-layout-local-cli|tests=17|run_tests=true\n",
"examples/projects/std-layout-local-env|tests=23|run_tests=true\n",
"examples/projects/std-layout-local-fs|tests=35|run_tests=true\n",
"examples/projects/std-layout-local-io|tests=12|run_tests=true\n",
"examples/projects/std-layout-local-json|tests=12|run_tests=true\n",
"examples/projects/std-layout-local-math|tests=7|run_tests=true\n",
"examples/projects/std-layout-local-net|tests=9|run_tests=true\n",
"examples/projects/std-layout-local-num|tests=5|run_tests=true\n",
"examples/projects/std-layout-local-option|tests=36|run_tests=true\n",
"examples/projects/std-layout-local-process|tests=21|run_tests=true\n",
"examples/projects/std-layout-local-random|tests=3|run_tests=true\n",
"examples/projects/std-layout-local-result|tests=26|run_tests=true\n",
"examples/projects/std-layout-local-string|tests=12|run_tests=true\n",
"examples/projects/std-layout-local-time|tests=3|run_tests=true\n",
"examples/projects/std-layout-local-vec_bool|tests=19|run_tests=true\n",
"examples/projects/std-layout-local-vec_f64|tests=20|run_tests=true\n",
"examples/projects/std-layout-local-vec_i32|tests=22|run_tests=true\n",
"examples/projects/std-layout-local-vec_i64|tests=20|run_tests=true\n",
"examples/projects/std-layout-local-vec_string|tests=19|run_tests=true\n",
"examples/projects/stdlib-composition|tests=3|run_tests=true\n",
"examples/workspaces/exp-5-local|tests=2|run_tests=true\n",
"examples/workspaces/std-import-option|tests=1|run_tests=true",
),
"beta25 user-project conformance matrix changed without updating the inventory assertion"
);
}
fn discover_fixture_paths(repo_root: &Path) -> Vec<String> {
let mut paths = Vec::new();
for parent in ["examples/projects", "examples/workspaces"] {
let parent_path = repo_root.join(parent);
for entry in fs::read_dir(&parent_path).unwrap_or_else(|err| {
panic!(
"read fixture inventory `{}`: {}",
parent_path.display(),
err
);
}) {
let entry = entry.unwrap_or_else(|err| {
panic!(
"read fixture entry under `{}`: {}",
parent_path.display(),
err
);
});
let path = entry.path();
if path.join("slovo.toml").is_file() {
paths.push(
path.strip_prefix(repo_root)
.expect("fixture path under repository root")
.to_string_lossy()
.replace('\\', "/"),
);
}
}
}
paths.sort();
paths
}
fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("compiler crate has repository parent")
.to_path_buf()
}
fn run_glagol<I, S>(args: I, cwd: &Path) -> Output
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
fs::create_dir_all(cwd).unwrap_or_else(|err| {
panic!("create command cwd `{}`: {}", cwd.display(), err);
});
let mut command = Command::new(env!("CARGO_BIN_EXE_glagol"));
configure_conformance_env(&mut command);
command
.args(args)
.current_dir(cwd)
.output()
.expect("run glagol")
}
fn configure_conformance_env(command: &mut Command) {
for key in [
"GLAGOL_STD_IMPORT_ENV_ALPHA_UNLIKELY_MISSING",
"GLAGOL_STD_LAYOUT_LOCAL_ENV_ALPHA_UNLIKELY_MISSING",
] {
command.env_remove(key);
}
for (key, value) in [
(
"GLAGOL_STD_IMPORT_ENV_ALPHA_PRESENT",
"glagol-std-import-env-alpha-value",
),
("GLAGOL_STD_IMPORT_ENV_ALPHA_PRESENT_I32", "42"),
("GLAGOL_STD_IMPORT_ENV_ALPHA_PRESENT_I64", "42000000000"),
("GLAGOL_STD_IMPORT_ENV_ALPHA_PRESENT_U32", "42"),
("GLAGOL_STD_IMPORT_ENV_ALPHA_PRESENT_U64", "4294967296"),
("GLAGOL_STD_IMPORT_ENV_ALPHA_PRESENT_F64", "42.5"),
("GLAGOL_STD_IMPORT_ENV_ALPHA_PRESENT_BOOL", "true"),
("GLAGOL_STD_IMPORT_ENV_ALPHA_INVALID", "not-a-number"),
(
"GLAGOL_STD_LAYOUT_LOCAL_ENV_ALPHA_PRESENT",
"glagol-std-layout-local-env-alpha-value",
),
("GLAGOL_STD_LAYOUT_LOCAL_ENV_ALPHA_PRESENT_I32", "42"),
(
"GLAGOL_STD_LAYOUT_LOCAL_ENV_ALPHA_PRESENT_I64",
"42000000000",
),
("GLAGOL_STD_LAYOUT_LOCAL_ENV_ALPHA_PRESENT_U32", "42"),
(
"GLAGOL_STD_LAYOUT_LOCAL_ENV_ALPHA_PRESENT_U64",
"4294967296",
),
("GLAGOL_STD_LAYOUT_LOCAL_ENV_ALPHA_PRESENT_F64", "42.5"),
("GLAGOL_STD_LAYOUT_LOCAL_ENV_ALPHA_PRESENT_BOOL", "true"),
("GLAGOL_STD_LAYOUT_LOCAL_ENV_ALPHA_INVALID", "not-a-number"),
] {
command.env(key, value);
}
}
fn temp_root(path: &str, command: &str) -> PathBuf {
let id = NEXT_TEMP_ID.fetch_add(1, Ordering::Relaxed);
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock before UNIX_EPOCH")
.as_nanos();
let fixture = path
.chars()
.map(|ch| if ch == '/' || ch == '-' { '_' } else { ch })
.collect::<String>();
std::env::temp_dir().join(format!(
"glagol-user-project-conformance-beta25-{}-{}-{}-{}-{}",
std::process::id(),
nanos,
id,
fixture,
command
))
}
fn assert_success_no_stderr(fixture: &str, command: &str, output: &Output) {
assert!(
output.status.success(),
"{} `{}` failed\nstatus: {:?}\nstdout:\n{}\nstderr:\n{}",
fixture,
command,
output.status.code(),
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(
output.stderr.is_empty(),
"{} `{}` wrote stderr:\n{}",
fixture,
command,
String::from_utf8_lossy(&output.stderr)
);
}
fn assert_stdout_eq(fixture: &str, command: &str, output: &Output, expected: &str) {
assert_eq!(
String::from_utf8_lossy(&output.stdout),
expected,
"{} `{}` stdout mismatch",
fixture,
command
);
}
fn assert_stdout_contains(fixture: &str, command: &str, output: &Output, needle: &str) {
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains(needle),
"{} `{}` stdout did not contain `{}`:\n{}",
fixture,
command,
needle,
stdout
);
}

View File

@ -41,6 +41,30 @@ Released in `1.0.0-beta.1`: `glagol run`, `glagol clean`,
installed std/runtime discovery, README coverage, focused DX tests, and a installed std/runtime discovery, README coverage, focused DX tests, and a
concise release-gate success line. concise release-gate success line.
Released in `1.0.0-beta.19`: test discovery and user-project conformance
tooling. The scope adds the
`glagol test --list <file|project|workspace>` command and legacy
`glagol --run-tests --list <file>` so users and tooling can list
checked/discovered tests without executing test bodies. It preserves existing
single-file, project, and workspace ordering, honors
`--filter <substring>`, and keeps the output beta-scoped rather than a stable
public schema.
Beta19 non-scope: no parallel test execution, retries, tags/groups, coverage
reports, event streams, stable artifact-manifest or Markdown schema freeze,
LSP/watch behavior, SARIF/daemon protocols, JSON expansion, runtime helper
names, source-language syntax, package registries, semver solving, or
performance claims.
Released in `1.0.0-beta.25`: user-project conformance matrix evidence. The
scope adds a deterministic matrix over the existing `examples/projects/` and
`examples/workspaces/` inventories so ordinary project and workspace usage has
stable-readiness evidence across all 43 top-level fixture roots and 655
discovered tests. It is tooling/conformance evidence only: no
source-language change, standard-library helper change, runtime behavior
change, package manager or registry behavior, lockfile behavior,
semantic-version solving, stable schema freeze, or performance claim.
Why first: it reduces friction for every later feature and gives users a better Why first: it reduces friction for every later feature and gives users a better
way to exercise the beta. way to exercise the beta.
@ -93,6 +117,29 @@ generated from `lib/std/*.slo` and guarded by `scripts/release-gate.sh`.
that composes `std.fs`, `std.string`, `std.math`, and `std.io` through explicit that composes `std.fs`, `std.string`, `std.math`, and `std.io` through explicit
standard imports, with focused check/test/doc/run coverage. standard imports, with focused check/test/doc/run coverage.
Released in `1.0.0-beta.20`: source-authored `std.string` search and ASCII
trim helpers. The scope adds `contains`, `index_of_option`,
`last_index_of_option`, `trim_ascii_start`, `trim_ascii_end`, and
`trim_ascii` over existing byte-oriented string primitives, with explicit
`std.string` import examples and focused compiler gates. Empty needles match
at first index `0` and last index `(len value)`; ASCII trimming removes only
bytes `9`, `10`, `11`, `12`, `13`, and `32`. Unicode/grapheme semantics,
case folding, regexes, tokenizers, mutable strings, slice/view syntax, new
runtime names, and stable stdlib/API promises remain deferred.
Released in `1.0.0-beta.23`: the public
[`docs/language/STDLIB_TIERS.md`](language/STDLIB_TIERS.md) ledger defines the
current standard-library tier labels `beta-supported`, `experimental`, and
`internal`, and aligns the docs around the generated
[`docs/language/STDLIB_API.md`](language/STDLIB_API.md) signature catalog.
JSON, loopback networking, random/time, and filesystem resource-handle helpers
are documented as experimental domains. Concrete vector modules remain
beta-supported concrete lanes, not a generic collections freeze. The slice is
documentation/catalog tooling clarity only: no syntax, helper, runtime,
manifest-schema, Markdown-schema, ABI/layout, or stable stdlib/API behavior
changes. It updates generated catalog output and the release gate so tier
metadata is visible and checked.
Why third: stdlib growth is already broad enough that naming and stability tiers Why third: stdlib growth is already broad enough that naming and stability tiers
matter more than adding another isolated helper group. matter more than adding another isolated helper group.
@ -105,7 +152,8 @@ Candidate features:
- `glagol run`-friendly `main` conventions and clearer entry-point diagnostics - `glagol run`-friendly `main` conventions and clearer entry-point diagnostics
- better `match` diagnostics and exhaustiveness checks where the current enum - better `match` diagnostics and exhaustiveness checks where the current enum
model allows it model allows it
- type aliases for long concrete types such as vectors, options, and results - concrete type aliases for long concrete types such as vectors, options, and
results
- destructuring for simple structs and enum payloads - destructuring for simple structs and enum payloads
- additional numeric completeness such as `f32`, only if it can be tested across - additional numeric completeness such as `f32`, only if it can be tested across
checker, formatter, runtime, and docs checker, formatter, runtime, and docs
@ -117,6 +165,8 @@ Released in `1.0.0-beta.4`: project/workspace build and run entry
diagnostics now use entry-specific codes and explicitly show the required diagnostics now use entry-specific codes and explicitly show the required
`(fn main () -> i32 ...)` contract. Non-exhaustive `match` diagnostics now use `(fn main () -> i32 ...)` contract. Non-exhaustive `match` diagnostics now use
clearer missing-arm wording and deterministic found-arm output. clearer missing-arm wording and deterministic found-arm output.
Concrete aliases were split into the follow-up `1.0.0-beta.8` language slice so
the syntax, formatter, diagnostics, and source fixtures could be gated directly.
### 5. Package And Workspace Discipline ### 5. Package And Workspace Discipline
@ -141,6 +191,14 @@ local-package rules. Lockfiles, remote registries, semver solving, publishing,
optional/dev/target dependencies, and stable package ABI/layout remain out of optional/dev/target dependencies, and stable package ABI/layout remain out of
scope. scope.
Released in `1.0.0-beta.24`: package manifest identity and dependency
diagnostics are tightened without changing the package model. Duplicate
package manifest keys, invalid dependency keys, and duplicate dependency keys
are explicit diagnostics. The slice adds no remote registry, lockfile,
semantic-version solving, package publishing, optional/dev/target
dependencies, stable package ABI/layout, source-language change, runtime
change, or standard-library change.
Why fifth: stable package rules are a prerequisite for a usable public language, Why fifth: stable package rules are a prerequisite for a usable public language,
but remote publishing can wait. but remote publishing can wait.
@ -185,58 +243,345 @@ Released in `1.0.0-beta.7`: `lib/std/json.slo` now provides explicit helpers
for compact JSON text construction over strings, booleans, numbers, null, for compact JSON text construction over strings, booleans, numbers, null,
fields, small arrays, and small objects. `std.json.quote_string` is a fields, small arrays, and small objects. `std.json.quote_string` is a
compiler-known runtime helper so JSON string escaping is correct before Slovo compiler-known runtime helper so JSON string escaping is correct before Slovo
has source-level byte/character scanning and slicing. Matching explicit has later byte-oriented string scanning helpers. Matching explicit
std/local source fixtures and a `json-quote-loop` benchmark scaffold are in std/local source fixtures and a `json-quote-loop` benchmark scaffold are in
place. JSON parsing, recursive JSON values, maps/sets, generic collections, place.
streaming encoders, schema validation, Unicode normalization, and stable text
encoding policy beyond the current runtime string ABI remain deferred. Released in `1.0.0-beta.17`: `lib/std/json.slo` now provides primitive scalar
JSON token parse facades for booleans, concrete numeric primitives, and exact
`null`. Broader JSON parsing beyond primitive scalar tokens remained deferred
for the next slices.
Released in `1.0.0-beta.18`: `lib/std/json.slo` adds
`parse_string_value_result` for one already-isolated ASCII JSON string token.
It requires exact quotes, rejects leading/trailing whitespace, decodes the
simple JSON escapes `\"`, `\\`, `\/`, `\b`, `\f`, `\n`, `\r`, and `\t`, and
returns `err 1` for ordinary parse failure. Full JSON document parsing,
object/array parsing, tokenizer objects, Unicode escape decoding or
normalization, embedded NUL policy, streaming, schema validation, and stable
ABI/API guarantees remain deferred.
Released in `1.0.0-beta.21`: `lib/std/json.slo` adds source-authored scalar
document parse facades for string, bool, `i32`, `u32`, `i64`, `u64`, `f64`,
and exact `null`. Each helper trims ASCII whitespace around the whole document
with `std.string.trim_ascii`, then delegates to the already released exact
value-token parser. This intentionally remains scalar document parsing only:
object/array parsing, recursive JSON values, parser/tokenizer objects,
maps/sets, streaming, new compiler-known runtime names, broader Unicode escape
policy, embedded NUL policy, and stable ABI/API guarantees remain deferred.
Why seventh: networking and CLI tools need data interchange, but a complete JSON Why seventh: networking and CLI tools need data interchange, but a complete JSON
library depends on collection work. library depends on collection work.
### 8. Generics And Collection Unification ### 8. Concrete Type Alias Foundation
Goal: stop duplicating every helper across concrete vector, option, and result Goal: reduce concrete type repetition without introducing generics or changing
families. runtime representation.
Work: Work:
- design generic function syntax and typed-core representation - add top-level `(type Alias TargetType)` declarations for aliases whose targets
- gate generic stdlib helpers behind conformance tests are already supported concrete Slovo types
- migrate repeated concrete helpers only after compatibility policy is written - resolve aliases before typed-core lowering, checked import signatures, backend
- add generic arrays/vectors first, then maps and sets layout, ABI decisions, and runtime behavior
- keep stable ABI/layout promises deferred until after real beta use - keep aliases module-local: no alias exports, imports, re-exports, or
cross-module alias visibility
- update formatter and diagnostics for malformed, duplicate, unsupported,
cyclic, exported, or imported aliases
- exercise aliases sparingly in JSON source facades and explicit source
fixtures without adding compiler-known runtime names
Why eighth: generics are important, but they should be introduced after the Released in `1.0.0-beta.8`: concrete aliases such as `(type JsonText string)`
are transparent names for existing concrete types. The compiler parses,
formats, checks, lowers, and erases aliases before backend behavior, while
project imports of functions that used local aliases see concrete target
types. Alias export/import attempts and unsupported targets are diagnostics.
Generic aliases, parameterized aliases, aliasing maps/sets, stable ABI/layout
names, and runtime changes remain deferred.
Why eighth: concrete aliases remove real noise from current stdlib and fixture
code while deliberately postponing generic type parameters until the compiler
and standard library have stronger design pressure.
### 9. Collection Alias Unification And Generic Reservation
Goal: apply concrete aliases to the existing collection/value facades and
reserve the generic/map/set direction without changing executable semantics.
Work:
- use beta.8 concrete aliases sparingly inside current vector, option, and
result source facades to reduce repeated long concrete types
- preserve all public helper names, exports, imports, runtime calls, and
concrete cross-module signatures
- document that current vectors/options/results are still concrete families
and that local aliases erase before lowering
- reserve executable generics, maps, sets, traits, inference,
monomorphization, iterators, and ABI stability for later gated releases
Released in `1.0.0-beta.9`: the concrete `std.vec_i32`, `std.vec_i64`,
`std.vec_f64`, `std.vec_bool`, `std.vec_string`, `std.option`, and
`std.result` facades now use module-local transparent aliases internally.
The exported helper surface remains concrete after alias normalization.
The release does not implement executable generics, maps, sets, traits,
inference, monomorphization, iterators, or stable ABI/layout promises.
Why ninth: generics are important, but they should be introduced after the
compiler, docs, tests, and stdlib have enough real pressure to validate the compiler, docs, tests, and stdlib have enough real pressure to validate the
design. design.
### 9. Developer Experience ### 10. Developer Experience API Discovery
Goal: make Slovo comfortable for repeated daily use. Goal: make Slovo comfortable for repeated daily use by making the current
standard-library API surface easier to inspect before deeper editor work.
Work: Work:
- upgrade the generated `lib/std` API catalog from exported names to exact
exported helper signatures
- normalize module-local beta.8/beta.9 concrete aliases in public signatures
so local aliases do not leak into docs
- validate that exported helpers have matching `(fn ...)` forms
- keep non-exported helpers and `(type ...)` aliases out of the public catalog
- language-server diagnostics and document symbols - language-server diagnostics and document symbols
- editor-facing symbol metadata for files, projects, and workspaces
- project watch mode - project watch mode
- generated API documentation for local packages and `lib/std`
- clearer benchmark harness output - clearer benchmark harness output
- machine-readable diagnostics stability policy - machine-readable diagnostics stability policy
Why ninth: editor support matters, but it should follow a stable enough syntax, Released in `1.0.0-beta.10`: `docs/language/STDLIB_API.md` now lists exact
exported helper signatures from `lib/std/*.slo`, and the renderer validates
exported helper forms while normalizing module-local aliases to concrete
public types. `glagol symbols <file.slo|project|workspace>` now emits
deterministic `slovo.symbols` metadata for editor integrations without
starting an LSP server. This is beta API discovery only; it does not add
executable generics, maps, sets, runtime changes, or a stable standard-library
API freeze. LSP, watch mode, benchmark-output work, stable Markdown schema,
stable stdlib/API compatibility freeze, SARIF/daemon protocols, and a
machine-readable diagnostics stability policy remain deferred.
Why tenth: editor support matters, but it should follow a stable enough syntax,
project model, and diagnostic model. project model, and diagnostic model.
### 11. Local Package API Documentation
Goal: extend beta API discovery from `lib/std` and symbol metadata to the
local packages and modules users document with `glagol doc`.
Work:
- make `glagol doc <file|project|workspace> -o <dir>` include deterministic
exported/public API sections for local packages and modules
- list exact exported function signatures
- list exported struct fields
- list exported enum variants and payload types
- keep non-exported functions, structs, enums, tests, and aliases out of the
public API sections
- normalize module-local concrete aliases in public docs so local alias names
do not leak across module/package boundaries
- keep Markdown layout and generated file names beta-scoped rather than stable
Released in `1.0.0-beta.11`: local file, project, package, and workspace docs
generated by `glagol doc <file|project|workspace> -o <dir>` include
deterministic public API sections with exact exported function signatures,
exported struct fields, exported enum variants/payload types, non-export
filtering, and module-local alias normalization. This extends beta10 API
discovery only; it does not freeze the Markdown schema, create a stable
stdlib/API compatibility freeze, add LSP/watch, define SARIF/daemon protocols,
set a diagnostics schema policy, implement executable generics/maps/sets, add
re-exports/globs/hierarchical modules, or define registry semantics.
Why eleventh: local packages are useful only if their public surface can be
reviewed without reading every source file, but the documentation format
should remain flexible until the package and editor stories are stronger.
### 12. Concrete Vector Query And Prefix Parity
Goal: close small source-authored helper gaps in the current concrete vector
facades before returning to larger language and tooling slices.
Work:
- add `count_of`, `starts_with`, `without_prefix`, `ends_with`, and
`without_suffix` to `std.vec_i64`
- add `count_of` to `std.vec_f64`
- keep all helpers source-authored over the already promoted concrete vector
runtime names, equality, `len`, `at`, `take`, `drop`, and recursive helper
style
- add explicit local helper project coverage for repeated count results and
prefix/suffix empty, mismatch, exact, and longer-than-input cases where
applicable
- document the slice as helper parity only, not a language/runtime change
Released in `1.0.0-beta.12`: `std.vec_i64` gains `count_of`, `starts_with`,
`without_prefix`, `ends_with`, and `without_suffix`; `std.vec_f64` gains
`count_of`; and focused Glagol fixture tests require the corresponding explicit
source-helper coverage. The release does not add generics, maps/sets,
iterators, mutable vectors, slice/view APIs, new runtime names, ABI/layout
stability, performance claims, or a stable stdlib/API freeze.
Why twelfth: concrete vectors are already broad enough that parity gaps create
surprising differences, and source-authored helpers can close those gaps
without committing to generic collection design.
### 13. Diagnostic Catalog And Schema Policy
Goal: document the existing diagnostic machine contract before larger tooling
or editor-facing slices depend on it.
Work:
- add [`docs/language/DIAGNOSTICS.md`](language/DIAGNOSTICS.md) as the beta
`slovo.diagnostic` version `1` policy
- document the S-expression and JSON encodings, required and optional fields,
severity/source/range/related-span semantics, JSON-line discipline,
source-less diagnostics, and artifact-manifest diagnostic metadata
- classify diagnostic changes as clarifying, additive, or migration-level,
with human prose remaining beta-flexible unless machine fields, schema
markers, codes, or golden fixture shape change intentionally
- inventory the current diagnostic codes covered by
`compiler/tests/diagnostics_contract.rs` and the matching `.diag` snapshots
- keep LSP/watch, SARIF, daemon protocols, stable Markdown schema, stable
`1.0.0` diagnostics freeze, and runtime/source-language changes out of scope
Released in `1.0.0-beta.13`:
[`docs/language/DIAGNOSTICS.md`](language/DIAGNOSTICS.md) now defines the beta
diagnostic schema policy and catalogs the current golden diagnostic codes. The
release is documentation/tooling policy only; it does not change Glagol
diagnostic output, the source language, runtime, stdlib/API surface, or
ABI/layout behavior.
Why thirteenth: diagnostics already have a machine schema and broad golden
coverage, but the compatibility policy and current code inventory need one
central reference before future tooling or migration work builds on them.
### 14. Benchmark Suite Catalog And Metadata Gate
Goal: document the existing benchmark suite inventory and metadata listing
commands before benchmark tooling grows additional consumers.
Work:
- add [`benchmarks/README.md`](../benchmarks/README.md) as the top-level
benchmark suite catalog
- document `python3 benchmarks/runner.py --suite-list` for the non-JSON suite
inventory
- document `python3 benchmarks/runner.py --suite-list --json` for beta tooling
metadata
- list the current suite inventory without adding new benchmark kernels
- state that benchmark timings are local-machine evidence only
- keep suite-list JSON beta-scoped rather than a stable public schema
- keep timing publication, performance thresholds, source-language/runtime/
stdlib/API/diagnostic-output changes, and ABI/layout changes out of scope
Released in `1.0.0-beta.14`:
[`benchmarks/README.md`](../benchmarks/README.md) now catalogs the current
benchmark suite, documents the root suite-list commands, records local-machine
evidence policy, and names the explicit exclusions. The release is
documentation/tooling metadata only; it does not add kernels, publish timing
numbers, define performance thresholds, define a stable JSON schema, or change
the source language, runtime, stdlib/API surface, diagnostics, or ABI/layout
behavior.
Why fourteenth: the benchmark suite is already part of the public monorepo, but
its suite-level inventory and metadata boundary need one central reference
before future tooling can rely on it.
### 15. Reserved Generic Collection Boundary Hardening And Collection Ledger
Goal: document the current concrete collection and value-family boundary before
executable generics, maps, sets, iterators, mutable vectors, or slice/view APIs
are designed as executable features.
Work:
- add [`docs/language/COLLECTIONS.md`](language/COLLECTIONS.md) as the
collection/value-family ledger
- link to the generated [`docs/language/STDLIB_API.md`](language/STDLIB_API.md)
catalog for exact public helper signatures instead of duplicating generated
counts
- inventory the current concrete vector, option, result, and related
option/result-returning facade surfaces
- record design pressure from duplicated concrete vector, option, and result
helper families
- define prerequisites before executable generics, generic aliases, maps,
sets, iterators, mutable vectors, or slice/view APIs can be promoted
- state that current unsupported diagnostics are boundaries, not behavior
changes
- centralize reserved generic/map/set diagnostics and reword affected
reserved-boundary messages from beta.9-specific text to current-beta wording
- keep source-language/runtime/stdlib/API changes, diagnostic output shape/
code/schema/span/expected/found/hint changes, benchmark metadata schema
changes, ABI/layout changes, performance claims, and stable API freeze out
of scope
Released in `1.0.0-beta.15`:
[`docs/language/COLLECTIONS.md`](language/COLLECTIONS.md) now records the
collection/value-family ledger and links to the generated standard-library API
catalog for exact signatures. The release also centralizes reserved
generic/map/set diagnostics and rewords affected reserved-boundary messages
away from beta.9-specific text. It does not change the source language,
runtime, stdlib/API surface, diagnostic output shape, diagnostic codes,
diagnostic schema, spans, expected/found values, hints, benchmark metadata
schema, ABI/layout behavior, or performance claims, and it does not create a
stable stdlib/API freeze.
Why fifteenth: the concrete vector, option, and result facades have enough
duplication to justify generic collection planning, but the public contract
needs an explicit boundary ledger before executable generic, map, set,
iterator, mutable-vector, or slice/view semantics are promoted.
### 16. String Scanning And Token Boundary Foundation
Goal: add the first byte-oriented string scanning and token-boundary helpers
without promoting Unicode/grapheme semantics, full JSON parsing, or language
slice/view syntax.
Work:
- add `byte_at_result`, `slice_result`, `starts_with`, and `ends_with` to
`lib/std/string.slo`
- mirror those source facades in the explicit local `std-layout-local-string`
fixture
- cover both local and explicit `std.string` imports with examples for success
and ordinary failure cases
- document byte-oriented behavior over current NUL-terminated runtime strings
- require invalid indexes and ranges to return `err 1`
- keep allocation failure on substring creation aligned with the existing
string allocation trap policy
- keep Unicode scalar/grapheme/display-width semantics, full JSON parsing,
object/array parsing, tokenizer objects, language slice/view features,
mutable strings, stable ABI/layout, and stable API freeze out of scope
Released in `1.0.0-beta.16`: `std.string` now exposes source facades and
examples for byte access, substring extraction, and prefix/suffix checks:
`byte_at_result`, `slice_result`, `starts_with`, and `ends_with`. The release is
byte-oriented over the current runtime string representation and uses `err 1`
for ordinary invalid indexes/ranges. It does not add Unicode/grapheme
semantics, full JSON parsing, object/array parsing, tokenizer objects,
language slice/view features, mutable strings, stable ABI/layout, or a stable
stdlib/API freeze.
Why sixteenth: JSON text construction and process/file/network helpers can
produce useful strings, but parsers and tokenizers need a smaller byte-boundary
foundation before richer string or data-interchange APIs are credible.
## Stable `1.0.0` Gate ## Stable `1.0.0` Gate
Slovo should not become stable until all of these are true: Slovo should not become stable until all of these are true:
- migration and deprecation policy is documented - migration and deprecation policy is documented
- `lib/std` has explicit stable and experimental tiers - `lib/std` has explicit beta-supported, experimental, and internal tiers plus
a later stable-tier/deprecation policy before `1.0.0`
- package/workspace behavior is deterministic - package/workspace behavior is deterministic
- conformance tests cover user-shaped projects - package manifest identity and dependency-key failures have explicit
diagnostics
- conformance tests and matrix evidence cover user-shaped projects and
workspaces
- release gates are reproducible on a clean checkout - release gates are reproducible on a clean checkout
- diagnostics and formatter output are stable for promoted features - diagnostics and formatter output are stable for promoted features
- performance publications are repeatable and labeled as local-machine evidence - performance publications are repeatable and labeled as local-machine evidence
- benchmark metadata promoted to stable, if any, has an explicit schema policy
- the language can build useful local CLI tools, libraries, file-processing - the language can build useful local CLI tools, libraries, file-processing
programs, and basic host-interaction programs without undocumented behavior programs, and basic host-interaction programs without undocumented behavior
@ -251,5 +596,9 @@ complete first:
- macro system - macro system
- stable C ABI/layout guarantees - stable C ABI/layout guarantees
- optimizing compiler claims - optimizing compiler claims
- mutable vectors, slice/view APIs, iterators, maps, sets, and executable
generics
- new runtime helper names or generic stdlib dispatch before an explicit
runtime/language slice
- web framework or HTTP server framework - web framework or HTTP server framework
- broad Unicode/string normalization policy - broad Unicode/string normalization policy

View File

@ -10,7 +10,607 @@ integration/readiness release, not the first real beta.
## Unreleased ## Unreleased
No unreleased changes yet. No active unreleased compiler scope is documented here yet.
## 1.0.0-beta.25
Release label: `1.0.0-beta.25`
Release date: 2026-05-23
Release state: user-project conformance matrix evidence
### Summary
The beta.25 compiler/tooling slice adds deterministic stable-readiness
evidence for ordinary project and workspace usage without changing the
language, runtime, stdlib, or package manager model:
- Add a user-project conformance matrix over all 43 top-level fixture roots
under existing `examples/projects/` and `examples/workspaces/`, covering
655 discovered tests.
- Keep matrix inputs repository-local and deterministically sorted by
repository-relative path.
- Record ordinary `check`, `test --list`, and stable `test` behavior for each
existing example in the matrix.
- Treat matrix output as beta tooling evidence rather than a stable public
schema.
### Explicit Deferrals
This release does not implement source-language change, standard-library
helper change, runtime behavior change, compiler-known `std.*` runtime names,
package manager behavior, remote registries, lockfiles, semantic-version
solving, package publishing, optional/dev/target dependencies, stable
artifact-manifest schema guarantees, stable Markdown schemas, stable
conformance-matrix schema guarantees, LSP/watch/SARIF/daemon protocols, stable
ABI/layout, or performance claims.
## 1.0.0-beta.24
Release label: `1.0.0-beta.24`
Release date: 2026-05-23
Release state: package manifest identity and local dependency diagnostic
hardening
### Summary
The beta.24 compiler/tooling slice tightens local package manifest diagnostics
without changing the language, runtime, stdlib, or package graph model:
- Bump the `glagol` compiler package version to `1.0.0-beta.24`.
- Diagnose duplicate package manifest keys explicitly.
- Diagnose invalid dependency keys explicitly.
- Diagnose duplicate dependency keys explicitly.
- Keep dependency records local-path-only and require dependency keys to match
target package names.
### Explicit Deferrals
This release does not implement remote registries, lockfiles,
semantic-version solving, package publishing, optional/dev/target
dependencies, feature flags, build scripts, package archives, stable package
ABI/layout, source-language syntax, runtime C capabilities, standard-library
helpers, compiler-known `std.*` runtime names, stable artifact-manifest schema
guarantees, stable Markdown schemas, LSP/watch/SARIF/daemon protocols, or
performance claims.
## 1.0.0-beta.23
Release label: `1.0.0-beta.23`
Release date: 2026-05-23
Release state: standard-library stability tier ledger and catalog alignment
### Summary
The beta.23 compiler/tooling slice aligns generated standard-library API
catalog output with the public tier ledger:
- Bump the `glagol` compiler package version to `1.0.0-beta.23`.
- Render per-module and per-helper `beta-supported` or `experimental` tier
metadata in `docs/language/STDLIB_API.md`.
- Add `scripts/check-stdlib-api-tiers.js` and wire it into the release gate.
- Mark JSON, loopback networking, random/time, and filesystem resource-handle
helpers as experimental in generated catalog output.
- Keep concrete vector modules beta-supported concrete lanes with an explicit
no-generic-collection-freeze note.
### Explicit Deferrals
This release does not implement source-language syntax, standard-library
helpers, compiler-known `std.*` runtime names, runtime C capabilities, package
or workspace behavior, stable artifact-manifest schema guarantees, stable
Markdown schemas, LSP/watch/SARIF/daemon protocols, performance claims,
stable ABI/layout, or a stable standard-library compatibility contract.
## 1.0.0-beta.22
Release label: `1.0.0-beta.22`
Release date: 2026-05-23
Release state: run manifest and execution report hardening
### Summary
The beta.22 compiler/tooling slice hardens `glagol run --manifest` evidence
without changing the Slovo language or standard-library surface:
- Bump the `glagol` compiler package version to `1.0.0-beta.22`.
- Add an additive run-report block to run-mode artifact manifests.
- Record executed-program exit status, captured stdout, captured stderr, and
forwarded program arguments.
- Keep the run report as beta tooling metadata under the existing
`slovo.artifact-manifest` version `1` contract rather than a stable schema
freeze.
- Keep non-run modes outside this run-report requirement.
### Explicit Deferrals
This release does not implement source-language syntax, standard-library
helpers, compiler-known `std.*` runtime names, runtime C capabilities, package
or workspace behavior, stable artifact-manifest schema guarantees, stable
Markdown schemas, LSP/watch/SARIF/daemon protocols, performance claims,
stable ABI/layout, or a stable standard-library compatibility contract.
## 1.0.0-beta.21
Release label: `1.0.0-beta.21`
Release date: 2026-05-23
Release state: JSON document scalar parsing foundation
### Summary
The beta.21 compiler/tooling slice adds focused coverage for source-authored
`std.json` document-scalar helpers without adding compiler-known runtime names:
- Bump the `glagol` compiler package version to `1.0.0-beta.21`.
- Add focused `standard_json_document_scalar_parsing_beta21` coverage for
explicit `std.json` imports of `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`.
- Gate the helpers as source-authored public facades over existing JSON token
parsers and source-level document trimming/composition.
- Require direct `std.json.parse_*_document_result` runtime calls and private
`__glagol_json_*document*` runtime symbols to remain unsupported.
- Add the focused beta21 test to `scripts/release-gate.sh`.
### Explicit Deferrals
This release does not implement object parsing, array parsing, recursive JSON
values, tokenizers, Unicode escape decoding, streaming, schema validation,
stable parser APIs, new compiler-known `std.*` runtime names, runtime C
changes, source-language syntax, stable ABI/layout, or a stable
standard-library compatibility contract.
## 1.0.0-beta.20
Release label: `1.0.0-beta.20`
Release date: 2026-05-23
Release state: string search and ASCII trim foundation
### Summary
The beta.20 compiler/tooling slice adds focused coverage for source-authored
`std.string` search and ASCII trim helpers without adding compiler-known
runtime names:
- Bump the `glagol` compiler package version to `1.0.0-beta.20`.
- Add focused `standard_string_search_trim_beta20` coverage for explicit
`std.string` imports of `contains`, `index_of_option`,
`last_index_of_option`, `trim_ascii_start`, `trim_ascii_end`, and
`trim_ascii`.
- Require the new public helpers to remain source facades over beta16-or-earlier
string primitives and existing result/option shapes.
- Gate direct `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` runtime calls as
unsupported compiler-known names.
- Add the focused beta20 test to `scripts/release-gate.sh`.
### Explicit Deferrals
This release does not implement new compiler-known `std.*` runtime names,
runtime C changes, source-language syntax, Unicode or grapheme semantics,
locale/case-folded search, regex, tokenizer/parser APIs, stable string
ABI/layout, stable allocation ownership rules, or a stable standard-library
compatibility contract.
## 1.0.0-beta.19
Release label: `1.0.0-beta.19`
Release date: 2026-05-23
Release state: test discovery and user-project conformance foundation
### Summary
The beta.19 compiler/tooling contract adds deterministic list-only test
discovery without changing normal test execution output:
- `glagol test --list <file|project|workspace>`
- `glagol --run-tests --list <file>` for legacy single-file test execution
- Bump the `glagol` compiler package version to `1.0.0-beta.19`.
- Reuse the same checked discovery path as normal `glagol test` for file,
project, and workspace inputs.
- Preserve current file/project/workspace test ordering.
- Honor `--filter <substring>` by marking selected and skipped discovered
tests without executing test bodies or triggering runtime/test side effects.
- Keep normal `glagol test` and legacy `glagol --run-tests` output unchanged
when `--list` is absent.
- Add focused beta19 release-gate coverage for test discovery.
### Explicit Deferrals
This release does not implement parallel test execution, retries, tags/groups, coverage,
event streams, stable artifact-manifest or Markdown schema freezes,
LSP/watch/SARIF/daemon protocols, JSON expansion, runtime helper names,
source-language syntax, package registries, semver solving, and performance
claims.
## 1.0.0-beta.18
Release label: `1.0.0-beta.18`
Release date: 2026-05-23
Release state: JSON string token parsing foundation
### Summary
The beta.18 compiler/runtime contract promotes a narrow JSON string-token
parser without claiming a full JSON parser or Unicode escape policy.
- Bump the `glagol` compiler package version to `1.0.0-beta.18`.
- Add the promoted compiler-known runtime name
`std.json.parse_string_value_result`.
- Lower the helper through the existing nullable string result ABI shape:
hosted C returns an allocated decoded string on success and `NULL` for
ordinary `err 1` parse failure.
- Enforce one-token ASCII JSON string checks: exact surrounding quotes, no
leading/trailing whitespace, no raw control bytes, no raw non-ASCII bytes,
no raw quotes/backslashes, and no trailing bytes.
- Decode simple JSON escapes `\"`, `\\`, `\/`, `\b`, `\f`, `\n`, `\r`, and
`\t`; reject all `\uXXXX` escapes for this beta slice.
- Run `glagol test` execution on a bounded larger worker stack so deep
source-authored stdlib fixture tests fail through normal test diagnostics
instead of host process stack overflow.
### Explicit Deferrals
This release does not implement JSON object parsing, array parsing, recursive
JSON values, tokenizers, Unicode escape decoding, Unicode normalization,
embedded NUL support in the current null-terminated string ABI, streaming,
schema validation, stable runtime helper symbols, stable ABI/layout, or a
stable standard-library compatibility contract.
## 1.0.0-beta.17
Release label: `1.0.0-beta.17`
Release date: 2026-05-22
Release state: JSON primitive scalar parsing foundation
### Summary
The beta.17 compiler/runtime contract promotes strict JSON primitive scalar
token parsers for booleans and concrete numeric primitives without claiming a
full JSON parser.
- Bump the `glagol` compiler package version to `1.0.0-beta.17`.
- Add promoted compiler-known runtime names for
`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`, and
`std.json.parse_f64_value_result`.
- Lower the helpers through the existing concrete result ABI shapes and add
hosted C runtime implementations plus matching test-runner behavior.
- Enforce whole-token JSON scalar checks: no leading/trailing whitespace, no
leading `+`, no leading-zero integer form except `0`, and no non-finite f64
values.
- Keep `std.json.parse_null_value_result` source-only and gate deferred JSON
string/object/array/value parser families as unsupported.
### Explicit Deferrals
This release does not implement JSON string parsing, object parsing, array
parsing, recursive JSON values, tokenizers, generic parse APIs,
whitespace-tolerant document parsing, streaming, schema validation, Unicode
escape handling, stable runtime helper symbols, stable ABI/layout, or a stable
standard-library compatibility contract.
## 1.0.0-beta.16
Release label: `1.0.0-beta.16`
Release date: 2026-05-22
Release state: string scanning and token-boundary runtime foundation
### Summary
The beta.16 compiler/runtime contract promotes a byte-oriented string
scanning foundation for source-authored stdlib wrappers without claiming
Unicode, grapheme, tokenizer, parser, or stable ABI behavior.
- Bump the `glagol` compiler package version to `1.0.0-beta.16`.
- Add promoted compiler-known runtime names for `std.string.byte_at_result`,
`std.string.slice_result`, `std.string.starts_with`, and
`std.string.ends_with`.
- Lower `byte_at_result` through the existing encoded `result i32 i32` shape
and `slice_result` through the existing nullable string-result host-call
shape. Invalid indexes or ranges return `err 1`; slice allocation failure
follows the existing string allocation trap.
- Add hosted C runtime implementations over current null-terminated runtime
strings and matching test-runner behavior for deterministic Slovo tests.
- Extend diagnostics, facade project checks, and release-gate coverage for
the beta16 string-scanning boundary while keeping richer scanning and token
APIs unsupported.
### Explicit Deferrals
This release does not implement Unicode or grapheme semantics, a JSON parser,
tokenizers, split/find/contains helpers, stable string ABI/layout, stable
allocation ownership rules, or a stable standard-library compatibility
contract.
## 1.0.0-beta.15
Release label: `1.0.0-beta.15`
Release date: 2026-05-22
Release state: reserved generic collection boundary hardening and collection ledger
### Summary
The beta.15 compiler-side contract hardens the already-reserved
generic-shaped collection boundary without promoting executable generics,
maps, or sets. It keeps diagnostic codes and machine shapes stable while
removing beta.9-specific wording from current reserved diagnostics.
- Bump the `glagol` compiler package version to `1.0.0-beta.15`.
- Centralize reserved generic and collection diagnostic construction in
`compiler/src/reserved.rs` for lowerer, formatter, and checker paths.
- Reword live compiler diagnostics and affected snapshots to say the surface
is reserved but not supported in the current beta while preserving existing
codes, schema, spans, expected/found values, and hints.
- Add focused `reserved_generic_collection_beta15` coverage for `check`,
`fmt --check`, and project-root `check` rejection of generic functions,
parameterized aliases, generic type parameters, generic vector spelling,
map/set types, and reserved generic stdlib calls `std.vec.empty` and
`std.result.map`.
- Run the focused beta.15 reserved-boundary test in `scripts/release-gate.sh`
before the full compiler test suite.
### Explicit Deferrals
This release does not implement executable generics, parameterized alias
expansion, generic vectors, map/set types, generic standard-library dispatch,
runtime collection names, ABI/layout claims, parser semantic expansion, or
current concrete collection behavior changes.
## 1.0.0-beta.14
Release label: `1.0.0-beta.14`
Release date: 2026-05-22
Release state: benchmark suite catalog and metadata gate
### Summary
The beta.14 compiler-side contract is tooling-only benchmark catalog hardening.
It keeps benchmark execution local and unchanged while adding deterministic
suite-level metadata for release verification.
- Bump the `glagol` compiler package version to `1.0.0-beta.14`.
- Add `python3 benchmarks/runner.py --suite-list --json` for deterministic
suite-level benchmark listing and verification while preserving normal
per-benchmark `run.py` execution through the shared runner.
- Emit all 10 current benchmark names and directories, timing modes, cold/hot
loop counts, checksum metadata, required scaffold-file status,
implementation slots, and the local-only timing disclaimer.
- Add focused `benchmark_suite_catalog_beta14` coverage requiring byte-stable
suite catalog output across two runs and the current 10-benchmark inventory.
- Run the focused beta.14 suite catalog test in `scripts/release-gate.sh`
before the full compiler test suite.
### Explicit Deferrals
This release does not implement `glagol bench`, timing-result publication, a
new benchmark kernel, runtime changes, source-language changes, a stable JSON
schema claim, benchmark thresholds, or cross-machine performance claims.
## 1.0.0-beta.13
Release label: `1.0.0-beta.13`
Release date: 2026-05-22
Release state: diagnostic catalog and schema policy hardening update
### Summary
The beta.13 compiler-side contract is tooling/docs-only diagnostics policy
hardening. It keeps emitted diagnostic machine shapes stable while making the
schema name/version a single compiler constant source and gating the current
policy with focused structural tests.
- Bump the `glagol` compiler package version to `1.0.0-beta.13`.
- Centralize the `slovo.diagnostic` schema name and version used by
S-expression rendering and newline-delimited JSON diagnostics, and use the
same schema version in artifact manifest diagnostics metadata.
- Add focused `diagnostics_schema_beta13` coverage for parse, check,
formatter, test-runner, project, source-less usage, toolchain, and manifest
diagnostic policy across S-expression and `--json-diagnostics` outputs.
- Require artifact manifests to keep recording diagnostics schema version,
diagnostics encoding, and project diagnostic counts deterministically.
- Run the focused beta.13 diagnostics schema test in `scripts/release-gate.sh`
before the full compiler test suite.
### Explicit Deferrals
This release does not implement LSP, watch mode, SARIF, daemon protocols,
stable human diagnostic text, a stable Markdown schema, generic collections,
generic vectors, maps, sets, runtime changes, ABI changes, source-language
expansion, standard-library/API expansion, or performance claims.
## 1.0.0-beta.12
Release label: `1.0.0-beta.12`
Release date: 2026-05-22
Release state: source-authored concrete vector helper parity update
### Summary
The beta.12 compiler-side contract is test and package metadata support for a
stdlib/helper parity release. It keeps Glagol language execution, runtime
lowering, and compiler-known `std.vec.*` names unchanged.
- Bump the `glagol` compiler package version to `1.0.0-beta.12`.
- Extend explicit local `std.vec_i64` source-helper fixture tests to require
`count_of`, `starts_with`, `without_prefix`, `ends_with`, and
`without_suffix`.
- Extend explicit local `std.vec_f64` source-helper fixture tests to require
`count_of`.
- Require focused fixture coverage for repeated count results and
prefix/suffix empty, mismatch, exact, and longer-than-input cases where
applicable.
### Explicit Deferrals
This release does not implement source-language runtime changes, executable
generics, generic vectors, maps, sets, iterators, mutable vectors, slice/view
APIs, generic stdlib dispatch, new compiler-known stdlib or runtime names,
stable collection ABI/layout, performance claims, or a stable stdlib/API
compatibility freeze.
## 1.0.0-beta.11
Release label: `1.0.0-beta.11`
Release date: 2026-05-22
Release state: local package API documentation update
### Summary
The beta.11 docs/tooling contract extends beta.10 API discovery so generated
local documentation exposes the public API of local files, projects, packages,
and workspaces without changing source-language execution semantics.
- `glagol doc <file|project|workspace> -o <dir>` includes deterministic
exported/public API sections for local modules and workspace packages.
- Public API sections render exact exported function signatures, exported
struct fields, and exported enum variants with payload types.
- Module-local concrete aliases are normalized in public docs before rendering.
- Non-exported functions, structs, enums, tests, and `(type ...)` aliases stay
out of the public API sections.
### Explicit Deferrals
This release does not define a stable Markdown schema, stable stdlib/API
compatibility freeze, an LSP server, watch mode, SARIF, daemon protocols,
diagnostics schema policy, executable generics, generic vectors, maps, sets,
iterators, re-exports, globs, hierarchical modules, package registry
semantics, runtime collection changes, new standard-library runtime APIs, or
stable ABI/layout promises.
## 1.0.0-beta.10
Release label: `1.0.0-beta.10`
Release date: 2026-05-22
Release state: developer-experience API discovery and symbol-metadata update
### Summary
The beta.10 tooling/docs slice upgrades generated standard-library API
discovery and adds deterministic editor-facing symbol metadata without
changing source-language execution semantics.
- `scripts/render-stdlib-api-doc.js` now validates exported `lib/std` helper
names against matching `(fn ...)` forms.
- `docs/language/STDLIB_API.md` now lists exact exported helper signatures
rather than names only.
- Module-local concrete aliases are normalized in public signatures, so beta.8
and beta.9 aliases do not leak into the public catalog.
- Non-exported helper functions and `(type ...)` aliases remain omitted from
the catalog.
- `glagol symbols <file.slo|project|workspace>` emits deterministic
`slovo.symbols` S-expression metadata for modules, imports, exports,
aliases, structs, enums, functions, tests, source spans/ranges, and
workspace package labels.
- `compiler/tests/symbols_beta10.rs` covers single-file, project, workspace,
`-o`/manifest, and help-surface behavior.
### Explicit Deferrals
This release does not implement source-language runtime changes, executable
generics, generic stdlib dispatch, generic vectors, maps, sets, iterators,
collection unification, stable collection ABI/layout, new standard-library
runtime APIs, an LSP server, SARIF, watch mode, or daemon protocols.
## 1.0.0-beta.9
Release label: `1.0.0-beta.9`
Release date: 2026-05-22
Release state: compiler generic and collection reservation update
### Summary
Glagol `1.0.0-beta.9` reserves generic-shaped collection and stdlib syntax
with explicit diagnostics instead of letting future-looking forms degrade into
unknown-name or generic type errors.
- Reject generic function declarations such as `(fn id (type_params T) ...)`
with `UnsupportedGenericFunction`.
- Reject parameterized aliases such as `(type VecOf (type_params T) ...)` with
`UnsupportedGenericTypeAlias`.
- Reject generic type-parameter use in concrete type positions, map types,
set types, and future generic stdlib calls with focused diagnostic codes.
- Mirror the rejection in `glagol fmt` and promotion-gate inventory.
- Keep runtime, backend, ABI, and current concrete collection behavior
unchanged.
### Explicit Deferrals
This release does not implement generics, generic aliases, generic stdlib
dispatch, generic vectors, maps, sets, iterators, collection unification,
stable collection ABI/layout, or new standard-library APIs.
## 1.0.0-beta.8
Release label: `1.0.0-beta.8`
Release date: 2026-05-22
Release state: released beta concrete type alias foundation update
### Summary
Glagol `1.0.0-beta.8` is scoped to transparent concrete type aliases. A source
module may declare `(type Alias TargetType)` for an existing supported concrete
type and then use `Alias` in supported type positions. The alias is not a new
runtime type and must be normalized before backend/ABI behavior.
- Parse top-level `(type Alias TargetType)` declarations.
- Resolve aliases to existing supported concrete target types before typed-core
lowering, checked import signatures, LLVM/backend layout, ABI decisions, and
runtime behavior.
- Format aliases canonically as one-line top-level declarations.
- Diagnose malformed aliases, duplicate alias/type/value names, unknown or
unsupported target types, cycles, exported aliases, imported aliases, and
parameterized/generic alias attempts.
- Keep runtime behavior unchanged: no new compiler-known runtime names, hosted
symbols, maps/sets, generic aliases, parameterized aliases, or cross-module
alias visibility.
### Explicit Deferrals
This release does not add generics, parameterized aliases, alias
exports/imports, alias re-exports, cross-module alias visibility, maps/sets,
stable layout names, runtime helpers, hosted runtime symbols, or
standard-library API freeze claims.
## 1.0.0-beta.7 ## 1.0.0-beta.7

View File

@ -9,10 +9,9 @@ Compiler rule: make the tree visible. The pipeline should remain:
.slo source -> tokens -> S-expression tree -> AST -> typed AST -> LLVM IR text -> hosted Clang/runtime executable step .slo source -> tokens -> S-expression tree -> AST -> typed AST -> LLVM IR text -> hosted Clang/runtime executable step
``` ```
Long-horizon compiler planning lives in Long-horizon compiler planning lives in `.llm/ROADMAP_TO_STABLE.md`. It
`.llm/GENERAL_PURPOSE_LANGUAGE_ROADMAP.md`. It mirrors Slovo's experimental mirrors Slovo's release train beyond the first real general-purpose beta
release train from the historical `v2.0.0-beta.1` tag toward and beyond the toolchain.
first real general-purpose beta toolchain.
Release maturity policy lives in `.llm/RELEASE_MATURITY_POLICY.md`. Historical Release maturity policy lives in `.llm/RELEASE_MATURITY_POLICY.md`. Historical
`exp-*` releases remain experimental maturity. `1.0.0-beta` is the first real `exp-*` releases remain experimental maturity. `1.0.0-beta` is the first real
@ -22,17 +21,145 @@ general-purpose beta release.
A Glagol feature is done only when it has parser/lowerer support, checker behavior, diagnostics for invalid forms, backend behavior or explicit unsupported diagnostics, and tests. A Glagol feature is done only when it has parser/lowerer support, checker behavior, diagnostics for invalid forms, backend behavior or explicit unsupported diagnostics, and tests.
Current stage: `1.0.0-beta.7`, released on 2026-05-22 as the first post-beta Current stage: `1.0.0-beta.25`, released on 2026-05-23 as user-project
serialization/data-interchange foundation update. It keeps the `1.0.0-beta` language/compiler conformance matrix evidence. It keeps the
support baseline and includes the `1.0.0-beta.1` tooling hardening release, the `1.0.0-beta` language/compiler support baseline and includes the
`1.0.0-beta.2` runtime/resource foundation release, the `1.0.0-beta.3` `1.0.0-beta.1` tooling hardening release, the `1.0.0-beta.2` runtime/resource
standard-library stabilization release, the `1.0.0-beta.4` foundation release, the `1.0.0-beta.3` standard-library stabilization release,
language-usability diagnostics release, the `1.0.0-beta.5` package/workspace the `1.0.0-beta.4` language-usability diagnostics release, the
discipline release, and the `1.0.0-beta.6` compiler-known `std.net` loopback `1.0.0-beta.5` package/workspace discipline release, the `1.0.0-beta.6`
TCP runtime family with focused lowering, interpreter, diagnostics, compiler-known `std.net` loopback TCP runtime family with focused lowering,
source-facade, promotion, and hosted smoke coverage, plus the `1.0.0-beta.7` interpreter, diagnostics, source-facade, promotion, and hosted smoke coverage,
compiler-known `std.json.quote_string` runtime family with matching the `1.0.0-beta.7` compiler-known `std.json.quote_string` runtime family with
source-facade, test-runner, hosted smoke, and benchmark-scaffold coverage. matching source-facade, test-runner, hosted smoke, and benchmark-scaffold
coverage, the `1.0.0-beta.8` concrete type alias foundation update, and
focused diagnostics/formatter/project-mode gates for reserved generic
functions, parameterized aliases, generic type parameters, maps, sets, and
future generic stdlib calls. The beta.10 tooling/docs slice upgrades
generated `lib/std` API discovery so public documentation lists exact exported
helper signatures after alias normalization, and adds
`glagol symbols <file.slo|project|workspace>` for deterministic
editor-facing source metadata over modules, imports, exports, aliases,
structs, enums, functions, tests, spans/ranges, and workspace package labels.
The beta.11 documentation slice extends
`glagol doc <file|project|workspace> -o <dir>` with deterministic
exported/public API sections for local packages and modules, including exact
exported function signatures, exported struct fields, exported enum
variants/payload types, non-export filtering, and module-local alias
normalization. The beta.12 stdlib/helper parity slice adds source-authored
coverage for `std.vec_i64.count_of`, `std.vec_i64.starts_with`,
`std.vec_i64.without_prefix`, `std.vec_i64.ends_with`,
`std.vec_i64.without_suffix`, and `std.vec_f64.count_of` without changing
source-language runtime behavior. The beta.13 tooling/docs-only diagnostics
slice centralizes the `slovo.diagnostic` schema name/version constants and
adds structural gates for S-expression diagnostics, `--json-diagnostics`, and
artifact-manifest diagnostics metadata without changing emitted machine
diagnostic shape. The beta.14 tooling-only benchmark slice adds deterministic
suite-level local benchmark listing and release-gate coverage for
`python3 benchmarks/runner.py --suite-list --json`, including all current
benchmark directories, timing modes, cold/hot loop counts, checksum metadata,
required scaffold-file status, implementation slots, and a local-only timing
disclaimer without publishing timing results or claiming a stable JSON schema.
The beta.15 compiler/tooling slice centralizes reserved generic and collection
diagnostics across lowerer, formatter, and checker paths, rewords current
reserved diagnostics to stage-neutral current-beta text, and gates the
collection ledger for generic functions, parameterized aliases, generic type
parameters, generic vector spelling, map/set types, `std.vec.empty`, and
`std.result.map` across check, fmt, and project paths without promoting those
surfaces. The beta.16 compiler/runtime slice adds byte-oriented
`std.string.byte_at_result`, `std.string.slice_result`,
`std.string.starts_with`, and `std.string.ends_with` over current
null-terminated runtime strings, with invalid indexes/ranges returning
`err 1`, without Unicode/grapheme semantics, tokenizer/parser claims, or
stable ABI/layout commitments.
The beta.17 compiler/runtime slice adds promoted `std.json` primitive scalar
parse helpers for bool, i32, u32, i64, u64, and f64 JSON tokens, with strict
whole-token checks, matching test-runner and hosted-runtime behavior, source
facade alignment, diagnostics coverage, and release-gate coverage. It keeps
`parse_null_value_result` source-only and leaves JSON strings, objects,
arrays, recursive values, tokenizers, schema validation, streaming, Unicode
escape handling, and stable JSON APIs deferred.
The beta.18 compiler/runtime slice adds promoted
`std.json.parse_string_value_result` for exact ASCII JSON string tokens, with
simple escape decoding and matching test-runner/hosted-runtime behavior. It
also runs `glagol test` execution on a bounded larger worker stack so deep
source-authored stdlib fixture tests remain gateable through ordinary
diagnostics. It keeps object parsing, array parsing, recursive values,
tokenizers, Unicode escape decoding, Unicode normalization, streaming, schema
validation, embedded NUL support in the current null-terminated string ABI, and
stable JSON APIs deferred.
The beta.19 compiler/tooling slice adds
`glagol test --list <file|project|workspace>` and legacy
`glagol --run-tests --list <file>` support. The list action reuses the same
checked discovery path as normal test execution, preserves existing
single-file, project, and workspace ordering, honors `--filter <substring>`,
and avoids executing test bodies. It keeps normal test execution output
unchanged when `--list` is absent.
The beta.20 compiler/tooling slice bumps the package version and gates
source-authored `std.string` facades for `contains`, `index_of_option`,
`last_index_of_option`, `trim_ascii_start`, `trim_ascii_end`, and `trim_ascii`
through explicit `std.string` imports. It keeps those helpers composed over
beta16-or-earlier string primitives and existing option/result shapes, and
verifies that direct compiler-known runtime calls for the new helper names
remain unsupported.
The beta.21 compiler/tooling slice bumps the package version and gates
source-authored `std.json` document scalar facades for
`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` through explicit
`std.json` imports. It keeps those helpers source-authored over the existing
JSON token parser family and existing source-level composition, and verifies
that direct compiler-known runtime calls and private `__glagol_json_*document*`
symbols for the new helper names remain unsupported.
The beta.22 compiler/tooling slice bumps the package version and hardens
`glagol run --manifest` evidence with an additive run-report block in
run-mode artifact manifests. The block records executed-program exit status,
captured stdout, captured stderr, and forwarded program arguments. It is beta
CLI evidence metadata under the existing `slovo.artifact-manifest` version
`1` contract, not a stable schema freeze, and it adds no source-language,
runtime, package, or standard-library surface.
The beta.23 compiler/tooling slice bumps the package version and updates
generated `docs/language/STDLIB_API.md` output with per-module and per-helper
tier metadata. It adds a release-gate checker for the tier catalog, marks JSON,
loopback networking, random/time, and filesystem resource-handle helpers as
experimental, and keeps concrete vector modules beta-supported concrete lanes
without claiming generic collection stability. It adds no source-language,
runtime, package, or standard-library helper surface.
The beta.24 compiler/tooling slice bumps the package version and tightens
local package manifest diagnostics. Duplicate package manifest keys, invalid
dependency keys, and duplicate dependency keys are explicit diagnostics. It
keeps the existing closed local workspace model and adds no remote registry,
lockfile, semantic-version solving, package publishing, optional/dev/target
dependencies, stable package ABI/layout, source-language behavior, runtime
behavior, or standard-library behavior.
The beta.25 compiler/tooling slice adds deterministic stable-readiness
evidence for ordinary project and workspace usage over the existing
`examples/projects/` and `examples/workspaces/` inventories. The conformance
matrix is repository-local, sorted by stable path, and records ordinary
`check`, `test --list`, and stable `test` behavior for all 43 top-level
fixture roots and 655 discovered tests. It adds no source-language behavior,
runtime behavior, standard-library helper behavior, package manager or
registry behavior, lockfile behavior, semantic-version solving, stable schema
freeze, or performance claim.
Generic vectors, generic collections, maps, sets, generic stdlib dispatch,
runtime collection changes, collection unification, stable human diagnostic
text, stable artifact-manifest, Markdown, or conformance-matrix schema
freezes, LSP/watch protocols, SARIF/daemon protocols,
re-exports/globs/hierarchical modules,
registry semantics, semver solving, mutable vectors, stable slice/view APIs,
tokenizers, broader JSON parsing, runtime helper names, source-language
syntax, parallel test execution, retries, tags/groups, coverage/event streams,
performance claims, ABI/layout stability, and a stable stdlib/API
compatibility freeze remain unimplemented until a later scoped contract
promotes them explicitly.
The final experimental precursor scope is `exp-125`. Its unsigned direct-value The final experimental precursor scope is `exp-125`. Its unsigned direct-value
flow, parse/format runtime lanes, and matching staged stdlib helper breadth flow, parse/format runtime lanes, and matching staged stdlib helper breadth
@ -606,6 +733,8 @@ Alpha is the latest compiler semantic/runtime-operation support slice.
- [x] Diagnose missing packages, duplicate package names, dependency cycles, - [x] Diagnose missing packages, duplicate package names, dependency cycles,
path escapes, invalid package names, invalid package versions, private path escapes, invalid package names, invalid package versions, private
visibility, and dependency key mismatches. visibility, and dependency key mismatches.
- [x] Diagnose duplicate package manifest keys, invalid dependency keys, and
duplicate dependency keys explicitly.
- [x] Keep registries, lockfiles, semver solving, aliases/globs/re-exports, - [x] Keep registries, lockfiles, semver solving, aliases/globs/re-exports,
generated code, build scripts, publishing, optional/dev/target generated code, build scripts, publishing, optional/dev/target
dependencies, stable package ABI, and remote dependencies deferred. dependencies, stable package ABI, and remote dependencies deferred.

View File

@ -0,0 +1,139 @@
# Slovo Collection Ledger
Status: beta design ledger for `1.0.0-beta.15`.
This document inventories the current concrete collection and value-family
surface without redefining the generated public API catalog. Exact exported
helper signatures live in
[`STDLIB_API.md`](STDLIB_API.md), which is generated from `lib/std/*.slo`.
This ledger records design boundaries, pressure, and promotion prerequisites
for future generic collection work.
`1.0.0-beta.15` is documentation/design and compiler-boundary hardening. It
does not change the source language, typed core, runtime,
standard-library/API surface, diagnostic output shape, diagnostic codes,
diagnostic schema, benchmark metadata schema, ABI/layout behavior, or
performance claims. It does reword current reserved generic/map/set diagnostic
prose from beta.9-specific text to current-beta wording.
## Catalog Boundary
[`STDLIB_API.md`](STDLIB_API.md) remains the source of truth for current
exported standard-library helper signatures. It normalizes module-local
concrete aliases such as `VecI32`, `OptionString`, and `ResultU64` to concrete
public types, and omits non-exported helpers and `(type ...)` aliases.
This ledger intentionally does not duplicate generated module or helper
counts. If an exact signature or count is needed, use the generated catalog.
If this ledger and the catalog drift, the catalog describes the generated API
surface while this file describes the intended design boundary.
## Current Concrete Surface
| Surface | Current concrete families | Public catalog reference | Boundary |
| --- | --- | --- | --- |
| Concrete vectors | `(vec i32)`, `(vec i64)`, `(vec f64)`, `(vec bool)`, `(vec string)` | [`std.vec_i32`](STDLIB_API.md#stdvec_i32), [`std.vec_i64`](STDLIB_API.md#stdvec_i64), [`std.vec_f64`](STDLIB_API.md#stdvec_f64), [`std.vec_bool`](STDLIB_API.md#stdvec_bool), [`std.vec_string`](STDLIB_API.md#stdvec_string) | Concrete immutable value-family helpers over the existing vector runtime names. No generic vector dispatch, element-level mutation, slice/view API, iterator API, nested vector family, new runtime helper name, stable helper-symbol contract, or ABI/layout promise is implied. |
| Option value families | `(option i32)`, `(option u32)`, `(option i64)`, `(option u64)`, `(option f64)`, `(option bool)`, `(option string)` | [`std.option`](STDLIB_API.md#stdoption) | Concrete constructors, observers, unwrap/fallback helpers, and option-to-result bridges. No generic option helper, mapping/chaining API, transpose/flatten family, stable API freeze, or payload-family expansion is implied. |
| Result value families | `(result i32 i32)`, `(result u32 i32)`, `(result i64 i32)`, `(result u64 i32)`, `(result f64 i32)`, `(result bool i32)`, `(result string i32)` | [`std.result`](STDLIB_API.md#stdresult) | Concrete `i32` error-code result helpers. No generic result helper, rich error ADT, mapping/chaining API, transpose/flatten family, stable API freeze, or alternate error payload family is implied. |
| Host and parsing result/option flows | Concrete option/result values returned by current host, CLI, file, network, parse, and numeric helpers | [`std.cli`](STDLIB_API.md#stdcli), [`std.env`](STDLIB_API.md#stdenv), [`std.fs`](STDLIB_API.md#stdfs), [`std.io`](STDLIB_API.md#stdio), [`std.net`](STDLIB_API.md#stdnet), [`std.num`](STDLIB_API.md#stdnum), [`std.string`](STDLIB_API.md#stdstring) | These helpers expose the same concrete value families through ordinary source facades. They do not create generic payload types, generic host errors, stable ABI names, or a stable compatibility freeze. |
Fixed arrays are part of the current language surface, but they are not a
standard-library helper family in the generated API catalog. Future work that
connects arrays to slices, views, iterators, maps, sets, or generic collection
helpers must define its own language, runtime, diagnostic, and documentation
contract.
## Design Pressure
The current surface deliberately repeats concrete helper families. That
duplication has been useful because each family could be promoted with narrow
tests and without a generic type system, but it now creates clear pressure:
- Vector helpers repeat construction, indexing, fallback, search, query,
concatenation, slicing-by-copy, replacement, and removal names across five
concrete element families.
- `std.option` and `std.result` repeat constructor, observer, unwrap,
fallback, and bridge helpers across concrete payload families, including the
unsigned payloads added before beta.
- Helper parity can drift between concrete modules, as shown by the beta12
vector query and prefix parity release.
- Module-local concrete aliases reduce source repetition inside facades, and
the generated catalog hides those aliases from public signatures, but aliases
do not define generic API compatibility or migration behavior.
- Host and parsing facades spread concrete option/result values through the
broader API surface, so future generic work must account for both dedicated
`std.option`/`std.result` modules and callers that already depend on
concrete return types.
This pressure is a reason to design generics carefully, not a license to infer
generic behavior from existing concrete helpers.
## Promotion Prerequisites
Before any executable generic collection feature is promoted, a release must
define and gate the full contract for that feature. At minimum:
- Executable generics need source syntax, type-parameter rules, explicit
inference policy, type-checking behavior, formatter layout, typed-core
representation, lowering/monomorphization or dispatch strategy, diagnostic
coverage, public documentation, examples, and migration rules for existing
concrete helpers.
- Generic aliases need a separate alias contract, including parameter syntax,
allowed targets, cycle checks, export/import visibility, formatter behavior,
diagnostics, and the relationship to runtime layout and public API docs.
- Maps and sets need key/value constraints, equality and hashing or ordering
policy, construction and update semantics, iteration order or non-order
policy, error behavior, runtime/resource ownership, diagnostics, and public
API stability rules.
- Iterators need an ownership and lifetime model, exhaustion semantics,
composition rules, interaction with `match` and future loops, diagnostics,
and guarantees about whether iteration is copying, borrowing, or consuming.
- Mutable vectors need element-mutation syntax, aliasing and ownership rules,
capacity/reallocation behavior, interaction with whole-value `var` / `set`,
runtime helper names, trap/error behavior, diagnostics, and layout/ABI
boundaries.
- Slice/view APIs need borrowing or ownership rules, bounds behavior,
invalidation rules when mutation exists, array/vector/string applicability,
formatter behavior, diagnostics, and runtime representation boundaries.
- Any stable stdlib/API claim needs explicit stable and experimental tiers,
compatibility and deprecation policy, migration tests, generated API-doc
behavior, and release-gate coverage.
Until those prerequisites are satisfied by a specific release, the current
concrete families remain the entire supported collection/value-family surface.
## Unsupported Boundaries
Current diagnostics that reject generic aliases, unsupported alias targets,
unsupported collection forms, unsupported vector element types, maps, sets,
iterators, slice/view APIs, mutable element operations, or broader
option/result payload shapes are boundary diagnostics. They document where the
current beta stops.
`1.0.0-beta.15` does not add, remove, rename, or reclassify diagnostic codes.
It does not change the `slovo.diagnostic` schema, JSON/S-expression output
shape, spans, expected/found values, hints, or human-readable diagnostic prose
policy. The only diagnostic-fixture change in this release is reserved-boundary
message text that no longer names beta.9 as the unsupported stage. Future
releases that change diagnostics for collection boundaries must do so through
the diagnostic policy in [`DIAGNOSTICS.md`](DIAGNOSTICS.md) and their own
release gates.
## Explicit Non-Changes
This ledger does not promote:
- executable generics
- generic aliases or parameterized aliases
- generic stdlib dispatch
- maps or sets
- iterator APIs
- mutable vector APIs
- slice/view APIs
- new runtime names or helper symbols
- stdlib/API additions, removals, or renames
- diagnostic output shape, code, schema, span, expected/found, or hint changes
- benchmark metadata schema changes
- ABI/layout guarantees
- performance claims or thresholds
- a stable `1.0.0` stdlib/API freeze

View File

@ -0,0 +1,294 @@
# Slovo Diagnostics
This document defines the `1.0.0-beta.13` beta policy for Slovo machine
diagnostics. It documents the existing `slovo.diagnostic` version `1` schema,
the relationship between the S-expression and JSON encodings, the current
golden diagnostic code catalog, and the migration rules for changing machine
diagnostic fields or codes.
This is a diagnostics policy and catalog slice only. It does not add source
language syntax, runtime behavior, stdlib APIs, ABI/layout guarantees, LSP,
watch mode, SARIF, daemon protocols, stable Markdown output, or a stable
`1.0.0` diagnostics freeze.
## Schema Identity
The machine diagnostic schema name is `slovo.diagnostic`.
The current schema version is `1`.
The S-expression form uses a root `(diagnostic ...)` object:
```text
(diagnostic
(schema slovo.diagnostic)
(version 1)
...)
```
The JSON form uses a single JSON object:
```json
{"schema":"slovo.diagnostic","version":1}
```
The schema name and version belong to the machine contract. Changing either one
requires an explicit migration note, release-note entry, and matching Glagol
snapshot or JSON fixture updates.
## Encodings
S-expression diagnostics and JSON diagnostics are two encodings of the same
diagnostic data model. They must not diverge semantically.
The default human diagnostics path may print human-readable prose and then the
S-expression machine form for source diagnostics. Human prose, spacing,
excerpts, hints, and surrounding text remain beta-flexible unless a later
release freezes them explicitly. Tools should consume the machine form, not the
human rendering.
`--json-diagnostics` emits JSON diagnostics on stderr. Each diagnostic is one
complete JSON object on one line. There is no JSON array wrapper, no pretty
printing, and no required trailing summary object for source failures. Consumers
should parse stderr as newline-delimited JSON objects and should treat each
line as an independent diagnostic record.
JSON and S-expression escaping must preserve the same string values. JSON uses
standard JSON string escaping. S-expression strings use the Glagol diagnostic
string escaping convention already used in golden `.diag` fixtures.
## Fields
Every source-attached compiler diagnostic must include these machine fields:
| Field | S-expression | JSON | Meaning |
| --- | --- | --- | --- |
| Schema | `(schema slovo.diagnostic)` | `"schema":"slovo.diagnostic"` | Diagnostic schema name. |
| Version | `(version 1)` | `"version":1` | Diagnostic schema version. |
| Severity | `(severity error)` | `"severity":"error"` | Diagnostic severity. |
| Code | `(code TypeMismatch)` | `"code":"TypeMismatch"` | Stable PascalCase diagnostic code for the condition. |
| Message | `(message "...")` | `"message":"..."` | Concise beta-flexible human message for the machine record. |
| Source file | `(file "...")` | `"file":"..."` | Source identity used by the invocation or project loader. |
| Span | `(span ...)` | `"span":{...}` | Primary source span and line/column range. |
Optional fields may appear when the compiler has precise data:
| Field | S-expression | JSON | Meaning |
| --- | --- | --- | --- |
| Expected | `(expected "...")` | `"expected":"..."` | Expected type, arity, value, or form. |
| Found | `(found "...")` | `"found":"..."` | Found type, arity, value, or form. |
| Hint | `(hint "...")` | `"hint":"..."` | Safe repair or orientation hint. |
| Related spans | repeated `(related ...)` | `"related":[...]` | Secondary source locations tied to the primary diagnostic. |
Optional fields are additive when they do not replace an existing machine field
or change an existing diagnostic code. Removing an optional field from an
existing golden fixture is a migration-level change unless the release note
states that the previous field was incorrect.
## Severity
Current source diagnostics use severity `error`.
JSON source-less or invocation-level messages may use `error` for failures and
`note` for informational tool output such as a machine-readable test-run
summary. A `note` is not by itself a source failure; consumers should still use
the process exit code and artifact manifest success field to determine command
success.
Adding a new severity value is an additive schema change only when old
consumers can safely ignore or display it. Reclassifying an existing failing
diagnostic from `error` to another severity is migration-level.
## Source And Range Semantics
`file` is the source identity reported by the compiler path, project loader, or
workspace loader. It is not a promise of absolute path normalization, URI
formatting, registry identity, or editor document identity.
Primary and related byte spans are zero-based and half-open: `start` is the
first byte included, and `end` is the first byte after the highlighted source.
Line and column ranges are one-based and derived from the original source text.
Columns are byte columns within the original UTF-8 source line; a tab counts as
one input byte. The end column is the first byte column after the highlighted
range on the end line.
Diagnostic locations are derived from source input, not formatter output,
lowered IR, generated C, LLVM IR, native object code, or runtime stack traces.
## Related Spans
Related spans identify secondary source locations such as an original
declaration, previous duplicate, or conflicting arm. A related span never
replaces the primary span.
In S-expression diagnostics, each related location is a repeated
`(related (span ...))` form. In JSON diagnostics, related locations are objects
inside the `related` array. Each related object has its own `file` and `span`;
it may also carry a `message` string.
Related messages are beta-flexible prose. The existence of a related span for a
current golden fixture is part of the machine shape for that fixture and should
change only intentionally.
## Source-Less Diagnostics
Some diagnostics describe tool invocation or usage failures rather than a
source range. Examples include invalid CLI arguments, missing inputs, and
source-loading failures before a source span is available.
JSON source-less diagnostics use:
```json
{"file":null,"span":null}
```
Source-less diagnostics must not invent dummy paths, byte offsets, or
line/column ranges. Text-mode source-less diagnostics may remain human-only in
current Glagol output; consumers that need machine-readable source-less
diagnostics should request `--json-diagnostics`.
If a future release adds S-expression source-less diagnostics, absence of a
source must be explicit and documented under `slovo.diagnostic` version `1` or
a later migrated version. It must not be encoded as fake source coordinates.
## Artifact Manifest Metadata
`slovo.artifact-manifest` records the diagnostic metadata for a tool
invocation. The relevant fields are:
- `(diagnostics-schema-version 1)`: the diagnostic schema version used by the
invocation.
- `(diagnostics-encoding sexpr)` or `(diagnostics-encoding json)`: the encoding
selected for diagnostics.
- `(primary-output (kind diagnostics) ...)`: the primary output kind when the
command failed by emitting diagnostics.
- `(diagnostic_artifacts ...)`: the diagnostic stream artifact metadata for
project/package/workspace modes.
- `(diagnostics_count N)`: the project-level count when available.
The manifest points to or records diagnostic streams. It does not define a
separate diagnostic schema, embed a parsed diagnostic catalog, freeze Markdown
documentation structure, or replace the newline discipline of JSON diagnostic
stderr.
## Compatibility And Migration
Diagnostic changes are classified as follows.
Clarifying changes:
- Rewording human-readable stderr prose.
- Rewording `message` or `hint` text without changing the diagnostic code,
source span, field set, severity, or expected/found semantics.
- Improving documentation around an existing code or fixture.
Additive changes:
- Adding a new diagnostic code for a newly covered boundary.
- Adding a new golden fixture for a newly rejected form.
- Adding an optional field when the old fields, code, severity, source span,
and JSON-line discipline remain valid.
- Adding related spans for a new fixture.
Migration changes:
- Renaming, removing, splitting, or merging an existing diagnostic code covered
by golden fixtures.
- Changing required field names, schema name, schema version, span shape, range
indexing, JSON null policy, or JSON-line discipline.
- Removing an existing optional field or related span from a golden fixture
without documenting the correction.
- Changing a source-attached diagnostic into a source-less diagnostic, or the
reverse, for an existing golden fixture.
- Reclassifying an existing failing diagnostic away from severity `error`.
- Changing artifact manifest diagnostic metadata fields or their meaning.
Every migration-level diagnostic change must update this document or the next
release policy, `docs/language/MIGRATION_POLICY.md` if needed,
`docs/language/RELEASE_NOTES.md`, and the matching Glagol golden snapshots or
tests.
## Current Golden Catalog
The current golden diagnostics contract is the snapshot inventory in
`compiler/tests/diagnostics_contract.rs`. As of `1.0.0-beta.13`, that contract
references 358 `.diag` snapshots under `tests/`, and those snapshots contain
114 unique diagnostic codes.
This catalog inventories those codes. It is policy-focused: messages and
fixture-specific prose remain in the snapshots.
| Area | Current codes |
| --- | --- |
| General calls, typing, signatures, and source shape | `ArityMismatch`, `ReturnTypeMismatch`, `SingleFileMainSignature`, `TypeMismatch`, `UnclosedList`, `UnknownFunction`, `UnknownTopLevelForm`, `UnsupportedBackendFeature`, `UnsupportedUnitSignatureType` |
| Control flow and tests | `EmptyWhileBody`, `IfBranchTypeMismatch`, `IfConditionNotBool`, `MalformedIfForm`, `MalformedTestForm`, `MalformedWhileForm`, `NestedWhileUnsupported`, `TestExpressionNotBool`, `WhileBodyFormNotUnit`, `WhileConditionNotBool` |
| Literals and strings | `I64LiteralOutOfRange`, `IntegerOutOfRange`, `InvalidI64Literal`, `UnsupportedFloatLiteral`, `UnsupportedStringConcatenation`, `UnsupportedStringEscape`, `UnsupportedStringLiteral` |
| Locals, assignment, and name conflicts | `CannotAssignImmutableLocal`, `CannotAssignParameter`, `DuplicateFunction`, `DuplicateLocal`, `DuplicateTestName`, `InvalidSetTarget`, `InvalidTestName`, `LocalDeclarationInWhileBodyUnsupported`, `LocalDeclarationNotAllowed`, `LocalRedeclaresParameter`, `LocalShadowsCallable`, `ParameterShadowsCallable`, `UnknownVariable`, `UnsupportedLocalType` |
| Type aliases, generics, maps, and sets | `DuplicateTypeAlias`, `MalformedTypeAlias`, `SelfTypeAlias`, `TypeAliasCycle`, `TypeAliasNameConflict`, `UnknownTypeAliasTarget`, `UnsupportedGenericFunction`, `UnsupportedGenericStandardLibraryCall`, `UnsupportedGenericTypeAlias`, `UnsupportedGenericTypeParameter`, `UnsupportedMapType`, `UnsupportedSetType`, `UnsupportedTypeAliasTarget` |
| Structs and fields | `DuplicateStruct`, `DuplicateStructConstructorField`, `DuplicateStructField`, `EmptyStructUnsupported`, `FieldAccessOnNonStruct`, `MissingStructField`, `RecursiveStructFieldUnsupported`, `StructConstructorFieldOrderMismatch`, `UnknownStructField`, `UnknownStructType`, `UnsupportedStructFieldType` |
| Arrays and vectors | `ArrayIndexNotI32`, `ArrayIndexOutOfBounds`, `EmptyArrayUnsupported`, `IndexOnNonArray`, `MutableArrayLocalUnsupported`, `UnsupportedArrayElementType`, `UnsupportedArrayEquality`, `UnsupportedArrayPrint`, `UnsupportedVectorElementType`, `UnsupportedVectorEquality`, `ZeroLengthArrayUnsupported` |
| Options, results, and match | `DuplicateMatchArm`, `MalformedMatchPattern`, `MalformedOptionConstructor`, `MalformedResultConstructor`, `MalformedUnwrapForm`, `MatchArmTypeMismatch`, `MatchBindingCollision`, `MatchSubjectTypeMismatch`, `NonExhaustiveMatch`, `OptionObservationTypeMismatch`, `OptionUnwrapTypeMismatch`, `ResultObservationTypeMismatch`, `ResultUnwrapTypeMismatch`, `UnsupportedMatchContainer`, `UnsupportedMatchMutation`, `UnsupportedMatchPayloadType`, `UnsupportedOptionPayloadType`, `UnsupportedOptionResultEquality`, `UnsupportedOptionResultPrint`, `UnsupportedResultPayloadType` |
| Enums | `DuplicateEnum`, `DuplicateEnumVariant`, `EmptyEnumUnsupported`, `EnumSubjectMismatch`, `InvalidEnumMatchArm`, `MixedEnumPayloadTypesUnsupported`, `RecursiveEnumPayloadStructUnsupported`, `UnknownEnumConstructor`, `UnknownVariantConstructor`, `UnsupportedEnumContainer`, `UnsupportedEnumEquality`, `UnsupportedEnumOrdering`, `UnsupportedEnumPayloadType`, `UnsupportedEnumPrint`, `VariantConstructorArity` |
| Unsafe operations | `MalformedUnsafeForm`, `UnsafeRequired`, `UnsupportedUnsafeOperation` |
| Standard-library reservation boundaries | `UnsupportedStandardLibraryCall` |
Future releases may add codes or split broad codes when the release scope
requires more precise tooling behavior. Such changes must be documented using
the compatibility classes above.
## Project And Workspace Codes
Project, package, and workspace diagnostics are covered by integration tests
because they often require multiple files, manifests, or generated temporary
directories rather than a single `.slo` golden fixture. These codes still use
the same `slovo.diagnostic` version `1` schema and PascalCase code policy.
Current package/workspace loader and graph diagnostics include:
- `DependencyNameMismatch`
- `DependencyPathEscape`
- `DuplicatePackageDependencyName`
- `DuplicatePackageName`
- `DuplicateWorkspaceMember`
- `InvalidPackageDependencyName`
- `InvalidPackageName`
- `InvalidPackageVersion`
- `MissingPackageDependency`
- `MissingPackageModule`
- `PackageDependencyCycle`
- `PackageImportNotDependency`
- `PackageManifestInvalid`
- `PackageSourceReadFailed`
- `PackageSourceRootEscape`
- `PackageSourceRootMissing`
- `ProjectEntryMainInvalidSignature`
- `ProjectEntryMainMissing`
- `ProjectManifestInvalid`
- `ProjectManifestReadFailed`
- `ProjectSourceReadFailed`
- `ProjectSourceRootMissing`
- `UnsupportedDependency`
- `WorkspaceBuildAmbiguousEntryPackage`
- `WorkspaceDefaultPackageEntryMissing`
- `WorkspaceDefaultPackageMissing`
- `WorkspaceEntryMainInvalidSignature`
- `WorkspaceEntryMainMissing`
- `WorkspaceManifestInvalid`
- `WorkspaceMemberManifestMissing`
- `WorkspaceMemberPathEscape`
## Explicit Deferrals
`1.0.0-beta.13` does not define:
- a stable `1.0.0` diagnostics freeze
- LSP diagnostics, watch mode, SARIF, daemon protocols, or debug adapters
- a stable Markdown documentation schema
- stable source-map, DWARF, LLVM debug metadata, or runtime stack trace schema
- warning, lint, or suggestion taxonomies beyond the current `error` and
source-less `note` uses
- localized diagnostic text
- machine-readable remediation edits
- a separate diagnostic catalog artifact emitted by the compiler
- stable package registry, URI, or workspace identity semantics for `file`

View File

@ -46,10 +46,18 @@ Slovo must not silently repurpose an old supported form with new meaning.
## Diagnostic And Tooling Changes ## Diagnostic And Tooling Changes
Diagnostic code or machine-shape changes require a documented schema migration [`docs/language/DIAGNOSTICS.md`](DIAGNOSTICS.md) is the beta policy for
and matching Glagol snapshot updates. Formatter changes require before/after `slovo.diagnostic` version `1`. Human-readable diagnostic prose remains
fixture coverage so agents can see whether the change is additive, beta-flexible, but machine fields, schema/version markers, diagnostic codes,
clarifying, or migration-level. source/span/range semantics, JSON-line discipline, related-span shape, and
artifact-manifest diagnostic metadata are compatibility-sensitive.
Diagnostic code or machine-shape changes require the compatibility class
defined in [`docs/language/DIAGNOSTICS.md`](DIAGNOSTICS.md), a documented
schema or catalog migration when the change is migration-level, and matching
Glagol snapshot updates. Formatter changes require before/after fixture
coverage so agents can see whether the change is additive, clarifying, or
migration-level.
Native executable output, package layout, stable ABI/layout promises, stable Native executable output, package layout, stable ABI/layout promises, stable
standard-runtime printing APIs, and raw-memory/FFI contracts remain outside the standard-runtime printing APIs, and raw-memory/FFI contracts remain outside the

View File

@ -56,6 +56,10 @@ used for solving dependency constraints.
dependency paths must stay inside the workspace/package boundary after dependency paths must stay inside the workspace/package boundary after
normalization and canonical path checks. normalization and canonical path checks.
Duplicate keys in package manifests are `PackageManifestInvalid` diagnostics.
The manifest loader does not silently choose one spelling for repeated package
identity or dependency entries.
Dependencies are local path records only. The dependency key must match the Dependencies are local path records only. The dependency key must match the
target package name: target package name:
@ -63,6 +67,11 @@ target package name:
mathlib = { path = "../mathlib" } mathlib = { path = "../mathlib" }
``` ```
Dependency keys use the same package-name shape as package identities. Invalid
dependency keys are `InvalidPackageDependencyName` diagnostics, and duplicate
dependency keys are `DuplicatePackageDependencyName` diagnostics before the
local dependency graph is accepted.
## Imports ## Imports
Within a workspace, a package imports a dependency module through the package Within a workspace, a package imports a dependency module through the package
@ -106,9 +115,12 @@ The package/workspace gate covers these user-facing error families:
- missing member manifests - missing member manifests
- duplicate normalized workspace members - duplicate normalized workspace members
- invalid member or dependency paths - invalid member or dependency paths
- duplicate package manifest keys
- invalid package names and versions - invalid package names and versions
- duplicate package names - duplicate package names
- missing local path dependencies - missing local path dependencies
- invalid dependency keys
- duplicate dependency keys
- dependency key/name mismatches - dependency key/name mismatches
- package dependency cycles - package dependency cycles
- private cross-package imports - private cross-package imports

View File

@ -8,7 +8,7 @@ Historical `exp-*` releases listed here are experimental maturity milestones.
The pushed tag `v2.0.0-beta.1` is historical. It is now documented as an The pushed tag `v2.0.0-beta.1` is historical. It is now documented as an
experimental integration/readiness release, not as a beta maturity claim. experimental integration/readiness release, not as a beta maturity claim.
The current release is `1.0.0-beta.7`, published on 2026-05-22. It keeps the The current release is `1.0.0-beta.25`, published on 2026-05-23. It keeps the
`1.0.0-beta` language surface, includes the first post-beta tooling/install `1.0.0-beta` language surface, includes the first post-beta tooling/install
hardening bundle from `1.0.0-beta.1`, and adds the first runtime/resource hardening bundle from `1.0.0-beta.1`, and adds the first runtime/resource
foundation bundle from `1.0.0-beta.2` plus the first standard-library foundation bundle from `1.0.0-beta.2` plus the first standard-library
@ -16,11 +16,583 @@ stabilization bundle from `1.0.0-beta.3`, the first language-usability
diagnostics bundle from `1.0.0-beta.4`, and the first local diagnostics bundle from `1.0.0-beta.4`, and the first local
package/workspace discipline bundle from `1.0.0-beta.5`, plus the first package/workspace discipline bundle from `1.0.0-beta.5`, plus the first
loopback networking foundation bundle from `1.0.0-beta.6`, and the first loopback networking foundation bundle from `1.0.0-beta.6`, and the first
serialization/data-interchange foundation bundle from `1.0.0-beta.7`. serialization/data-interchange foundation bundle from `1.0.0-beta.7`, and the
first concrete type alias foundation from `1.0.0-beta.8`, plus the first
collection alias unification and generic reservation slice from
`1.0.0-beta.9`, the first developer-experience API discovery slice from
`1.0.0-beta.10`, and the local package API documentation extension from
`1.0.0-beta.11`, plus the concrete vector query and prefix parity slice from
`1.0.0-beta.12`, and the diagnostic catalog and schema policy slice from
`1.0.0-beta.13`, plus the benchmark suite catalog and metadata gate from
`1.0.0-beta.14`, plus the reserved generic collection boundary hardening and
collection ledger from `1.0.0-beta.15`, plus the string scanning and token
boundary foundation from `1.0.0-beta.16`, and the JSON primitive scalar
parsing foundation from `1.0.0-beta.17`, plus the JSON string token parsing
foundation from `1.0.0-beta.18`, the test discovery and user-project
conformance foundation from `1.0.0-beta.19`, and the string search and ASCII
trim foundation from `1.0.0-beta.20`, plus the JSON document scalar parsing
foundation from `1.0.0-beta.21`, and the run manifest execution-report
hardening slice from `1.0.0-beta.22`, plus the standard-library stability tier
ledger and catalog alignment slice from `1.0.0-beta.23`, the package manifest
identity and local dependency diagnostic hardening slice from
`1.0.0-beta.24`, and the user-project conformance matrix evidence slice from
`1.0.0-beta.25`.
## Unreleased ## Unreleased
No unreleased changes yet. No active unreleased language scope is documented here yet.
## 1.0.0-beta.25
Release label: `1.0.0-beta.25`
Release name: User Project Conformance Matrix
Release date: 2026-05-23
Status: released beta tooling/conformance evidence on the `1.0.0-beta`
language baseline.
This release adds deterministic stable-readiness evidence for ordinary
project and workspace usage over the existing `examples/projects/` and
`examples/workspaces/` inventories. The conformance matrix records
repository-local example coverage for all 43 top-level fixture roots and 655
discovered tests through ordinary Glagol project/workspace entry points.
This release adds no source-language change, standard-library helper change,
runtime behavior change, compiler-known runtime names, package manager or
registry behavior, lockfile behavior, semantic-version solving, stable
artifact-manifest schema, stable Markdown schema, stable conformance-matrix
schema, stable ABI/layout, or performance claim.
## 1.0.0-beta.24
Release label: `1.0.0-beta.24`
Release name: Package Manifest Identity And Dependency Discipline
Release date: 2026-05-23
Status: released beta package/workspace diagnostic hardening on the
`1.0.0-beta` language baseline.
This release tightens diagnostics for local package manifests only. Duplicate
package manifest keys, invalid dependency keys, and duplicate dependency keys
are explicit beta package/workspace diagnostics.
The package model remains the existing closed local workspace model:
dependencies are local path records, dependency keys must match target package
names, and package-qualified imports still resolve through the local workspace
graph.
This release adds no remote registry, lockfile, semantic-version solving,
package publishing, optional/dev/target dependencies, stable package
ABI/layout, source-language syntax or semantics, runtime behavior,
standard-library helpers, compiler-known runtime names, stable manifest schema,
stable Markdown schema, or performance claim.
## 1.0.0-beta.23
Release label: `1.0.0-beta.23`
Release name: Standard Library Stability Tier Ledger And Catalog Alignment
Release date: 2026-05-23
Status: released beta documentation/catalog clarity on the `1.0.0-beta`
language baseline.
This release adds [`STDLIB_TIERS.md`](STDLIB_TIERS.md) as the public maturity
ledger for the generated [`STDLIB_API.md`](STDLIB_API.md) standard-library API
catalog. The ledger defines the tier labels `beta-supported`, `experimental`,
and `internal`.
The beta23 ledger classifies JSON, loopback networking, random/time, and
filesystem resource-handle helpers as experimental domains. Concrete vector
modules remain beta-supported concrete lanes; this is not a generic
collections freeze and does not imply executable generics, maps, sets,
iterators, mutable vectors, slice/view APIs, runtime collection changes, or
stable ABI/layout.
This release adds no source-language syntax, standard-library helpers,
compiler-known runtime names, runtime behavior, package behavior, stable
artifact-manifest schema, stable Markdown schema, stable ABI/layout, or stable
stdlib/API compatibility freeze. It does update generated catalog output and
the release gate so tier metadata is visible and checked.
## 1.0.0-beta.22
Release label: `1.0.0-beta.22`
Release name: Run Manifest And Execution Report Hardening
Release date: 2026-05-23
Status: released beta tooling/CLI evidence hardening on the `1.0.0-beta`
language baseline.
This release does not change the Slovo language or standard-library surface.
It documents the matching Glagol tooling update: `glagol run --manifest`
artifact manifests now include an additive run-report block for the executed
program's exit status, captured stdout, captured stderr, and forwarded program
arguments.
The run-report block is beta tooling metadata under the existing
`slovo.artifact-manifest` version `1` contract. It is not a stable manifest
schema freeze and does not add source-language syntax, stdlib helpers,
compiler-known runtime names, runtime C capabilities, package/import behavior,
stable ABI/layout guarantees, or stable stdlib/API compatibility.
## 1.0.0-beta.21
Release label: `1.0.0-beta.21`
Release name: JSON Document Scalar Parsing Foundation
Release date: 2026-05-23
Status: released beta standard-library JSON document scalar parsing foundation
on the `1.0.0-beta` language baseline.
The source facade adds these source-authored `std.json` helpers:
- `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)`
Each helper trims ASCII whitespace around the whole 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
still returns `err 1` through the underlying exact parser.
This release adds no compiler-known runtime names, parser objects, object/array
parsing, recursive `JsonValue`, maps/sets, streaming, Unicode escape decoding
beyond the existing string-token behavior, embedded NUL policy, stable
ABI/layout guarantees, or stable stdlib/API freeze.
## 1.0.0-beta.20
Release label: `1.0.0-beta.20`
Release name: String Search And ASCII Trim Foundation
Release date: 2026-05-23
Status: released beta standard-library string helper foundation on the
`1.0.0-beta` language baseline.
The source facade adds these 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`
Search remains byte-oriented over current runtime strings. Empty needles match:
`index_of_option` returns `some 0`, `last_index_of_option` returns
`some (len value)`, and `contains` returns `true`. Missing needles return
`none`.
The ASCII trim helpers remove only bytes `9`, `10`, `11`, `12`, `13`, and
`32` from the requested edges. This release adds no compiler-known runtime
names, Unicode/grapheme semantics, case folding, locale-sensitive matching,
regular expressions, tokenizer APIs, language slice/view syntax, mutable
strings, stable ABI/layout guarantees, or stable stdlib/API freeze.
## 1.0.0-beta.19
Release label: `1.0.0-beta.19`
Release name: Test Discovery And User-Project Conformance Foundation
Release date: 2026-05-23
Status: released beta tooling/conformance foundation on the `1.0.0-beta`
language baseline.
The beta19 contract is tooling/conformance only. It adds deterministic test
discovery listing for:
- `glagol test --list <file|project|workspace>`
- `glagol --run-tests --list <file>` for the legacy single-file test path
List mode parses, lowers, type-checks, and discovers tests through the same
front-end path as normal test execution, then prints the discovered/selected
tests without evaluating their bodies. It preserves current single-file,
project, and workspace test ordering, honors `--filter <substring>`, and
leaves normal `glagol test` execution output unchanged.
This release does not add source-language syntax, runtime helper names, JSON
expansion, parallel test execution, retries, tags/groups, coverage reports,
event streams, stable manifest schemas, stable Markdown schemas, LSP/watch
behavior, SARIF/daemon protocols, package registries, semver solving, or
performance claims.
## 1.0.0-beta.18
Release label: `1.0.0-beta.18`
Release name: JSON String Token Parsing Foundation
Release date: 2026-05-23
Status: released beta JSON string-token parsing foundation on the
`1.0.0-beta` language baseline.
The source facade adds `std.json.parse_string_value_result`:
- signature: `(string) -> (result string i32)`
- input: one already-isolated ASCII JSON string token
- success: `ok decoded_text`
- ordinary parse failure: `err 1`
The helper requires exact surrounding quotes and no leading/trailing
whitespace. It decodes simple JSON escapes `\"`, `\\`, `\/`, `\b`, `\f`,
`\n`, `\r`, and `\t`. It rejects raw control bytes, bad escapes,
unterminated/trailing bytes, raw non-ASCII, and all `\uXXXX` escapes for this
slice.
This is string-token parsing only. It does not add object/array parsing,
recursive `JsonValue`, tokenizer APIs, generic parse APIs,
whitespace-tolerant document parsing, streaming, schema validation, Unicode
escape decoding or normalization, embedded NUL policy, stable ABI/layout, or a
stable stdlib/API freeze.
## 1.0.0-beta.17
Release label: `1.0.0-beta.17`
Release name: JSON Primitive Scalar Parsing Foundation
Release date: 2026-05-22
Status: released beta primitive JSON scalar token parsing foundation on the
`1.0.0-beta` language baseline.
`1.0.0-beta.17` adds primitive scalar JSON token parse facades to `std.json`:
- `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`
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. `parse_null_value_result` is
source-only and returns `ok true` only for exact `null`.
This is primitive scalar token parsing only. Broader JSON parsing beyond the
beta.18 ASCII string-token helper, object/array parsing, recursive `JsonValue`,
tokenizers, generic parse APIs, whitespace-tolerant document parsing,
streaming, schema validation, Unicode escape handling, stable ABI/layout, or a
stable stdlib/API freeze remain deferred.
## 1.0.0-beta.16
Release label: `1.0.0-beta.16`
Release name: String Scanning And Token Boundary Foundation
Release date: 2026-05-22
Status: released beta string scanning and token-boundary foundation on the
`1.0.0-beta` language baseline.
`1.0.0-beta.16` adds the first explicit string scanning/token-boundary helper
surface:
- `std.string.byte_at_result : (string, i32) -> (result i32 i32)`
- `std.string.slice_result : (string, i32, i32) -> (result string i32)`
- `std.string.starts_with : (string, string) -> bool`
- `std.string.ends_with : (string, string) -> bool`
- matching `lib/std/string.slo` exports and explicit local/`std.string`
examples
The helpers are byte-oriented over the current NUL-terminated runtime string
representation and observe bytes before the trailing NUL. Invalid byte indexes
or ranges return `err 1`. `slice_result` returns a runtime-owned string on
success; allocation failure may follow the existing string allocation trap
policy.
This release does not add Unicode scalar, grapheme, display-width, or locale
semantics; full JSON parsing; object or array parsing; tokenizer/scanner
objects; a language slice/view feature; mutable strings; string containers;
stable ABI/layout; or a stable stdlib/API freeze.
## 1.0.0-beta.15
Release label: `1.0.0-beta.15`
Release name: Reserved Generic Collection Boundary Hardening And Collection Ledger
Release date: 2026-05-22
Status: released beta docs/design ledger and compiler-boundary hardening
update on the `1.0.0-beta` language baseline.
`1.0.0-beta.15` documents the current concrete collection and value-family
boundary without changing executable behavior:
- Adds [`COLLECTIONS.md`](COLLECTIONS.md) as the collection/value-family
ledger.
- Links to the generated [`STDLIB_API.md`](STDLIB_API.md) catalog for exact
public helper signatures instead of duplicating generated helper counts.
- Inventories the current concrete vector, option, result, and related
option/result-returning facade surfaces.
- Records design pressure from duplicated concrete vector, option, and result
helper families.
- Defines prerequisites before executable generics, generic aliases, maps,
sets, iterators, mutable vectors, or slice/view APIs can be promoted.
- Documents current unsupported diagnostics as boundaries, not behavior
changes.
- Centralizes reserved generic/map/set diagnostics and rewords affected
reserved-boundary messages from beta.9-specific text to current-beta wording
while preserving diagnostic codes, schema, spans, expected/found values,
hints, and output shape.
This release does not change the source language, typed core, runtime,
stdlib/API surface, diagnostic output shape, diagnostic codes, diagnostic
schema, benchmark metadata schema, compiler-known runtime names, ABI/layout
behavior, optimizer behavior, or performance claims. It does not add
executable generics, generic aliases, maps, sets, iterators, mutable vectors,
slice/view APIs, new runtime names, or a stable stdlib/API freeze.
## 1.0.0-beta.14
Release label: `1.0.0-beta.14`
Release name: Benchmark Suite Catalog And Metadata Gate
Release date: 2026-05-22
Status: released beta documentation/tooling metadata update on the
`1.0.0-beta` language baseline.
`1.0.0-beta.14` documents the existing benchmark suite catalog and metadata
gate without changing the source language, runtime, standard library, API
surface, diagnostic output, compiler-known runtime names, or ABI/layout
behavior:
- Adds [`benchmarks/README.md`](../../benchmarks/README.md) as the top-level
catalog for the current ten benchmark suites:
`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`.
- Documents `python3 benchmarks/runner.py --suite-list` as the non-JSON suite
inventory command.
- Documents `python3 benchmarks/runner.py --suite-list --json` as the beta
tooling metadata form for local gates and adapters.
- States that benchmark timings are local-machine evidence only and publishes
no timing numbers.
- Records that suite-list JSON is beta tooling metadata, not a stable public
schema.
This release does not add benchmark kernels, publish timings, define
performance thresholds, define a stable JSON schema, change source-language,
runtime, stdlib/API, diagnostic-output, compiler ABI/layout, or optimizer
behavior, or make cross-machine performance claims.
## 1.0.0-beta.13
Release label: `1.0.0-beta.13`
Release name: Diagnostic Catalog And Schema Policy
Release date: 2026-05-22
Status: released beta documentation/tooling policy update on the
`1.0.0-beta` language baseline.
`1.0.0-beta.13` documents the existing diagnostic machine contract without
changing the source language, runtime, standard library, CLI, or diagnostic
output shape:
- Adds [`docs/language/DIAGNOSTICS.md`](DIAGNOSTICS.md) as the beta
`slovo.diagnostic` version `1` policy.
- Documents the S-expression and JSON relationship, required and optional
fields, severity/source/range/related-span semantics, JSON-line discipline,
source-less diagnostics, and artifact-manifest diagnostic metadata.
- Defines compatibility and migration classes for diagnostic machine fields
and codes. Human-readable prose remains beta-flexible unless a release
intentionally changes machine fields, schema/version markers, codes, or
golden fixture shape.
- Inventories the 114 current diagnostic codes covered by the 358-snapshot
golden diagnostics contract in `compiler/tests/diagnostics_contract.rs`.
This release does not add LSP/watch behavior, SARIF, daemon protocols, stable
Markdown schema, stable `1.0.0` diagnostics freeze, source-language/runtime
changes, stdlib/API changes, or ABI/layout promises.
## 1.0.0-beta.12
Release label: `1.0.0-beta.12`
Release name: Concrete Vector Query And Prefix Parity
Release date: 2026-05-22
Status: released beta stdlib/helper parity update on the `1.0.0-beta`
language baseline.
`1.0.0-beta.12` is a source-authored concrete vector helper release. It closes
small parity gaps in the current concrete vector facades without changing the
language, typed core, runtime, ABI, or compiler-known `std.vec.*` runtime
names:
- `std.vec_i64` gains `count_of`, `starts_with`, `without_prefix`,
`ends_with`, and `without_suffix`.
- `std.vec_f64` gains `count_of`.
- The helper behavior stays source-authored over the existing concrete vector
runtime names, current equality, `len`, `at`, `take`, `drop`, and recursive
helper style.
- Focused Glagol fixture coverage exercises repeated `count_of` results and
prefix/suffix empty, mismatch, exact, and longer-than-input cases where the
helper family applies.
This release does not add executable generics, maps, sets, iterators, mutable
vectors, slice/view APIs, new runtime names, new compiler-known stdlib names,
ABI/layout stability, performance claims, stable stdlib API freeze, generic
stdlib dispatch, or broad collection abstractions.
## 1.0.0-beta.11
Release label: `1.0.0-beta.11`
Release name: Local Package API Documentation
Release date: 2026-05-22
Status: released beta documentation/API-discovery update on the
`1.0.0-beta` language baseline.
`1.0.0-beta.11` extends the beta.10 API discovery lane from the generated
standard-library catalog and `glagol symbols` metadata into local package and
module documentation:
- `glagol doc <file|project|workspace> -o <dir>` includes deterministic
exported/public API sections for local source files, projects, packages, and
workspaces.
- Public API sections list exact exported function signatures, exported struct
fields, and exported enum variants with payload types.
- Module-local concrete aliases are normalized before public rendering, so
local names do not leak into package/module API docs.
- Non-exported functions, structs, enums, tests, and `(type ...)` aliases
remain omitted from the public API surface.
This release does not define a stable Markdown schema, stable stdlib/API
compatibility freeze, LSP server, watch mode, SARIF, daemon protocols,
diagnostics schema policy, executable generics, maps, sets, re-exports, globs,
hierarchical modules, package registry semantics, runtime changes, new
compiler-known runtime names, or stable ABI/layout promises.
## 1.0.0-beta.10
Release label: `1.0.0-beta.10`
Release name: Developer Experience API Discovery
Release date: 2026-05-22
Status: released beta documentation/API-discovery update on the
`1.0.0-beta` language baseline.
`1.0.0-beta.10` upgrades `docs/language/STDLIB_API.md` from an exported-name
index to a generated exported-signature catalog and adds an editor-facing
symbol metadata command:
- `scripts/render-stdlib-api-doc.js` parses each `lib/std/*.slo` module,
validates that exported helpers have matching `(fn ...)` forms, and renders
their public parameter and return types.
- Module-local concrete aliases introduced in beta.8 and used across beta.9
collection/value-family facades are normalized before rendering, so public
signatures show concrete types such as `(vec i32)`, `(option string)`, and
`(result u64 i32)` instead of local names such as `VecI32` or `ResultU64`.
- Non-exported helper functions and `(type ...)` aliases remain omitted from
the public catalog.
- `glagol symbols <file.slo|project|workspace>` emits deterministic
`slovo.symbols` S-expression metadata for modules, imports, exports,
aliases, structs, enums, functions, tests, source spans/ranges, and
workspace package labels.
This release does not add executable generics, generic aliases,
parameterized aliases, maps, sets, traits, inference, monomorphization,
iterators, runtime changes, new compiler-known runtime names, stable
ABI/layout promises, an LSP server, watch mode, SARIF, daemon protocols, or a
stable standard-library API freeze.
## 1.0.0-beta.9
Release label: `1.0.0-beta.9`
Release name: Collection Alias Unification And Generic Reservation
Release date: 2026-05-22
Status: released beta collection-alias documentation/source update on the
`1.0.0-beta` language baseline.
`1.0.0-beta.9` applies beta.8 transparent concrete aliases sparingly inside
the source-authored collection and value-family facades:
- `std.vec_i32`, `std.vec_i64`, `std.vec_f64`, `std.vec_bool`, and
`std.vec_string` name their local concrete vector family once and reuse that
alias through helper signatures.
- `std.option` and `std.result` name the current concrete option/result helper
families once and reuse those local aliases through their facade signatures.
- Public helper names, imports, exports, constructors, runtime calls, and
behavior stay concrete. Cross-module users still see normalized concrete
types such as `(vec bool)`, `(option string)`, and `(result u64 i32)`.
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 for
future diagnostics and design work only.
## 1.0.0-beta.8
Release label: `1.0.0-beta.8`
Release name: Concrete Type Alias Foundation
Release date: 2026-05-22
Status: released beta concrete type alias foundation update on the
`1.0.0-beta` language baseline.
`1.0.0-beta.8` promotes transparent type aliases for current concrete types:
```slo
(type JsonText string)
```
An alias is a module-local source name. It may be used in supported type
positions, and the compiler resolves it to the existing concrete target type
before backend and ABI behavior. This release does not add generics,
parameterized aliases, cross-module alias imports, alias re-exports, maps/sets,
stable layout names, new compiler-known runtime names, or runtime changes.
- Top-level `(type Alias TargetType)` declarations name existing concrete
supported target types without introducing new runtime representations.
- Aliases are transparent before typed-core lowering, checked import
signatures, backend layout, ABI decisions, and runtime behavior.
- The JSON source facade and explicit JSON example projects use local
`JsonText` / `JsonField` aliases as narrow source fixtures.
- Alias exports, imports, re-exports, cross-module alias visibility, generic
aliases, parameterized aliases, maps/sets, and runtime changes remain out of
scope.
## 1.0.0-beta.7 ## 1.0.0-beta.7

View File

@ -5,23 +5,138 @@ compiler implementation inside the same monorepo.
Guiding rule: the manifest wins. A feature is not accepted until it has surface syntax, typed-core meaning, lowering behavior, formatter behavior, diagnostics, and tests. Guiding rule: the manifest wins. A feature is not accepted until it has surface syntax, typed-core meaning, lowering behavior, formatter behavior, diagnostics, and tests.
Long-horizon planning lives in Long-horizon planning lives in `.llm/ROADMAP_TO_STABLE.md`. It defines the
`.llm/GENERAL_PURPOSE_LANGUAGE_ROADMAP.md`. It defines the experimental release train beyond the first real general-purpose beta Slovo contract.
release train from the historical `v2.0.0-beta.1` tag toward and beyond the
first real general-purpose beta Slovo contract.
Current stage: `1.0.0-beta.7`, released on 2026-05-22 as the first post-beta Current stage: `1.0.0-beta.25`, released on 2026-05-23 as post-beta
serialization/data-interchange foundation update. It keeps the `1.0.0-beta` language contract and user-project conformance matrix evidence. It keeps the `1.0.0-beta` language
includes the `1.0.0-beta.1` tooling hardening release, the `1.0.0-beta.2` contract and includes the `1.0.0-beta.1` tooling
runtime/resource foundation release, the `1.0.0-beta.3` standard-library hardening release, the
stabilization release, the `1.0.0-beta.4` language-usability diagnostics `1.0.0-beta.2` runtime/resource foundation release, the `1.0.0-beta.3`
release, the `1.0.0-beta.5` package/workspace discipline release, and a narrow standard-library stabilization release, the `1.0.0-beta.4`
`std.net` source facade for blocking loopback TCP client/server primitives over language-usability diagnostics release, the `1.0.0-beta.5` package/workspace
opaque `i32` handles and concrete `result` families, plus a narrow `std.json` discipline release, the `1.0.0-beta.6` loopback networking foundation,
source facade for compact JSON text construction. JSON parsing, recursive JSON `1.0.0-beta.7` compact JSON text construction, `1.0.0-beta.8` concrete type
values, maps/sets, DNS, TLS, UDP, async IO, non-loopback binding, HTTP aliases, `1.0.0-beta.9` collection alias unification and generic reservation,
frameworks, rich host-error ADTs, stable ABI/layout, and a stable `1.0.0-beta.10` API discovery, `1.0.0-beta.11` local package API
standard-library API freeze remain deferred. documentation, `1.0.0-beta.12` concrete vector helper parity,
`1.0.0-beta.13` diagnostic catalog and schema policy, `1.0.0-beta.14`
benchmark suite catalog and metadata gate, `1.0.0-beta.15` reserved generic
collection boundary hardening and collection ledger, and `1.0.0-beta.16`
string scanning and token boundary helpers, `1.0.0-beta.17` JSON primitive
scalar token parsing, `1.0.0-beta.18` JSON string token parsing,
`1.0.0-beta.19` test discovery and user-project conformance tooling, and
`1.0.0-beta.20` string search and ASCII trim helpers, plus
`1.0.0-beta.21` JSON document scalar parsing helpers, and
`1.0.0-beta.22` run manifest execution-report hardening, plus
`1.0.0-beta.23` standard-library stability tier documentation,
`1.0.0-beta.24` package manifest/dependency diagnostic hardening, and
`1.0.0-beta.25` user-project conformance matrix evidence.
`1.0.0-beta.16` adds `std.string` source facades and examples for
`byte_at_result`, `slice_result`, `starts_with`, and `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 may use the existing string allocation trap policy.
`1.0.0-beta.16` does not add Unicode scalar, grapheme, display-width, or locale
semantics; full JSON parsing; object/array parsing; tokenizer objects;
language slice/view syntax; mutable strings; stable ABI/layout; performance
claims; or a stable stdlib/API freeze.
The current released JSON stage adds primitive scalar JSON token parse facades
for booleans, concrete numeric primitives, exact `null`, one narrow ASCII JSON
string-token helper, and scalar JSON document helpers that accept leading and
trailing ASCII whitespace. Object/array parsing, recursive JSON values,
tokenizers, schema validation, streaming, Unicode escape
decoding/normalization beyond the existing string-token helper, embedded NUL
policy, stable ABI/layout, and stable stdlib/API freeze remain deferred.
Full JSON parsing, object/array parsing, recursive JSON values,
executable generics, generic aliases, parameterized aliases, cross-module
alias visibility, maps/sets, traits, inference, monomorphization, iterators,
runtime changes for generic collections, DNS, TLS, UDP, async IO,
non-loopback binding, HTTP frameworks, rich host-error ADTs, stable ABI/layout,
stable Markdown schema, stable stdlib/API compatibility freeze, LSP/watch,
SARIF/daemon protocols, stable `1.0.0` diagnostics freeze,
re-exports/globs/hierarchical modules, mutable vectors, language slice/view
APIs, additional runtime names, Unicode/grapheme string semantics, timing
publication, performance claims, stable benchmark JSON schema, and package
registry semantics remain deferred.
`1.0.0-beta.19` is a tooling/conformance slice, not a new source-language
feature. It adds the `glagol test --list <file|project|workspace>` command
plus legacy `glagol --run-tests --list <file>`: parse, lower, type-check, and
discover tests through the same front-end path as normal test execution, then
list the discovered/selected tests without evaluating test bodies. The mode
preserves current single-file, project, and workspace ordering, honors
`--filter <substring>`, and leaves normal `glagol test` execution behavior
unchanged.
The beta19 tooling scope does not claim executable generics, maps/sets,
iterators, runtime helper names, source-language syntax, JSON expansion,
parallel test execution, retries, tags/groups, coverage/event streams,
LSP/watch protocols, SARIF/daemon protocols, stable artifact-manifest or
Markdown schemas, stable benchmark JSON schema, stable `1.0.0` diagnostics
freeze, stable standard-library/API compatibility freeze, registry semantics,
semver solving, performance claims, mutable vectors, language slice/view APIs,
additional runtime names, or Unicode/grapheme semantics.
`1.0.0-beta.20` adds source-authored `std.string` helpers for byte-oriented
`contains`, first/last index option search, and ASCII edge trimming over bytes
`9`, `10`, `11`, `12`, `13`, and `32`. Empty needles match at first index `0`
and last index `(len value)`. The scope adds no compiler-known runtime names,
Unicode/grapheme semantics, case folding, regexes, tokenizer APIs, language
slice/view syntax, mutable strings, stable ABI/layout guarantees, or stable
stdlib/API freeze.
`1.0.0-beta.21` adds source-authored
`std.json.parse_*_document_result` helpers for string, bool, `i32`, `u32`,
`i64`, `u64`, `f64`, and exact `null` documents by trimming ASCII whitespace
around the whole document with `std.string.trim_ascii`, then delegating to the
existing exact value-token parser for each scalar family. The scope keeps
object/array parsing, recursive JSON values, tokenizer objects, maps/sets,
streaming, new compiler-known runtime names, Unicode escape decoding beyond
the existing string-token helper, embedded NUL policy, stable ABI/layout, and
stable stdlib/API freeze deferred.
`1.0.0-beta.22` is tooling/CLI evidence hardening, not a language feature. It
documents the matching Glagol `run --manifest` update: run-mode artifact
manifests gain an additive run-report block for executed-program exit status,
captured stdout, captured stderr, and forwarded program arguments. The block is
beta tooling metadata under the existing `slovo.artifact-manifest` version `1`
contract. It is not a stable schema freeze and adds no language syntax,
standard-library helpers, runtime capabilities, package/import behavior,
stable ABI/layout, or stable stdlib/API guarantees.
`1.0.0-beta.23` is documentation/catalog clarity, not a language or stdlib
behavior feature. It adds [`STDLIB_TIERS.md`](STDLIB_TIERS.md) as the public
stability-tier ledger beside the generated [`STDLIB_API.md`](STDLIB_API.md)
signature catalog. The ledger defines `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. It adds no source-language syntax, helpers, runtime names, runtime
behavior, stable manifest schema, stable Markdown schema, stable ABI/layout,
or stable stdlib/API freeze. It does update generated catalog output and the
release gate so tier metadata is visible and checked.
`1.0.0-beta.24` is package/workspace diagnostic hardening, not a language,
runtime, package-manager, or stdlib behavior feature. It makes duplicate
package manifest keys, invalid dependency keys, and duplicate dependency keys
explicit diagnostics while keeping the existing closed local workspace model.
It adds no remote registry, lockfile, semantic-version solving, package
publishing, optional/dev/target dependencies, stable package ABI/layout,
source-language change, runtime change, or stdlib change.
`1.0.0-beta.25` is tooling/conformance evidence, not a language, stdlib,
runtime, package-manager, schema-freeze, or performance feature. It adds a
deterministic user-project conformance matrix over the existing
`examples/projects/` and `examples/workspaces/` inventories so ordinary
project and workspace usage has stable-readiness evidence. The release covers
all 43 top-level fixture roots and 655 discovered tests. It adds no
source-language change, standard-library helper change, runtime behavior
change, package manager or registry behavior, lockfile behavior,
semantic-version solving, stable schema freeze, or performance claim.
The final experimental precursor scope is `exp-125`, defined in The final experimental precursor scope is `exp-125`, defined in
`.llm/EXP_125_UNSIGNED_U32_U64_NUMERIC_AND_STDLIB_BREADTH_ALPHA.md`. Its `.llm/EXP_125_UNSIGNED_U32_U64_NUMERIC_AND_STDLIB_BREADTH_ALPHA.md`. Its
@ -2989,8 +3104,9 @@ exp-20 f64 numeric primitive alpha decisions:
## Phase 8: Improve v1 Tooling Contracts ## Phase 8: Improve v1 Tooling Contracts
- [x] Define `slovo.diagnostic` version `1` as the stable v1 machine - [x] Define `slovo.diagnostic` version `1` as the current v1 machine
diagnostic schema. diagnostic schema, with beta policy and catalog details in
[`docs/language/DIAGNOSTICS.md`](DIAGNOSTICS.md).
- [x] Define the v1 direction for preserving source spans through LLVM IR - [x] Define the v1 direction for preserving source spans through LLVM IR
emission while deferring stable debug metadata and source-map files. emission while deferring stable debug metadata and source-map files.
- [x] Define `slovo.artifact-manifest` version `1` for compiler outputs and - [x] Define `slovo.artifact-manifest` version `1` for compiler outputs and

View File

@ -1,9 +1,22 @@
# Slovo v1 Specification # Slovo v1 Specification
Status: living beta contract for `1.0.0-beta`, with the post-beta Status: living beta contract for `1.0.0-beta`, with the post-beta
`1.0.0-beta.1` tooling/install update and `1.0.0-beta.2` runtime/resource `1.0.0-beta.1` tooling/install update, `1.0.0-beta.2` runtime/resource
foundation update. The language contract integrates foundation update, `1.0.0-beta.10` developer-experience API discovery update,
promoted language slices through `exp-125` and the historical publication `1.0.0-beta.11` local package API documentation update, and `1.0.0-beta.12`
concrete vector query and prefix parity update, and `1.0.0-beta.13`
diagnostic catalog and schema policy update, and `1.0.0-beta.14` benchmark
suite catalog and metadata gate, `1.0.0-beta.15` reserved generic collection
boundary hardening and collection ledger, `1.0.0-beta.16` string scanning
and token boundary foundation, `1.0.0-beta.17` JSON primitive scalar parsing
foundation, `1.0.0-beta.18` JSON string token parsing foundation,
`1.0.0-beta.19` test discovery and user-project conformance tooling, and
`1.0.0-beta.20` string search and ASCII trim foundation, and
`1.0.0-beta.21` JSON document scalar parsing foundation, and
`1.0.0-beta.22` run manifest and execution-report hardening, and
`1.0.0-beta.23` standard-library stability tier ledger and catalog alignment.
The language contract integrates promoted language slices through `exp-125`
and the historical publication
baseline through `exp-123`. `1.0.0-beta` is the first real general-purpose baseline through `exp-123`. `1.0.0-beta` is the first real general-purpose
beta release. `exp-125` completed the unsigned numeric and stdlib breadth beta release. `exp-125` completed the unsigned numeric and stdlib breadth
precursor scope, `exp-124` is the last tagged experimental alpha language precursor scope, `exp-124` is the last tagged experimental alpha language
@ -107,20 +120,180 @@ Current v1 release surface and explicit experimental targets:
v1.1-v1.7 surface is supported for small real flat local projects through v1.1-v1.7 surface is supported for small real flat local projects through
`glagol new`, `check`, `fmt --check/--write`, `test`, `build`, `doc`, `glagol new`, `check`, `fmt --check/--write`, `test`, `build`, `doc`,
artifact manifests, JSON diagnostics, and the release-gate script artifact manifests, JSON diagnostics, and the release-gate script
- `1.0.0-beta.8` concrete type alias target: top-level
`(type Alias TargetType)` declarations name existing supported concrete
target types and are resolved before typed-core lowering, backend layout, ABI
decisions, and runtime behavior; no generic aliases, parameterized
aliases, alias exports/imports/re-exports, cross-module alias visibility,
maps/sets, or runtime changes are included, once the matching Glagol gates
pass
- `1.0.0-beta.9` collection alias unification and generic reservation target:
existing concrete vector, option, and result source facades may use
module-local concrete aliases internally while exported helper behavior and
cross-module types remain concrete; executable generics, maps, sets, traits,
inference, monomorphization, iterators, stable ABI/layout, and runtime
changes remain out of scope, once the matching Glagol gates pass
- `1.0.0-beta.10` developer-experience API discovery target: the generated
`docs/language/STDLIB_API.md` catalog lists exact exported helper
signatures from `lib/std/*.slo`, normalizes module-local concrete aliases to
public concrete types, and omits non-exported helpers and `(type ...)`
aliases; `glagol symbols` emits deterministic editor-facing source metadata
for files, projects, and workspaces; this is beta API discovery only, not
executable generics, maps, sets, runtime changes, LSP/watch, or a stable
`1.0.0` standard-library freeze
- `1.0.0-beta.11` local package API documentation target:
`glagol doc <file|project|workspace> -o <dir>` includes deterministic
exported/public API sections for local packages and modules with exact
exported function signatures, exported struct fields, exported enum
variants/payload types, non-export filtering, and module-local alias
normalization; this remains beta API discovery only, not a stable Markdown
schema, stable stdlib/API
compatibility freeze, LSP/watch contract, SARIF/daemon protocol, diagnostics
schema policy, executable generics, maps/sets, re-exports, globs,
hierarchical modules, or registry semantics
- `1.0.0-beta.12` concrete vector query and prefix parity target:
source-authored `std.vec_i64` gains `count_of`, `starts_with`,
`without_prefix`, `ends_with`, and `without_suffix`; source-authored
`std.vec_f64` gains `count_of`; this is helper parity over the current
concrete vector runtime names only, not a source-language/runtime change,
executable generics, maps/sets, iterators, mutable vectors, slice/view APIs,
new runtime names, ABI/layout stability, performance claims, or a stable
stdlib API freeze
- `1.0.0-beta.13` diagnostic catalog and schema policy target:
[`docs/language/DIAGNOSTICS.md`](DIAGNOSTICS.md) documents the existing
`slovo.diagnostic` version `1` beta policy, S-expression/JSON relationship,
required and optional fields, JSON-line discipline, source-less diagnostics,
artifact-manifest diagnostic metadata, compatibility/migration classes, and
current golden diagnostic code catalog; this is docs/tooling policy only, not
a source-language/runtime change, LSP/watch contract, SARIF/daemon protocol,
stable Markdown schema, or stable `1.0.0` diagnostics freeze
- `1.0.0-beta.14` benchmark suite catalog and metadata target:
[`benchmarks/README.md`](../../benchmarks/README.md) documents the existing
benchmark suite inventory and root suite-list commands:
`python3 benchmarks/runner.py --suite-list` and
`python3 benchmarks/runner.py --suite-list --json`; benchmark timing remains
local-machine evidence only, and suite-list JSON is beta tooling metadata
only, not a stable public schema; this is docs/tooling metadata only, not new
benchmark kernels, timing publication, performance thresholds,
source-language/runtime/stdlib/API changes, diagnostic-output changes, or
ABI/layout changes
- `1.0.0-beta.15` reserved generic collection boundary hardening target:
[`COLLECTIONS.md`](COLLECTIONS.md) documents the current concrete collection
and value-family boundary by linking to the generated
[`STDLIB_API.md`](STDLIB_API.md) catalog for exact helper signatures,
recording design pressure from duplicated concrete vector/option/result
families, defining promotion prerequisites for executable generics, generic
aliases, maps, sets, iterators, mutable vectors, and slice/view APIs, and
treating current unsupported diagnostics as boundaries; this is docs/design
and compiler-boundary hardening only, not a source-language, runtime,
stdlib/API, diagnostic output shape, diagnostic code, diagnostic schema,
diagnostic span, expected/found value, hint, benchmark metadata schema,
ABI/layout, performance-claim, or stable stdlib/API change; affected
reserved-boundary messages are reworded from beta.9-specific text to
current-beta wording
- `1.0.0-beta.16` string scanning and token boundary target:
`std.string.byte_at_result`, `std.string.slice_result`,
`std.string.starts_with`, and `std.string.ends_with` provide byte-oriented
scanning helpers over the current NUL-terminated runtime string
representation; invalid indexes and ranges return `err 1`, substring
allocation failure may follow the existing string allocation trap policy, and
this target does not add Unicode/grapheme/display-width semantics, JSON
parsing, object/array parsing, tokenizer objects, language slice/view syntax,
mutable strings, stable ABI/layout, performance claims, or a stable
stdlib/API freeze
- `1.0.0-beta.17` JSON primitive scalar parsing target:
`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`, and `std.json.parse_null_value_result`
consume one already-isolated primitive scalar token and return concrete
`(result ... i32)` values; JSON document parsing beyond the beta21 scalar
helpers, string/object/array parsing, tokenizers, recursive `JsonValue`,
Unicode escape handling, stable ABI/layout, performance claims, and stable
stdlib/API freeze remain out of scope
- `1.0.0-beta.18` JSON string token parsing target:
`std.json.parse_string_value_result : (string) -> (result string i32)`
consumes one already-isolated ASCII JSON string token with exact quotes and
no leading/trailing whitespace, decodes simple JSON escapes, returns
`err 1` for ordinary parse failure, and rejects raw control bytes, bad
escapes, unterminated/trailing bytes, raw non-ASCII, and all `\uXXXX` escapes
for this slice; JSON document parsing beyond the beta21 scalar helpers,
object/array parsing, tokenizer APIs, recursive `JsonValue`, Unicode escape
decoding/normalization, embedded NUL policy, stable ABI/layout, performance
claims, and stable stdlib/API freeze remain out of scope
- `1.0.0-beta.19` test discovery and user-project conformance target:
`glagol test --list <file|project|workspace>` and legacy
`glagol --run-tests --list <file>` list checked/discovered tests without
executing test bodies; list mode preserves current test ordering, honors
`--filter <substring>`, and remains beta tooling rather than a stable
schema. This target does not add source-language syntax, runtime helper
names, JSON expansion, parallel test execution, retries, tags/groups,
coverage/event streams, stable artifact-manifest or Markdown schemas,
LSP/watch behavior, SARIF/daemon protocols, package registries, semver
solving, or performance claims
- `1.0.0-beta.20` string search and ASCII trim target:
source-authored `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` compose over the
existing byte-oriented string primitives; search is byte-oriented, missing
needles return `none`, empty needles match at first index `0` and last index
`(len value)`, and trim removes only bytes `9`, `10`, `11`, `12`, `13`, and
`32` from the requested edges. This target does not add compiler-known
runtime names, Unicode/grapheme semantics, case folding, locale-sensitive
matching, regex, tokenizer APIs, language slice/view syntax, mutable
strings, stable ABI/layout, performance claims, or stable stdlib/API freeze
- `1.0.0-beta.21` JSON document scalar parsing:
source-authored `std.json.parse_string_document_result`,
`std.json.parse_bool_document_result`,
`std.json.parse_i32_document_result`,
`std.json.parse_u32_document_result`,
`std.json.parse_i64_document_result`,
`std.json.parse_u64_document_result`,
`std.json.parse_f64_document_result`, and
`std.json.parse_null_document_result` trim ASCII whitespace around the whole
document with `std.string.trim_ascii`, then delegate to the existing exact
value-token parsers for one scalar JSON document. This target does not add
compiler-known runtime names, object/array parsing, recursive `JsonValue`,
parser/tokenizer objects, maps/sets, streaming, Unicode escape decoding
beyond the existing string-token behavior, embedded NUL policy,
ABI/layout guarantees, performance claims, or stable stdlib/API freeze
- `1.0.0-beta.22` run manifest and execution-report hardening:
`glagol run --manifest` artifact manifests include an additive run-report
block after a supported program executes. The block records the executed
program exit status, captured stdout, captured stderr, and forwarded
program arguments. This target is beta tooling evidence only; it does not
add source-language syntax, stdlib helpers, compiler-known runtime names,
runtime C capabilities, package/import behavior, stable artifact-manifest
schema guarantees, stable ABI/layout, or stable stdlib/API freeze
- `1.0.0-beta.23` standard-library stability tier ledger and catalog
alignment:
[`STDLIB_TIERS.md`](STDLIB_TIERS.md) defines the public tier labels
`beta-supported`, `experimental`, and `internal` beside the generated
[`STDLIB_API.md`](STDLIB_API.md) signature catalog. JSON, loopback
networking, random/time, and filesystem resource-handle helpers are
experimental domains. Concrete vector modules remain beta-supported concrete
lanes, not a generic collections freeze. This target does not add
source-language syntax, stdlib helpers, compiler-known runtime names,
runtime behavior, stable artifact-manifest schema, stable Markdown schema,
stable ABI/layout, or stable stdlib/API freeze. It does update generated
catalog output and the release gate so tier metadata is visible and checked
- `exp-1` owned runtime strings: compiler-known `std.string.concat` accepts two - `exp-1` owned runtime strings: compiler-known `std.string.concat` accepts two
`string` values and returns an immutable runtime-owned `string`; existing `string` values and returns an immutable runtime-owned `string`; existing
string equality, length, printing, locals, parameters, returns, and calls work string equality, length, printing, locals, parameters, returns, and calls work
over both literal-backed and runtime-owned strings over both literal-backed and runtime-owned strings
- `exp-2` collections alpha plus `exp-94` vec-i64 baseline, `exp-99` - `exp-2` collections alpha plus `exp-94` vec-i64 baseline, `exp-99`
vec-string baseline, and `exp-103` vec-f64 baseline: concrete growable vec-string baseline, `exp-103` vec-f64 baseline, and `exp-104` vec-bool
vector types `(vec i32)`, `(vec i64)`, `(vec f64)`, and `(vec string)`, baseline: concrete growable vector types `(vec i32)`, `(vec i64)`,
`(vec f64)`, `(vec bool)`, and `(vec string)`,
compiler-known compiler-known
`std.vec.i32.empty`, `std.vec.i32.append`, `std.vec.i32.len`, `std.vec.i32.empty`, `std.vec.i32.append`, `std.vec.i32.len`,
`std.vec.i32.index`, compiler-known `std.vec.i64.empty`, `std.vec.i32.index`, compiler-known `std.vec.i64.empty`,
`std.vec.i64.append`, `std.vec.i64.len`, and `std.vec.i64.index`, `std.vec.i64.append`, `std.vec.i64.len`, and `std.vec.i64.index`,
compiler-known `std.vec.f64.empty`, `std.vec.f64.append`, compiler-known `std.vec.f64.empty`, `std.vec.f64.append`,
`std.vec.f64.len`, and `std.vec.f64.index`, `std.vec.f64.len`, and `std.vec.f64.index`, compiler-known
`std.vec.bool.empty`, `std.vec.bool.append`, `std.vec.bool.len`, and
`std.vec.bool.index`,
compiler-known `std.vec.string.empty`, `std.vec.string.append`, compiler-known `std.vec.string.empty`, `std.vec.string.append`,
`std.vec.string.len`, and `std.vec.string.index`, vector equality with `=` `std.vec.string.len`, and `std.vec.string.index`, vector equality with `=`
on same-family operands, and immutable vector flow through locals, on same-family operands, and immutable vector flow through locals,
@ -928,6 +1101,22 @@ structs, functions, and tests. It is a documentation generator, not a semantic
reflection API, and it does not expose stable typed-core, debug metadata, reflection API, and it does not expose stable typed-core, debug metadata,
source-map, ABI, layout, runtime reflection, or compiler-internal contracts. source-map, ABI, layout, runtime reflection, or compiler-internal contracts.
As of `1.0.0-beta.11`, `glagol doc <file|project|workspace> -o <dir>` also
includes deterministic exported/public API sections for local packages and
modules. These sections render 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 normalize module-local concrete aliases before rendering public
types.
`glagol symbols <file.slo|project|workspace>` emits deterministic
machine-readable S-expression metadata using the `slovo.symbols` schema. It
reports module paths, package labels when available, imports, exports,
module-local type aliases, structs, enums, functions, tests, and source
spans/ranges. It is an editor-integration building block, not an LSP server,
watch protocol, semantic reflection API, debug metadata, source-map contract,
ABI/layout promise, or stable compiler-internal contract.
The repository must provide a local release-gate script or documented command The repository must provide a local release-gate script or documented command
entry point that runs the full v1 release gate for the matching Slovo and entry point that runs the full v1 release gate for the matching Slovo and
Glagol release without network, tag, push, or release-publication side effects. Glagol release without network, tag, push, or release-publication side effects.
@ -1042,6 +1231,25 @@ solving, package publishing, archive formats, optional/dev/target
dependencies, feature flags, package build scripts, or stable package dependencies, feature flags, package build scripts, or stable package
ABI/layout promises. ABI/layout promises.
`1.0.0-beta.11` extends the generated local-package documentation surface, but
not the package model itself. `glagol doc <file|project|workspace> -o <dir>`
includes deterministic exported/public API sections for local modules and
workspace packages:
- exported functions are rendered with exact parameter and return types
- exported structs are rendered with exported field names and field types
- exported enums are rendered with exported variant names and payload types
- non-exported functions, structs, enums, tests, and `(type ...)` aliases are
excluded from the public API sections
- module-local concrete aliases are normalized to their concrete public target
types before rendering
The generated Markdown remains a beta documentation format. This does not
define a stable Markdown schema, stable stdlib/API compatibility freeze,
LSP/watch behavior, SARIF or daemon protocols, diagnostics schema policy,
executable generics, maps/sets, re-exports, glob imports, hierarchical modules,
registry semantics, runtime behavior, or stable ABI/layout.
### 4.4.4 Post-Beta Networking Foundation ### 4.4.4 Post-Beta Networking Foundation
Status: released in `1.0.0-beta.6`. Status: released in `1.0.0-beta.6`.
@ -1091,6 +1299,14 @@ std.json.quote_string: (string) -> string
std.json.null_value: () -> string std.json.null_value: () -> string
std.json.bool_value: (bool) -> string std.json.bool_value: (bool) -> string
std.json.i32_value/u32_value/i64_value/u64_value/f64_value std.json.i32_value/u32_value/i64_value/u64_value/f64_value
std.json.parse_string_value_result: (string) -> (result string i32)
std.json.parse_bool_value_result: (string) -> (result bool i32)
std.json.parse_i32_value_result/parse_u32_value_result/parse_i64_value_result/parse_u64_value_result/parse_f64_value_result
std.json.parse_null_value_result: (string) -> (result bool i32)
std.json.parse_string_document_result: (string) -> (result string i32)
std.json.parse_bool_document_result: (string) -> (result bool i32)
std.json.parse_i32_document_result/parse_u32_document_result/parse_i64_document_result/parse_u64_document_result/parse_f64_document_result
std.json.parse_null_document_result: (string) -> (result bool i32)
std.json.field_string/field_bool/field_i32/field_u32/field_i64/field_u64/field_f64/field_null std.json.field_string/field_bool/field_i32/field_u32/field_i64/field_u64/field_f64/field_null
std.json.array0/array1/array2/array3 std.json.array0/array1/array2/array3
std.json.object0/object1/object2/object3 std.json.object0/object1/object2/object3
@ -1102,12 +1318,316 @@ character iteration. It returns a complete compact JSON string literal,
including surrounding quotes. The source helpers compose already-encoded JSON including surrounding quotes. The source helpers compose already-encoded JSON
fragments with compact comma/colon separators. fragments with compact comma/colon separators.
This release is not a complete JSON or serialization contract. JSON parsing, The Slovo-facing `1.0.0-beta.17` JSON foundation adds primitive scalar token
recursive JSON values, maps/sets, generic collections, streaming encoders, parse facades only: exact boolean and numeric primitive token conversion
schema validation, Unicode normalization, stable text encoding policy beyond through concrete `(result ... i32)` values, plus `parse_null_value_result`
returning `ok true` only for exact `null` and `err 1` otherwise.
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.
The `1.0.0-beta.18` JSON foundation adds
`parse_string_value_result` for one already-isolated ASCII JSON string token.
The token must start and end with quotes and contain no leading or trailing
whitespace. It decodes the simple JSON escapes `\"`, `\\`, `\/`, `\b`, `\f`,
`\n`, `\r`, and `\t`. It rejects raw control bytes, bad escapes,
unterminated or trailing bytes, raw non-ASCII, and all `\uXXXX` escapes for
this slice.
The `1.0.0-beta.21` JSON foundation adds source-authored
`parse_*_document_result` helpers for string, bool, concrete numeric
primitives, and exact `null`. Each helper accepts leading and trailing ASCII
whitespace around one scalar JSON document by composing with
`std.string.trim_ascii`, then delegates to the matching exact value-token
parser. Trailing non-whitespace still fails through the exact parser.
This is not a complete JSON or serialization contract. JSON parsing beyond the
scalar document helpers, object/array parsing, recursive JSON values, maps/sets,
generic collections, tokenizer objects, streaming decoders or encoders, schema
validation, Unicode escape decoding or normalization beyond the existing
string-token behavior, embedded NUL policy, stable text encoding policy beyond
the current null-terminated runtime string ABI, stable runtime helper symbols, the current null-terminated runtime string ABI, stable runtime helper symbols,
and stable standard-library API guarantees remain deferred. and stable standard-library API guarantees remain deferred.
### 4.4.6 Post-Beta Concrete Type Alias Foundation
Status: released in `1.0.0-beta.8` with matching Glagol parser, checker,
formatter, diagnostics, fixture, documentation, and promotion gates.
`1.0.0-beta.8` promotes transparent aliases for existing concrete type forms:
```slo
(type JsonText string)
(type Scores (vec i32))
(type MaybeName (option string))
(type ReadResult (result string i32))
```
The declaration form is exactly `(type Alias TargetType)` at top level. `Alias`
must be a single user type name. `TargetType` must resolve to an already
supported concrete Slovo type: direct scalar and string types, current fixed
array families, current concrete vec families, current concrete option/result
families, current known struct names, or current enum names where that type is
already legal in the use position.
Aliases are transparent. Using `Alias` in a type position is equivalent to
using its resolved target type. The resolved target controls value flow,
operators, constructors, `match`, field access, imports of functions that use
the type, lowering, runtime behavior, and diagnostics. The alias does not create
a nominal type, wrapper, cast, runtime tag, layout name, hosted symbol, or C ABI
boundary.
Alias declarations are module-local. They may be used by later declarations in
the same module and by exported functions or structs after normalization, but
the alias name itself is not exportable, importable, or re-exportable. A module
that imports a function whose signature used a local alias sees the normalized
concrete target type, not the alias name.
Name resolution for type positions checks local concrete aliases alongside
local struct and enum type names before applying builtin/concrete type forms.
Duplicate alias, struct, enum, function, import-list, export-list, local, or
parameter names remain diagnostics under the existing duplicate-name policy.
Aliases must not shadow builtins, compiler-known standard-runtime names, or
reserved unsafe heads.
Formatter output keeps aliases as one-line top-level declarations:
```slo
(type JsonText string)
```
An alias target that is already a parenthesized concrete type form stays inline,
for example `(type JsonItems (vec string))`. Comments are allowed around the
top-level alias declaration under the existing full-line comment rules, not
inside the declaration form.
Required diagnostics include malformed alias declarations, duplicate alias
names, unknown target types, unsupported target types, cyclic aliases,
parameterized alias attempts, exported aliases, imported aliases, and
cross-module alias references. An implementation may use precise diagnostic
codes, but it must not silently treat an alias as `string`, `i32`, or any other
fallback type after a failed resolution.
This target explicitly does not add generic aliases, parameterized aliases,
alias type parameters, higher-kinded aliases, alias re-exports, cross-module
alias imports, import aliases, glob imports, maps/sets, alias-driven overloads,
implicit casts, new runtime helpers, standard-runtime names, stable ABI/layout
promises, or a stable standard-library API freeze.
### 4.4.7 Post-Beta Collection Alias Unification And Generic Reservation
Status: released in `1.0.0-beta.9` as a Slovo stdlib/docs contract update on
top of the beta.8 alias semantics.
`1.0.0-beta.9` applies beta.8 concrete aliases to the source-authored
collection/value-family facades where they remove repeated concrete type
spellings:
- `std.vec_i32`, `std.vec_i64`, `std.vec_f64`, `std.vec_bool`, and
`std.vec_string` use one module-local alias for their concrete vector family.
- `std.option` and `std.result` use module-local aliases for the current
concrete option/result helper families.
These aliases are not public standard-library API names. They are normalized to
their concrete targets for imports, typed-core lowering, backend layout, ABI
decisions, runtime behavior, and generated cross-module signatures. Current
vectors, options, and results remain concrete families such as `(vec bool)`,
`(option string)`, and `(result u64 i32)`.
This target reserves generic and collection-unification spelling space through
diagnostics and documentation only. It does not add executable generics,
generic aliases, parameterized aliases, maps, sets, traits, inference,
monomorphization, iterators, new runtime helpers, stable ABI/layout promises,
or a stable standard-library API freeze.
### 4.4.8 Post-Beta Local Package API Documentation
Status: released in `1.0.0-beta.11` as a documentation/API-discovery update on
top of the beta.10 discovery lane.
`1.0.0-beta.11` extends `glagol doc <file|project|workspace> -o <dir>` so the
generated Markdown includes deterministic exported/public API sections for
local source files, projects, packages, and workspaces. The public API surface
is derived from explicit module exports and local package boundaries.
The public API sections include:
- exact exported function signatures, including parameter names, parameter
types, and return types
- exported struct field names and field types
- exported enum variant names and payload types, including payloadless variants
and current single-payload variants
- deterministic ordering that follows the existing module/package
documentation ordering
Non-exported functions, structs, enums, tests, and `(type ...)` aliases are
not part of the public API sections. A declaration can still appear in other
source-structure summaries when those summaries already exist, but it must not
be presented as exported/public API unless the module export list exposes it.
Module-local concrete aliases are normalized before public rendering. Public
docs show concrete target types such as `(vec i32)`, `(option string)`, and
`(result u64 i32)` rather than private alias names introduced by the
documented module. This mirrors beta.10 `lib/std` catalog behavior for local
packages and modules.
This is not a stable documentation schema. It does not freeze Markdown
headings, anchors, file names, machine parsing contracts, stable stdlib/API
compatibility freeze, LSP/watch behavior, SARIF or daemon protocols,
diagnostics schema policy, executable generics, maps/sets, re-exports, glob
imports, hierarchical modules, registry semantics, runtime behavior, package
ABI, or layout.
### 4.4.9 Post-Beta Concrete Vector Query And Prefix Parity
Status: released in `1.0.0-beta.12` as a source-authored stdlib/helper parity
update on the existing concrete vector surface.
`1.0.0-beta.12` closes narrow helper gaps in the concrete vector facades:
- `std.vec_i64` exports `count_of`, `starts_with`, `without_prefix`,
`ends_with`, and `without_suffix`.
- `std.vec_f64` exports `count_of`.
The helpers are ordinary Slovo source helpers. They build on the existing
compiler-known concrete vector runtime names, equality, `len`, `at`, and
already staged source helpers such as `take` and `drop`. They do not add new
source-language syntax, typed-core forms, runtime calls, compiler-known stdlib
names, ABI/layout commitments, or performance guarantees.
The helper semantics follow the existing concrete vector family conventions:
`count_of(values,target)` returns the number of elements equal to `target`;
`starts_with(values,prefix)` and `ends_with(values,suffix)` accept empty,
exact, and shorter matching vectors and reject longer or mismatched vectors;
`without_prefix` and `without_suffix` return the original vector when the
prefix/suffix does not match and return the remaining vector when it does.
This target explicitly does not add executable generics, generic vector
dispatch, maps, sets, iterators, mutable vectors, slice/view APIs, new runtime
names, stable ABI/layout promises, performance claims, or a stable stdlib API
freeze.
### 4.4.10 Post-Beta Diagnostic Catalog And Schema Policy
Status: released in `1.0.0-beta.13` as a docs/tooling policy update for the
existing diagnostic surface.
`1.0.0-beta.13` adds
[`docs/language/DIAGNOSTICS.md`](DIAGNOSTICS.md) as the central beta policy for
`slovo.diagnostic` version `1`. The document records the S-expression and JSON
encodings, source-attached and source-less diagnostic field rules,
severity/source/range/related-span semantics, JSON-line discipline,
artifact-manifest diagnostic metadata, diagnostic compatibility classes, and
the current golden diagnostic code catalog.
The source language, typed core, runtime, standard library, compiler-known
runtime names, ABI/layout behavior, CLI behavior, and diagnostic output shape
are unchanged by this target. Human-readable diagnostic prose remains
beta-flexible; schema names, versions, machine fields, diagnostic codes, source
span/range semantics, JSON-line discipline, and golden fixture shape change
only intentionally through the documented migration policy.
This target explicitly does not add LSP/watch behavior, SARIF output, daemon
protocols, stable Markdown schema, stable `1.0.0` diagnostics freeze,
source-map/debug-metadata contracts, localized diagnostic text, automated
machine fix-its, or a compiler-emitted diagnostic catalog artifact.
### 4.4.11 Post-Beta Benchmark Suite Catalog And Metadata Gate
Status: released in `1.0.0-beta.14` as a docs/tooling metadata update for the
existing benchmark suite.
`1.0.0-beta.14` adds
[`benchmarks/README.md`](../../benchmarks/README.md) as the top-level
benchmark suite catalog. The catalog records the current suite inventory,
base/hot-loop checksum metadata, local evidence policy, and explicit
exclusions.
The root suite-list commands are documented as:
```bash
python3 benchmarks/runner.py --suite-list
python3 benchmarks/runner.py --suite-list --json
```
The non-JSON listing is local review output. The JSON listing is beta tooling
metadata for local gates and adapters. The field set is intentionally not a
stable public schema in this release.
The source language, typed core, runtime, standard library, public API surface,
compiler diagnostics, diagnostic output shape, compiler-known runtime names,
ABI/layout behavior, and optimizer contract are unchanged by this target.
Benchmark timings remain local-machine evidence only and no timing numbers are
published by the release contract.
This target explicitly does not add benchmark kernels, implementation language
slots, timing publication, performance thresholds, cross-machine performance
claims, a stable benchmark JSON schema, source-language/runtime/stdlib/API
changes, diagnostic-output changes, or ABI/layout guarantees.
### 4.4.12 Post-Beta Reserved Generic Collection Boundary Hardening And Collection Ledger
Status: released in `1.0.0-beta.15` as a docs/design ledger and
compiler-boundary hardening update for the existing concrete collection and
value-family surface.
`1.0.0-beta.15` adds
[`docs/language/COLLECTIONS.md`](COLLECTIONS.md) as the public collection
ledger. The ledger links to the generated
[`docs/language/STDLIB_API.md`](STDLIB_API.md) catalog for exact exported
standard-library helper signatures instead of duplicating generated counts.
The ledger inventories the current concrete vector, option, result, and
related option/result-returning facade surfaces. It records design pressure
from duplicated concrete vector/option/result helper families 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 boundaries of the existing
beta surface. This target does not add, remove, rename, or reclassify
diagnostic codes and does not change the `slovo.diagnostic` schema,
diagnostic output shape, golden fixture shape, or human-readable diagnostic
prose policy. It does intentionally reword affected reserved-boundary messages
from beta.9-specific text to current-beta wording while preserving diagnostic
codes, schema, spans, expected/found values, hints, and output shape.
The source language, typed core, runtime, standard library, public API surface,
benchmark metadata schema, compiler-known runtime names, ABI/layout behavior,
optimizer contract, and performance claims are unchanged by this target. This
target explicitly does not add executable generics, generic aliases,
parameterized aliases, maps, sets, generic stdlib dispatch, iterators, mutable
vectors, slice/view APIs, new runtime names, or a stable stdlib/API freeze.
### 4.4.13 Post-Beta String Scanning And Token Boundary Foundation
Status: released in `1.0.0-beta.16` as a byte-oriented string scanning and
token-boundary foundation over the existing runtime string representation.
`1.0.0-beta.16` adds these `std.string` helpers:
- `byte_at_result ((value string) (index i32)) -> (result i32 i32)`
- `slice_result ((value string) (start i32) (count i32)) -> (result string i32)`
- `starts_with ((value string) (prefix string)) -> bool`
- `ends_with ((value string) (suffix string)) -> bool`
The helpers operate on bytes before the trailing NUL in the current
NUL-terminated runtime string representation. `byte_at_result` returns `ok`
with the byte value as `i32` for valid zero-based byte indexes and returns
`err 1` for invalid indexes. `slice_result` returns `ok` with a runtime-owned
string for valid byte ranges and returns `err 1` for invalid ranges. Allocation
failure while creating the substring may follow the existing string allocation
trap policy.
`starts_with` and `ends_with` compare byte prefixes and suffixes. Empty prefixes
and empty suffixes match. No helper performs Unicode normalization, grapheme
segmentation, display-width measurement, locale-sensitive matching, or
case-folding.
This target does not add full JSON parsing, object/array parsing, tokenizer
or scanner objects, language slice/view syntax or borrowed substring views,
mutable strings, string containers, stable runtime ABI/layout, performance
claims, or a stable standard-library/API freeze.
## 4.5 v2.0.0-beta.1 Experimental Integration Readiness ## 4.5 v2.0.0-beta.1 Experimental Integration Readiness
Status: current experimental Slovo-side release contract, released 2026-05-17. Status: current experimental Slovo-side release contract, released 2026-05-17.
@ -1303,22 +1823,23 @@ ABI symbols or stable runtime helper symbols. The exact promoted names are
reserved from user function, export, import, local, and parameter reserved from user function, export, import, local, and parameter
shadowing. shadowing.
The latest released Slovo-side collection target after exp-107 is `exp-108`, The latest released Slovo-side concrete collection helper target is
Standard Vec String, F64, And Bool Prefix And Suffix Helpers Alpha. It `1.0.0-beta.12`, Concrete Vector Query And Prefix Parity. It broadens
broadens `std/vec_string.slo`, `std/vec_f64.slo`, and `std/vec_bool.slo` `std/vec_i64.slo` with `count_of`, `starts_with`, `without_prefix`,
with exactly `starts_with`, `without_prefix`, `ends_with`, and `ends_with`, and `without_suffix`, and broadens `std/vec_f64.slo` with
`without_suffix` over the already released concrete vec-string, vec-f64, and `count_of`. The helper lanes stay source-authored, recursive, immutable, and
vec-bool helper surfaces. The helper lanes stay source-authored, limited to the already promoted concrete vector runtime names. They do not
recursive, and immutable and do not widen the promoted collection runtime widen the promoted collection runtime surface or add new compiler-known names.
surface beyond those connected prefix/suffix helpers.
The collections alpha slice explicitly defers generic vectors, vector element The collections alpha slice explicitly defers generic vectors, vector element
types other than `i32`, `i64`, `f64`, `bool`, and `string`, vector mutation, vector types other than `i32`, `i64`, `f64`, `bool`, and `string`, vector mutation,
`var`, vector `set`, vector literals besides empty/append construction, a `push` mutable vector operations beyond whole-value `var`/`set` already promoted for
alias, nested vectors, vectors in arrays, vectors in structs, vectors in current concrete vectors, vector literals besides empty/append construction, a
options, vectors in results, iterators, slices, maps, sets, user-visible `push` alias, nested vectors, vectors in arrays, vectors in structs, vectors in
deallocation, stable ABI/layout/helper-symbol promises, package expansion, options, vectors in results, iterators, slices/views, maps, sets, user-visible
and IO expansion. deallocation, new runtime names, stable ABI/layout/helper-symbol promises,
performance claims, package expansion, stable stdlib API freeze, and IO
expansion.
The collection fixture targets are: The collection fixture targets are:
@ -4694,7 +5215,9 @@ owned-runtime-string compiler-supported fixture. exp-13 adds
`examples/supported/string-parse-i32-result.slo` as the first string parse `examples/supported/string-parse-i32-result.slo` as the first string parse
result compiler-supported fixture. exp-34 adds result compiler-supported fixture. exp-34 adds
`examples/supported/string-parse-bool-result.slo` for the exact lowercase bool `examples/supported/string-parse-bool-result.slo` for the exact lowercase bool
parse result slice. parse result slice. `1.0.0-beta.16` adds byte-oriented string scanning and
token-boundary helpers through the `std.string` source facade and explicit
examples.
### 8.1 Goal ### 8.1 Goal
@ -4728,6 +5251,9 @@ The promoted slice supports:
`(result i32 i32)` value from an entire ASCII decimal signed `i32` string `(result i32 i32)` value from an entire ASCII decimal signed `i32` string
- exp-34 released `std.string.parse_bool_result`, returning `(result bool i32)` - exp-34 released `std.string.parse_bool_result`, returning `(result bool i32)`
for exact lowercase `true`/`false` parsing with `err 1` ordinary failures for exact lowercase `true`/`false` parsing with `err 1` ordinary failures
- `1.0.0-beta.16` byte-oriented string helpers:
`std.string.byte_at_result`, `std.string.slice_result`,
`std.string.starts_with`, and `std.string.ends_with`
Everything else in the string family remains deferred until separately Everything else in the string family remains deferred until separately
specified, implemented, diagnosed, formatted, and tested. specified, implemented, diagnosed, formatted, and tested.
@ -4799,6 +5325,29 @@ locals, parameters, returns, and calls returning `string`. No import is
required, no legacy alias is introduced, and no user-visible allocation or required, no legacy alias is introduced, and no user-visible allocation or
deallocation form is exposed. deallocation form is exposed.
`1.0.0-beta.16` adds byte-oriented string scanning and token-boundary helpers:
```slo
(std.string.byte_at_result "slovo" 0)
(std.string.slice_result "slovo" 1 3)
(std.string.starts_with "slovo" "slo")
(std.string.ends_with "slovo" "ovo")
```
`std.string.byte_at_result` has one `string` argument and one `i32` index. It
returns `ok byte` for valid zero-based byte indexes before the trailing NUL and
`err 1` for ordinary invalid indexes.
`std.string.slice_result` has one `string` argument, one `i32` start byte, and
one `i32` byte count. It returns `ok text` for a valid range and `err 1` for an
ordinary invalid range. The successful result is runtime-owned and immutable.
Allocation failure may trap with the existing string allocation failure policy.
`std.string.starts_with` and `std.string.ends_with` compare byte prefixes and
suffixes and return `bool`. Empty prefixes and suffixes match. These helpers do
not perform Unicode normalization, grapheme segmentation, display-width
measurement, locale-sensitive matching, or case-folding.
v1.2 promotes bool printing through the legacy alias: v1.2 promotes bool printing through the legacy alias:
```slo ```slo
@ -4877,14 +5426,18 @@ Promoted uses of `string` values:
- operands to `=` when both operands check as `string` - operands to `=` when both operands check as `string`
- argument to `string_len` or `std.string.len` - argument to `string_len` or `std.string.len`
- exp-1 arguments to and result from `std.string.concat` - exp-1 arguments to and result from `std.string.concat`
- `1.0.0-beta.16` byte indexes and byte ranges through
`std.string.byte_at_result` and `std.string.slice_result`
- `1.0.0-beta.16` prefix and suffix byte comparisons through
`std.string.starts_with` and `std.string.ends_with`
Still deferred: Still deferred:
- string ordering or comparison other than equality - string ordering or comparison other than equality
- string values inside arrays or vectors - string values inside arrays or vectors
- slices or borrowed substring views - language slice/view syntax or borrowed substring views
- concatenation beyond exp-1 `std.string.concat` - concatenation beyond exp-1 `std.string.concat`
- indexing - unchecked indexing or character/grapheme indexing
- ownership or lifetime annotations - ownership or lifetime annotations
- user-defined runtime bindings involving `string` - user-defined runtime bindings involving `string`
@ -4911,6 +5464,11 @@ Required lowering shape:
- exp-1 `std.string.concat` lowers to a runtime-owned immutable string - exp-1 `std.string.concat` lowers to a runtime-owned immutable string
allocation, byte copy, and trailing terminator write; allocation failure allocation, byte copy, and trailing terminator write; allocation failure
traps with `slovo runtime error: string allocation failed` traps with `slovo runtime error: string allocation failed`
- `1.0.0-beta.16` `std.string.byte_at_result`,
`std.string.slice_result`, `std.string.starts_with`, and
`std.string.ends_with` operate over bytes before the trailing NUL; invalid
byte indexes/ranges return `err 1`, and successful `slice_result` returns a
runtime-owned immutable string
- since embedded NUL is not promoted, printing observes the whole decoded - since embedded NUL is not promoted, printing observes the whole decoded
literal and equality/length observe the whole decoded value literal and equality/length observe the whole decoded value
@ -4989,9 +5547,10 @@ as `string`. `string_len` and `print_bool` use the same compact call style as
other promoted intrinsics. v1.5 `std.io.print_*` and `std.string.len` calls use other promoted intrinsics. v1.5 `std.io.print_*` and `std.string.len` calls use
the same compact call style. exp-1 `std.string.concat` uses the same compact the same compact call style. exp-1 `std.string.concat` uses the same compact
call style and remains inline when nested in `std.io.print_string`, call style and remains inline when nested in `std.io.print_string`,
`std.string.len`, equality, local initializers, returns, and user calls. The `std.string.len`, equality, local initializers, returns, and user calls.
formatter must preserve the decoded literal meaning when re-emitting the `1.0.0-beta.16` string scanning helpers use the same compact call style. The
current supported escapes. formatter must preserve the decoded literal meaning when re-emitting the current
supported escapes.
### 8.7 Diagnostics ### 8.7 Diagnostics
@ -5032,8 +5591,9 @@ Required promoted-boundary diagnostics:
- string ordering/comparison other than equality, string containers beyond the - string ordering/comparison other than equality, string containers beyond the
current direct struct fields, current fixed string arrays, current direct current direct struct fields, current fixed string arrays, current direct
fixed-array struct fields, and current concrete option/result families, fixed-array struct fields, and current concrete option/result families,
slices, concatenation beyond exp-1 language slice/view syntax, concatenation beyond exp-1
`std.string.concat`, indexing, mutation, user-visible allocation, `std.string.concat`, unchecked indexing, character/grapheme indexing,
mutation, user-visible allocation,
user-visible deallocation, and user-defined runtime bindings must remain user-visible deallocation, and user-defined runtime bindings must remain
unsupported with structured diagnostics or explicit backend-feature unsupported with structured diagnostics or explicit backend-feature
diagnostics; they must not reach a backend panic diagnostics; they must not reach a backend panic
@ -5241,10 +5801,14 @@ requirement.
Slovo v1 keeps the v0 rule that diagnostics have both human-readable and Slovo v1 keeps the v0 rule that diagnostics have both human-readable and
machine-readable forms generated from one compiler diagnostic object. Human machine-readable forms generated from one compiler diagnostic object. Human
rendering may choose layout and surrounding prose, but the machine form is a rendering may choose layout and surrounding prose. The machine form is the
stable tool API. tooling contract, with beta compatibility and migration rules defined by the
diagnostics policy rather than by human text.
The v1 machine diagnostic schema is `slovo.diagnostic` version `1`. The v1 machine diagnostic schema is `slovo.diagnostic` version `1`. As of
`1.0.0-beta.13`, the detailed beta schema policy, JSON-line discipline,
source-less diagnostic policy, compatibility classes, and current code catalog
live in [`docs/language/DIAGNOSTICS.md`](DIAGNOSTICS.md).
Every machine diagnostic emitted for a v1 compiler boundary must include: Every machine diagnostic emitted for a v1 compiler boundary must include:
@ -5306,8 +5870,10 @@ Diagnostic locations are derived from original source text, not from formatter
output. output.
The schema version belongs in every machine diagnostic and every golden The schema version belongs in every machine diagnostic and every golden
diagnostic fixture. Future machine-shape changes require a documented schema diagnostic fixture. Human-readable diagnostic prose remains beta-flexible, but
migration and updated Glagol snapshots. future machine-shape or code changes require the compatibility class defined in
[`docs/language/DIAGNOSTICS.md`](DIAGNOSTICS.md) and updated Glagol snapshots
when the current golden contract changes.
### 9.2 Artifact Manifest Schema ### 9.2 Artifact Manifest Schema
@ -5388,7 +5954,7 @@ Successful modes that intentionally produce no primary text may use:
Recommended v1 modes are `check`, `format`, `emit-llvm`, Recommended v1 modes are `check`, `format`, `emit-llvm`,
`inspect-lowering`, and `test`. Recommended output and artifact kinds are `inspect-lowering`, and `test`. Recommended output and artifact kinds are
`diagnostics`, `formatted-source`, `llvm-ir`, `lowering-inspector`, `stdout`, `diagnostics`, `formatted-source`, `llvm-ir`, `lowering-inspector`, `stdout`,
`stderr`, `no-output`, and `test-report`. `stderr`, `symbols`, `no-output`, and `test-report`.
A failed invocation still writes a manifest when the tool mode supports A failed invocation still writes a manifest when the tool mode supports
manifests. In that case `(success false)` is paired with a diagnostics artifact manifests. In that case `(success false)` is paired with a diagnostics artifact
@ -5442,6 +6008,7 @@ comments inside:
- `(module ...)` forms - `(module ...)` forms
- `(struct ...)` forms - `(struct ...)` forms
- `(type ...)` alias declaration forms
- `fn` and `test` headers before their body begins - `fn` and `test` headers before their body begins
- type forms such as `(array i32 N)`, `(option i32)`, `(result i32 i32)`, - type forms such as `(array i32 N)`, `(option i32)`, `(result i32 i32)`,
exp-2 `(vec i32)`, exp-94 `(vec i64)`, exp-103 `(vec f64)`, and exp-4 exp-2 `(vec i32)`, exp-94 `(vec i64)`, exp-103 `(vec f64)`, and exp-4
@ -5553,12 +6120,18 @@ manifest source root and uses deterministic v1.3 module ordering. Plain
`glagol fmt <file.slo>` continues to write formatted source to stdout. `glagol fmt <file.slo>` continues to write formatted source to stdout.
Documentation generation records modules, imports/exports, structs, functions, Documentation generation records modules, imports/exports, structs, functions,
and tests as deterministic Markdown. It is generated documentation, not tests, and beta.11 exported/public API sections as deterministic Markdown.
runtime reflection, typed-core reflection, debug metadata, DWARF, source maps, Public API sections include exact exported function signatures, exported
or ABI/layout information. struct fields, exported enum variants/payload types, non-export filtering, and
module-local alias normalization. It is generated documentation, not runtime
reflection, typed-core reflection, debug metadata, DWARF, source maps,
ABI/layout information, or a stable Markdown schema.
LSP, watch mode, daemon protocols, SARIF, debug adapters, stable debug LSP, watch mode, daemon protocols, SARIF, debug adapters, diagnostics schema
metadata, DWARF emission, and standalone source-map files remain deferred. policy, stable debug metadata, DWARF emission, standalone source-map files,
stable stdlib/API compatibility freeze, executable generics, maps/sets,
re-exports, globs, hierarchical modules, and registry semantics remain
deferred.
## 10. Slice 6: Unsafe, Memory, And FFI ## 10. Slice 6: Unsafe, Memory, And FFI
@ -5641,7 +6214,8 @@ promises remain deferred until a future spec explicitly promotes them.
host error ADTs, stdin APIs beyond exp-12, parsing APIs beyond exp-13 host error ADTs, stdin APIs beyond exp-12, parsing APIs beyond exp-13
`std.string.parse_i32_result`, exp-25 `std.string.parse_i64_result`, exp-28 `std.string.parse_i32_result`, exp-25 `std.string.parse_i64_result`, exp-28
`std.string.parse_f64_result`, and exp-34 exact lowercase `std.string.parse_f64_result`, and exp-34 exact lowercase
`std.string.parse_bool_result`, `std.string.parse_bool_result`, string scanning/tokenizing APIs beyond
`1.0.0-beta.16` byte access, substring, and prefix/suffix helpers,
result helper payload families beyond the explicitly listed result helper payload families beyond the explicitly listed
`(result i32 i32)`, `(result i64 i32)`, `(result string i32)`, exp-28 `(result i32 i32)`, `(result i64 i32)`, `(result string i32)`, exp-28
returned `(result f64 i32)`, and exp-34 returned `(result bool i32)` returned `(result f64 i32)`, and exp-34 returned `(result bool i32)`
@ -5650,8 +6224,9 @@ promises remain deferred until a future spec explicitly promotes them.
concrete option source helpers, concrete option source helpers,
randomness beyond the exp-11 target, time beyond randomness beyond the exp-11 target, time beyond
the exp-8 host time/sleep target, vectors/collections beyond the exp-2 the exp-8 host time/sleep target, vectors/collections beyond the exp-2
`(vec i32)`, exp-94 `(vec i64)`, exp-99 `(vec string)`, and exp-103 `(vec i32)`, exp-94 `(vec i64)`, exp-99 `(vec string)`, exp-103
`(vec f64)` targets, `(vec f64)`, exp-104 `(vec bool)`, and beta.12 source-helper parity
targets,
user-defined standard modules, overloading, and generic standard-library APIs user-defined standard modules, overloading, and generic standard-library APIs
- numeric primitives and conversions beyond exp-20 direct `f64`, exp-21 - numeric primitives and conversions beyond exp-20 direct `f64`, exp-21
direct `i64`, exp-22 explicit `std.num.i32_to_i64`, direct `i64`, exp-22 explicit `std.num.i32_to_i64`,
@ -5696,19 +6271,25 @@ promises remain deferred until a future spec explicitly promotes them.
facade wrappers, including wall-clock/calendar/timezone APIs, facade wrappers, including wall-clock/calendar/timezone APIs,
high-resolution timers, async timers, cancellation, and scheduling high-resolution timers, async timers, cancellation, and scheduling
guarantees guarantees
- generic vectors, vector element types other than `i32`, vector mutation, - generic vectors, vector element types other than `i32`, `i64`, `f64`,
vector literals beyond empty/append construction, `push`, nested vectors, `bool`, and `string`, element-level mutable vectors and mutable vector
vectors in arrays/structs/options/results, iterators, slices, maps, sets, operations beyond whole-value local reassignment, vector literals beyond
user-visible vector deallocation, and stable vector ABI/layout/helper symbols empty/append construction, `push`, nested vectors, vectors in
arrays/structs/options/results, iterators, slices/views, maps, sets, new
runtime vector helper names, performance claims, user-visible vector
deallocation, stable stdlib API freeze, and stable vector ABI/layout/helper
symbols
- string concatenation beyond exp-1 `std.string.concat`, parsing beyond - string concatenation beyond exp-1 `std.string.concat`, parsing beyond
exp-13 `std.string.parse_i32_result`, exp-25 exp-13 `std.string.parse_i32_result`, exp-25
`std.string.parse_i64_result`, and exp-28 `std.string.parse_i64_result`, and exp-28
`std.string.parse_f64_result`, plus exp-34 exact lowercase `std.string.parse_f64_result`, plus exp-34 exact lowercase
`std.string.parse_bool_result`, indexing, slicing, string containers beyond `std.string.parse_bool_result`, unchecked indexing, character/grapheme
indexing, language slice/view syntax, string containers beyond
the current direct struct fields, current fixed string arrays, current the current direct struct fields, current fixed string arrays, current
direct fixed-array struct fields, and current concrete option/result direct fixed-array struct fields, and current concrete option/result
families, user-visible string allocation or deallocation, Unicode length or families, user-visible string allocation or deallocation, Unicode length,
digit semantics, and stable string ABI/layout grapheme, display-width, locale, or digit semantics, full JSON parsing,
object/array parsing, and stable string ABI/layout
- pointer types, allocation, deallocation, load, store, pointer arithmetic, - pointer types, allocation, deallocation, load, store, pointer arithmetic,
reinterpretation, unchecked indexing, raw memory operations, and FFI reinterpretation, unchecked indexing, raw memory operations, and FFI
- stable ABI and stable layout promises - stable ABI and stable layout promises

View File

@ -1,14 +1,17 @@
# Slovo Standard Runtime Catalog # Slovo Standard Runtime Catalog
Status: standard-runtime catalog through exp-101 over the released exp-14 Status: standard-runtime catalog through exp-104 plus the post-beta
`1.0.0-beta.18` JSON string token parsing foundation over the released exp-14
conformance baseline. The latest Slovo experimental alpha release is conformance baseline. The latest Slovo experimental alpha release is
`exp-101`, Standard Vec String Option And Transform Helpers Alpha. exp-29, exp-30, exp-32, `exp-104`, Standard Vec Bool Baseline Alpha. exp-29, exp-30, exp-32,
exp-33, and exp-35 through exp-93 add no compiler-known `std.*` names; exp-33, and exp-35 through exp-93 add no compiler-known `std.*` names;
exp-31 adds exactly one checked numeric conversion name; exp-34 adds exactly exp-31 adds exactly one checked numeric conversion name; exp-34 adds exactly
one strict bool parse name; exp-94 adds exactly four concrete `(vec i64)` one strict bool parse name; exp-94 adds exactly four concrete `(vec i64)`
vector names; exp-95 through exp-98 add no compiler-known `std.*` names; and vector names; exp-95 through exp-98 add no compiler-known `std.*` names;
exp-99 adds exactly four concrete `(vec string)` vector names. exp-100 and exp-99 adds exactly four concrete `(vec string)` vector names; exp-100 and
exp-101 add no new compiler-known `std.*` names. exp-101 add no new compiler-known `std.*` names; exp-103 adds exactly four
concrete `(vec f64)` vector names; and exp-104 adds exactly four concrete
`(vec bool)` vector names.
This document catalogs promoted compiler-known `std.*` operations only. It This document catalogs promoted compiler-known `std.*` operations only. It
does not add source syntax, runtime APIs, standard library functions, manifest does not add source syntax, runtime APIs, standard library functions, manifest
@ -29,10 +32,29 @@ platform error values.
The `1.0.0-beta.7` serialization/data-interchange foundation release adds The `1.0.0-beta.7` serialization/data-interchange foundation release adds
`std.json.quote_string` behind the `std.json` source facade. It provides `std.json.quote_string` behind the `std.json` source facade. It provides
deterministic compact JSON string quoting before source-level string scanning, deterministic compact JSON string quoting before later byte-oriented string
slicing, maps, or recursive JSON values are available. scanning helpers, maps, or recursive JSON values are available.
The exp-era catalog is closed to names promoted through exp-101. exp-29, The `1.0.0-beta.16` string scanning and token-boundary foundation release adds
byte-oriented `std.string` runtime helpers behind the source facade. The helpers
operate over bytes before the trailing NUL in the current runtime string
representation, return `err 1` for ordinary invalid indexes or ranges, and do
not define Unicode scalar, grapheme, display-width, locale, full JSON parser,
object/array parser, language slice/view, or stable API semantics.
The `1.0.0-beta.17` JSON primitive scalar parsing foundation release adds
promoted bool and numeric `std.json.parse_*_value_result` runtime helpers
behind the source facade. The helpers consume a whole isolated JSON primitive
token and return `err 1` for ordinary parse failure. The exact `null` helper
is source-only and is not cataloged as a promoted runtime operation.
The `1.0.0-beta.18` JSON string token parsing foundation release adds promoted
`std.json.parse_string_value_result` for one isolated ASCII JSON string token.
It decodes simple escapes, rejects Unicode escapes for this slice, and leaves
recursive JSON values, tokenizers, streaming, schema validation, and stable
JSON parse errors deferred.
The exp-era catalog is closed to names promoted through exp-104. exp-29,
exp-30, exp-32, exp-33, and exp-35 through exp-93 add no new standard-runtime exp-30, exp-32, exp-33, and exp-35 through exp-93 add no new standard-runtime
operation names. exp-32/exp-39/exp-56/exp-57 `std/math.slo` helpers, operation names. exp-32/exp-39/exp-56/exp-57 `std/math.slo` helpers,
exp-33/exp-35 `std/result.slo` helpers, exp-36 `std/option.slo` helpers, exp-33/exp-35 `std/result.slo` helpers, exp-36 `std/option.slo` helpers,
@ -49,7 +71,10 @@ entries; exp-99 adds `std.vec.string.empty`, `std.vec.string.append`,
`std.vec.string.len`, and `std.vec.string.index`; exp-100 broadens option `std.vec.string.len`, and `std.vec.string.index`; exp-100 broadens option
language/source-helper support only and adds no new catalog entries; exp-101 language/source-helper support only and adds no new catalog entries; exp-101
broadens vec-string source-helper support only and adds no new catalog broadens vec-string source-helper support only and adds no new catalog
entries. entries; exp-103 adds `std.vec.f64.empty`, `std.vec.f64.append`,
`std.vec.f64.len`, and `std.vec.f64.index`; and exp-104 adds
`std.vec.bool.empty`, `std.vec.bool.append`, `std.vec.bool.len`, and
`std.vec.bool.index`.
Legacy compatibility Legacy compatibility
aliases such as `print_i32`, `print_string`, `print_bool`, and `string_len` aliases such as `print_i32`, `print_string`, `print_bool`, and `string_len`
are not `std.*` names and are therefore excluded from this catalog. Legacy are not `std.*` names and are therefore excluded from this catalog. Legacy
@ -76,16 +101,35 @@ source-level result helper names are the `std.result.*` names cataloged below.
| `std.num.f64_to_i64_result` | `(f64) -> (result i64 i32)` | exp-31 | `examples/supported/f64-to-i64-result.slo` | Returns `ok value` only when the `f64` input is finite, exactly integral, and in the signed `i64` range; returns `err 1` for non-finite, fractional, or out-of-range input without trapping. Conservative fixture values avoid pinning every `f64`/`i64` edge. | Uses existing standard-runtime usage recording if present; no schema change. | Unchecked casts, unchecked f64-to-i64, cast syntax, generic `cast_checked`, f32, unsigned/narrower integer families, mixed numeric arithmetic, broad math, stable helper ABI/layout/ownership. | | `std.num.f64_to_i64_result` | `(f64) -> (result i64 i32)` | exp-31 | `examples/supported/f64-to-i64-result.slo` | Returns `ok value` only when the `f64` input is finite, exactly integral, and in the signed `i64` range; returns `err 1` for non-finite, fractional, or out-of-range input without trapping. Conservative fixture values avoid pinning every `f64`/`i64` edge. | Uses existing standard-runtime usage recording if present; no schema change. | Unchecked casts, unchecked f64-to-i64, cast syntax, generic `cast_checked`, f32, unsigned/narrower integer families, mixed numeric arithmetic, broad math, stable helper ABI/layout/ownership. |
| `std.string.len` | `(string) -> i32` | v1.5 | `examples/supported/standard-runtime.slo` | Returns the existing decoded byte-count length used by legacy `string_len`. | Uses existing standard-runtime usage recording if present; no schema change. | Unicode scalar/grapheme length, slicing, indexing, stable string ABI/layout. | | `std.string.len` | `(string) -> i32` | v1.5 | `examples/supported/standard-runtime.slo` | Returns the existing decoded byte-count length used by legacy `string_len`. | Uses existing standard-runtime usage recording if present; no schema change. | Unicode scalar/grapheme length, slicing, indexing, stable string ABI/layout. |
| `std.string.concat` | `(string, string) -> string` | exp-1 | `examples/supported/owned-string-concat.slo` | Returns an immutable runtime-owned string; allocation failure traps as `slovo runtime error: string allocation failed`. | Uses existing standard-runtime usage recording if present; no concat-specific schema field. | Mutable strings, string containers, user-visible allocation/deallocation, stable string ABI/layout. | | `std.string.concat` | `(string, string) -> string` | exp-1 | `examples/supported/owned-string-concat.slo` | Returns an immutable runtime-owned string; allocation failure traps as `slovo runtime error: string allocation failed`. | Uses existing standard-runtime usage recording if present; no concat-specific schema field. | Mutable strings, string containers, user-visible allocation/deallocation, stable string ABI/layout. |
| `std.json.quote_string` | `(string) -> string` | `1.0.0-beta.7` | `examples/projects/std-layout-local-json` | Returns a compact JSON string literal for the input text, including surrounding quotes; it escapes quote, backslash, newline, tab, carriage return, backspace, form feed, and other control bytes as JSON escapes. Allocation failure traps as `slovo runtime error: string allocation failed`. | Uses existing standard-runtime usage recording if present; no schema change. | JSON parsing, recursive JSON values, maps/sets, streaming encoders, schema validation, Unicode normalization, embedded NUL support in the current null-terminated string ABI, stable helper ABI/layout/ownership. | | `std.string.byte_at_result` | `(string, i32) -> (result i32 i32)` | `1.0.0-beta.16` | `examples/projects/std-layout-local-string` | Returns `ok byte` for a valid zero-based byte index before the trailing NUL, or `err 1` for an invalid index. | Uses existing standard-runtime usage recording if present; no schema change. | Character/grapheme indexing, unchecked indexing, Unicode scalar values, stable string ABI/layout. |
| `std.vec.i32.empty` | `() -> (vec i32)` | exp-2 | `examples/supported/vec-i32.slo` | Returns an empty immutable runtime-owned `(vec i32)`. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Generic vectors, element families beyond `i32`, `i64`, and `string`, vector mutation, stable vector ABI/layout. | | `std.string.slice_result` | `(string, i32, i32) -> (result string i32)` | `1.0.0-beta.16` | `examples/projects/std-layout-local-string` | Returns `ok text` for a valid byte range before the trailing NUL, or `err 1` for an invalid range; allocation failure may trap with the existing string allocation policy. | Uses existing standard-runtime usage recording if present; no schema change. | Borrowed substring views, language slice/view syntax, Unicode/grapheme slicing, stable string ABI/layout/ownership. |
| `std.string.starts_with` | `(string, string) -> bool` | `1.0.0-beta.16` | `examples/projects/std-layout-local-string` | Returns whether the first string starts with the prefix bytes; the empty prefix matches. | Uses existing standard-runtime usage recording if present; no schema change. | Locale-sensitive matching, case folding, Unicode normalization, tokenizer/parser APIs, stable string ABI/layout. |
| `std.string.ends_with` | `(string, string) -> bool` | `1.0.0-beta.16` | `examples/projects/std-layout-local-string` | Returns whether the first string ends with the suffix bytes; the empty suffix matches. | Uses existing standard-runtime usage recording if present; no schema change. | Locale-sensitive matching, case folding, Unicode normalization, tokenizer/parser APIs, stable string ABI/layout. |
| `std.json.quote_string` | `(string) -> string` | `1.0.0-beta.7` | `examples/projects/std-layout-local-json` | Returns a compact JSON string literal for the input text, including surrounding quotes; it escapes quote, backslash, newline, tab, carriage return, backspace, form feed, and other control bytes as JSON escapes. Allocation failure traps as `slovo runtime error: string allocation failed`. | Uses existing standard-runtime usage recording if present; no schema change. | Full JSON parsing, recursive JSON values, maps/sets, streaming encoders, schema validation, Unicode normalization, embedded NUL support in the current null-terminated string ABI, stable helper ABI/layout/ownership. |
| `std.json.parse_string_value_result` | `(string) -> (result string i32)` | `1.0.0-beta.18` | `examples/projects/std-layout-local-json` | Returns `ok decoded` for one exact ASCII JSON string token with surrounding quotes and simple escapes, or `err 1` for ordinary parse failure; rejects leading/trailing whitespace, raw control bytes, raw non-ASCII bytes, raw quote/backslash, trailing bytes, and all `\uXXXX` escapes. Allocation failure may trap with the existing string allocation policy. | Uses existing standard-runtime usage recording if present; no schema change. | Full JSON parsing, object/array/value parsing, Unicode escape decoding, Unicode normalization, embedded NUL support in the current null-terminated string ABI, stable parse error taxonomy, stable helper ABI/layout/ownership. |
| `std.json.parse_bool_value_result` | `(string) -> (result bool i32)` | `1.0.0-beta.17` | `examples/projects/std-layout-local-json` | Returns `ok true` for exact `true`, `ok false` for exact `false`, or `err 1` otherwise. | Uses existing standard-runtime usage recording if present; no schema change. | Full JSON parsing, whitespace-tolerant document parsing, stable parse error taxonomy, stable helper ABI/layout. |
| `std.json.parse_i32_value_result` | `(string) -> (result i32 i32)` | `1.0.0-beta.17` | `examples/projects/std-layout-local-json` | Returns `ok value` for a whole JSON integer token in signed `i32` range, or `err 1`; rejects leading/trailing whitespace, leading `+`, and leading-zero integer forms except `0`. | Uses existing standard-runtime usage recording if present; no schema change. | Full JSON parsing, generic numeric parsing, stable parse error taxonomy, stable helper ABI/layout. |
| `std.json.parse_u32_value_result` | `(string) -> (result u32 i32)` | `1.0.0-beta.17` | `examples/projects/std-layout-local-json` | Returns `ok value` for a whole non-negative JSON integer token in `u32` range, or `err 1`; rejects leading/trailing whitespace, leading `+`, negative tokens, and leading-zero integer forms except `0`. | Uses existing standard-runtime usage recording if present; no schema change. | Full JSON parsing, generic numeric parsing, stable parse error taxonomy, stable helper ABI/layout. |
| `std.json.parse_i64_value_result` | `(string) -> (result i64 i32)` | `1.0.0-beta.17` | `examples/projects/std-layout-local-json` | Returns `ok value` for a whole JSON integer token in signed `i64` range, or `err 1`; rejects leading/trailing whitespace, leading `+`, and leading-zero integer forms except `0`. | Uses existing standard-runtime usage recording if present; no schema change. | Full JSON parsing, generic numeric parsing, stable parse error taxonomy, stable helper ABI/layout. |
| `std.json.parse_u64_value_result` | `(string) -> (result u64 i32)` | `1.0.0-beta.17` | `examples/projects/std-layout-local-json` | Returns `ok value` for a whole non-negative JSON integer token in `u64` range, or `err 1`; rejects leading/trailing whitespace, leading `+`, negative tokens, and leading-zero integer forms except `0`. | Uses existing standard-runtime usage recording if present; no schema change. | Full JSON parsing, generic numeric parsing, stable parse error taxonomy, stable helper ABI/layout. |
| `std.json.parse_f64_value_result` | `(string) -> (result f64 i32)` | `1.0.0-beta.17` | `examples/projects/std-layout-local-json` | Returns `ok value` for a whole finite JSON number token, including exponent form, or `err 1`; rejects leading/trailing whitespace, leading `+`, leading-zero whole-number forms except `0`, and non-finite results. | Uses existing standard-runtime usage recording if present; no schema change. | Full JSON parsing, generic numeric parsing, stable parse error taxonomy, stable helper ABI/layout. |
| `std.vec.i32.empty` | `() -> (vec i32)` | exp-2 | `examples/supported/vec-i32.slo` | Returns an empty immutable runtime-owned `(vec i32)`. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Generic vectors, element families beyond current concrete vector families, vector mutation, stable vector ABI/layout. |
| `std.vec.i32.append` | `((vec i32), i32) -> (vec i32)` | exp-2 | `examples/supported/vec-i32.slo` | Returns a new immutable vector containing the input elements and appended value; allocation failure traps with the exp-2 vector allocation message. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Mutation, `push`, capacity APIs, user deallocation, stable vector ABI/layout. | | `std.vec.i32.append` | `((vec i32), i32) -> (vec i32)` | exp-2 | `examples/supported/vec-i32.slo` | Returns a new immutable vector containing the input elements and appended value; allocation failure traps with the exp-2 vector allocation message. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Mutation, `push`, capacity APIs, user deallocation, stable vector ABI/layout. |
| `std.vec.i32.len` | `((vec i32)) -> i32` | exp-2 | `examples/supported/vec-i32.slo` | Returns vector length as `i32`. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Generic length APIs and stable vector ABI/layout. | | `std.vec.i32.len` | `((vec i32)) -> i32` | exp-2 | `examples/supported/vec-i32.slo` | Returns vector length as `i32`. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Generic length APIs and stable vector ABI/layout. |
| `std.vec.i32.index` | `((vec i32), i32) -> i32` | exp-2 | `examples/supported/vec-i32.slo` | Returns the indexed value; out-of-bounds access traps with the exp-2 vector bounds message. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Slices/views, iterators, mutation, stable vector ABI/layout. | | `std.vec.i32.index` | `((vec i32), i32) -> i32` | exp-2 | `examples/supported/vec-i32.slo` | Returns the indexed value; out-of-bounds access traps with the exp-2 vector bounds message. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Slices/views, iterators, mutation, stable vector ABI/layout. |
| `std.vec.i64.empty` | `() -> (vec i64)` | exp-94 | `examples/supported/vec-i64.slo` | Returns an empty immutable runtime-owned `(vec i64)`. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Generic vectors, element families beyond `i32`, `i64`, and `string`, vector mutation, stable vector ABI/layout. | | `std.vec.i64.empty` | `() -> (vec i64)` | exp-94 | `examples/supported/vec-i64.slo` | Returns an empty immutable runtime-owned `(vec i64)`. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Generic vectors, element families beyond current concrete vector families, vector mutation, stable vector ABI/layout. |
| `std.vec.i64.append` | `((vec i64), i64) -> (vec i64)` | exp-94 | `examples/supported/vec-i64.slo` | Returns a new immutable vector containing the input elements and appended value; allocation failure traps with the existing vector allocation message. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Mutation, `push`, capacity APIs, user deallocation, stable vector ABI/layout. | | `std.vec.i64.append` | `((vec i64), i64) -> (vec i64)` | exp-94 | `examples/supported/vec-i64.slo` | Returns a new immutable vector containing the input elements and appended value; allocation failure traps with the existing vector allocation message. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Mutation, `push`, capacity APIs, user deallocation, stable vector ABI/layout. |
| `std.vec.i64.len` | `((vec i64)) -> i32` | exp-94 | `examples/supported/vec-i64.slo` | Returns vector length as `i32`. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Generic length APIs and stable vector ABI/layout. | | `std.vec.i64.len` | `((vec i64)) -> i32` | exp-94 | `examples/supported/vec-i64.slo` | Returns vector length as `i32`. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Generic length APIs and stable vector ABI/layout. |
| `std.vec.i64.index` | `((vec i64), i32) -> i64` | exp-94 | `examples/supported/vec-i64.slo` | Returns the indexed value; out-of-bounds access traps with the existing vector bounds message. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Slices/views, iterators, mutation, stable vector ABI/layout. | | `std.vec.i64.index` | `((vec i64), i32) -> i64` | exp-94 | `examples/supported/vec-i64.slo` | Returns the indexed value; out-of-bounds access traps with the existing vector bounds message. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Slices/views, iterators, mutation, stable vector ABI/layout. |
| `std.vec.string.empty` | `() -> (vec string)` | exp-99 | `examples/supported/vec-string.slo` | Returns an empty immutable runtime-owned `(vec string)`. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Generic vectors, element families beyond `i32`, `i64`, and `string`, vector mutation, stable vector ABI/layout. | | `std.vec.f64.empty` | `() -> (vec f64)` | exp-103 | `examples/supported/vec-f64.slo` | Returns an empty immutable runtime-owned `(vec f64)`. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Generic vectors, element families beyond current concrete vector families, vector mutation, stable vector ABI/layout. |
| `std.vec.f64.append` | `((vec f64), f64) -> (vec f64)` | exp-103 | `examples/supported/vec-f64.slo` | Returns a new immutable vector containing the input elements and appended value; allocation failure traps with the existing vector allocation message. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Mutation, `push`, capacity APIs, user deallocation, stable vector ABI/layout. |
| `std.vec.f64.len` | `((vec f64)) -> i32` | exp-103 | `examples/supported/vec-f64.slo` | Returns vector length as `i32`. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Generic length APIs and stable vector ABI/layout. |
| `std.vec.f64.index` | `((vec f64), i32) -> f64` | exp-103 | `examples/supported/vec-f64.slo` | Returns the indexed value; out-of-bounds access traps with the existing vector bounds message. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Slices/views, iterators, mutation, stable vector ABI/layout. |
| `std.vec.bool.empty` | `() -> (vec bool)` | exp-104 | `examples/supported/vec-bool.slo` | Returns an empty immutable runtime-owned `(vec bool)`. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Generic vectors, element families beyond current concrete vector families, vector mutation, stable vector ABI/layout. |
| `std.vec.bool.append` | `((vec bool), bool) -> (vec bool)` | exp-104 | `examples/supported/vec-bool.slo` | Returns a new immutable vector containing the input elements and appended value; allocation failure traps with the existing vector allocation message. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Mutation, `push`, capacity APIs, user deallocation, stable vector ABI/layout. |
| `std.vec.bool.len` | `((vec bool)) -> i32` | exp-104 | `examples/supported/vec-bool.slo` | Returns vector length as `i32`. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Generic length APIs and stable vector ABI/layout. |
| `std.vec.bool.index` | `((vec bool), i32) -> bool` | exp-104 | `examples/supported/vec-bool.slo` | Returns the indexed value; out-of-bounds access traps with the existing vector bounds message. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Slices/views, iterators, mutation, stable vector ABI/layout. |
| `std.vec.string.empty` | `() -> (vec string)` | exp-99 | `examples/supported/vec-string.slo` | Returns an empty immutable runtime-owned `(vec string)`. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Generic vectors, element families beyond current concrete vector families, vector mutation, stable vector ABI/layout. |
| `std.vec.string.append` | `((vec string), string) -> (vec string)` | exp-99 | `examples/supported/vec-string.slo` | Returns a new immutable vector containing the input elements and appended string; allocation failure traps with the existing vector allocation message. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Mutation, `push`, capacity APIs, user deallocation, stable vector ABI/layout. | | `std.vec.string.append` | `((vec string), string) -> (vec string)` | exp-99 | `examples/supported/vec-string.slo` | Returns a new immutable vector containing the input elements and appended string; allocation failure traps with the existing vector allocation message. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Mutation, `push`, capacity APIs, user deallocation, stable vector ABI/layout. |
| `std.vec.string.len` | `((vec string)) -> i32` | exp-99 | `examples/supported/vec-string.slo` | Returns vector length as `i32`. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Generic length APIs and stable vector ABI/layout. | | `std.vec.string.len` | `((vec string)) -> i32` | exp-99 | `examples/supported/vec-string.slo` | Returns vector length as `i32`. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Generic length APIs and stable vector ABI/layout. |
| `std.vec.string.index` | `((vec string), i32) -> string` | exp-99 | `examples/supported/vec-string.slo` | Returns the indexed value; out-of-bounds access traps with the existing vector bounds message. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Slices/views, iterators, mutation, stable vector ABI/layout. | | `std.vec.string.index` | `((vec string), i32) -> string` | exp-99 | `examples/supported/vec-string.slo` | Returns the indexed value; out-of-bounds access traps with the existing vector bounds message. | Uses existing standard-runtime usage recording if present; no vector-specific schema field. | Slices/views, iterators, mutation, stable vector ABI/layout. |
@ -170,8 +214,10 @@ integer-to-string calls, the exp-26 f64-to-string call, and the released
`std.string.parse_i64_result` / `std.string.parse_i64_result` /
`std.string.parse_f64_result` / `std.string.parse_f64_result` /
`std.string.parse_bool_result` result calls, bool parsing beyond exact `std.string.parse_bool_result` result calls, bool parsing beyond exact
lowercase `true`/`false`, string/bytes parse, lowercase `true`/`false`, string scanning/tokenizing APIs beyond
underscores, rich parse errors, Unicode digit parsing, f64 containers, `1.0.0-beta.16` byte access, substring, and prefix/suffix helpers,
string/bytes parse, underscores, rich parse errors, Unicode digit parsing,
f64 containers,
writable resource handles, binary IO, directory handles, directory writable resource handles, binary IO, directory handles, directory
enumeration, recursive filesystem operations, process handles, sockets, async enumeration, recursive filesystem operations, process handles, sockets, async
resource handling, platform-specific host error codes, rich host-error ADTs, resource handling, platform-specific host error codes, rich host-error ADTs,

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,68 @@
# Slovo Standard Library Stability Tiers
Status: beta public ledger for `1.0.0-beta.23`.
This document records the current maturity labels for the source-authored
standard-library facade surface. Exact exported helper signatures remain in
[`STDLIB_API.md`](STDLIB_API.md), which is generated from `lib/std/*.slo`.
This ledger explains how to read that catalog during the beta line.
`1.0.0-beta.23` is documentation and catalog-tooling alignment only. It adds no
source syntax, standard-library helpers, compiler-known runtime names, runtime
behavior, package behavior, manifest schema guarantee, Markdown schema
guarantee, ABI/layout guarantee, or stable standard-library/API compatibility
freeze. It does make the generated API catalog emit tier metadata and makes the
release gate check that experimental tier metadata is present.
## Tier Labels
Slovo uses these public standard-library tier labels:
| Tier | Meaning |
| --- | --- |
| `beta-supported` | Exported from `lib/std`, covered by current beta source-search, promotion, facade, or composition gates, and suitable for beta programs. Names and behavior are still beta contracts, not a stable `1.0.0` freeze. |
| `experimental` | Exported or documented in the beta line, but intentionally fluid because the domain still depends on deferred language, runtime, resource, platform, or schema policy. Use for feedback and narrow programs, not compatibility promises. |
| `internal` | Not public standard-library API. This includes non-exported helper names, module-local concrete aliases, implementation details, generated-document internals, and private runtime symbols. Internal names may be omitted from public catalogs or changed without migration promises. |
## Catalog Boundary
[`STDLIB_API.md`](STDLIB_API.md) is the generated signature inventory: it lists
exported `(fn ...)` helpers from `lib/std/*.slo`, normalizes module-local
concrete aliases to public concrete types, and omits non-exported helpers and
`(type ...)` aliases.
This ledger is the public maturity companion to that generated catalog. If the
catalog lists a signature, read the tier marker beside that signature as the
generated summary of this ledger. The generated catalog is a beta discovery
aid, not a stable Markdown schema or stable API freeze.
## Current Tier Ledger
| Surface | Current tier | Notes |
| --- | --- | --- |
| `std.cli`, `std.env`, `std.io`, `std.math`, `std.num`, `std.option`, `std.process`, `std.result`, and byte-oriented `std.string` helpers | `beta-supported` | Current source facade helpers over promoted concrete runtime/value families. The beta label still allows additive changes and scoped migrations before `1.0.0`. |
| Concrete vector modules: `std.vec_i32`, `std.vec_i64`, `std.vec_f64`, `std.vec_bool`, and `std.vec_string` | `beta-supported` | These are concrete immutable vector lanes over existing concrete runtime families. This is not a generic collections freeze and does not imply executable generics, generic aliases, maps, sets, iterators, mutable vectors, slice/view APIs, runtime collection changes, or stable ABI/layout. |
| Basic filesystem text/status helpers such as `read_text`, `write_text_result`, `exists`, `is_file`, `is_dir`, `remove_file_result`, and `create_dir_result` | `beta-supported` | Current concrete file helpers remain beta contracts over host filesystem behavior. Platform-specific errors and richer host-error ADTs remain deferred. |
| Filesystem resource-handle helpers such as `open_text_read_result`, `read_open_text_result`, `close_result`, `read_text_via_handle_result`, and `close_ok` | `experimental` | Handles are beta-scoped opaque `i32` values. Writable handles, directory handles/enumeration, stable handle ABI/layout, and richer platform error policy remain deferred. |
| `std.json` | `experimental` | Includes compact JSON text construction, exact primitive scalar token parsing, ASCII string-token parsing, and scalar document parsing. Object/array parsing, recursive `JsonValue`, parser/tokenizer objects, maps/sets, streaming, broad Unicode escape policy, embedded NUL policy, stable text encoding policy, and stable JSON API compatibility remain deferred. |
| `std.net` | `experimental` | Current blocking loopback TCP facade only. DNS, TLS, UDP, async IO, non-loopback binding, HTTP frameworks, stable socket ABI/layout, and rich host-error ADTs remain deferred. |
| `std.random` and `std.time` | `experimental` | Available as narrow beta host facades, but deterministic testing policy, seeding/time-source contracts, monotonic/wall-clock distinctions, reproducibility guarantees, and stable platform behavior remain deferred. |
| Non-exported helper names, module-local aliases, private runtime symbols, generated catalog internals, and release-tool implementation details | `internal` | Not part of the public standard-library compatibility surface. |
## Explicit Non-Changes
This ledger does not promote:
- source-language syntax
- standard-library helper additions, removals, or renames
- compiler-known runtime names
- runtime behavior changes
- executable generics or generic aliases
- generic stdlib dispatch
- maps, sets, iterators, mutable vectors, or slice/view APIs
- object/array JSON parsing or stable JSON schemas
- DNS, TLS, UDP, async networking, or non-loopback binding
- stable resource-handle ABI/layout
- stable artifact-manifest schema
- stable generated Markdown schema
- stable `1.0.0` standard-library/API compatibility freeze

Some files were not shown because too many files have changed in this diff Show More