Compare commits

..

19 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
sanjin
be6cdfb87c Release 1.0.0-beta.7 serialization foundation 2026-05-22 18:07:24 +02:00
271 changed files with 22842 additions and 1939 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,61 @@
# 1.0.0-beta.7 Serialization And Data Interchange Target
Status: released as `1.0.0-beta.7` on 2026-05-22.
`1.0.0-beta.7` targets a deliberately narrow serialization/data-interchange
foundation after the beta.6 networking slice. The goal is compact JSON text
construction for CLI, file, and loopback-network programs without pretending
that Slovo already has the collections and string APIs needed for a complete
JSON library.
## Slovo Source Surface
The staged source facade is `lib/std/json.slo`, importable explicitly as
`std.json`.
Exported helpers:
- `quote_string : (string) -> string`
- `null_value : () -> string`
- `bool_value : (bool) -> string`
- `i32_value : (i32) -> string`
- `u32_value : (u32) -> string`
- `i64_value : (i64) -> string`
- `u64_value : (u64) -> string`
- `f64_value : (f64) -> string`
- `field_string`, `field_bool`, `field_i32`, `field_u32`, `field_i64`,
`field_u64`, `field_f64`, and `field_null`
- `array0`, `array1`, `array2`, `array3`
- `object0`, `object1`, `object2`, `object3`
The array and object helpers accept already-encoded JSON text fragments. They
are compact construction helpers, not recursive data structures.
## Runtime Call
The facade wraps one compiler-known runtime call:
- `std.json.quote_string(value string) -> string`
The hosted runtime symbol is `__glagol_json_quote_string`. It returns a
complete compact JSON string literal including surrounding quotes. It escapes
quote, backslash, newline, tab, carriage return, backspace, form feed, and
other control bytes. Allocation failure uses the existing string allocation
trap.
## Fixtures And Benchmarks
- `examples/projects/std-import-json/` exercises explicit `std.json` source
import.
- `examples/projects/std-layout-local-json/` mirrors the facade as a local
module fixture and keeps the source-search contract explicit.
- `benchmarks/json-quote-loop/` adds a local-machine timing scaffold for JSON
string quoting across Slovo, C, Rust, Python, Clojure, and Common Lisp/SBCL.
## Deferrals
This scope does not add JSON parsing, recursive JSON values, maps/sets, generic
collections, source-level byte/character scanners, slicing, streaming encoders,
schema validation, Unicode normalization, embedded NUL support in the current
null-terminated runtime string ABI, stable runtime helper symbols, stable
ABI/layout/ownership guarantees, or a stable standard-library API freeze.

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,13 +24,20 @@ implementation scope.
networking (released in `1.0.0-beta.2` with read-only text file
handles plus narrow filesystem status and mutation calls).
3. Stabilize `lib/std` module boundaries and document beta-vs-stable APIs.
4. Improve language usability around entry points, `match`, aliases, and
concrete numeric completeness.
4. Improve language usability around entry points, `match`, concrete aliases,
and concrete numeric completeness.
5. Expand package/workspace discipline before remote registry work.
6. Add networking only after resource/error policy is coherent.
7. Add serialization/data-interchange helpers before richer network libraries.
8. Design generics and collection unification from real stdlib duplication
pressure.
9. Add editor-facing diagnostics, watch mode, and generated documentation
7. Add serialization/data-interchange helpers before richer network libraries
(released in `1.0.0-beta.7` with compact JSON text construction and JSON
string quoting).
8. Promote concrete type aliases before generics so long concrete vector,
option/result, array, JSON, and resource-handle signatures can be named
without changing runtime representation (released in `1.0.0-beta.8`).
9. Design generics and collection unification from real stdlib duplication
pressure by first applying concrete aliases to existing collection/value
facades and reserving generic/map/set directions without executable
semantics (released in `1.0.0-beta.9`).
10. Add editor-facing diagnostics, watch mode, and generated documentation
improvements.
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,37 @@
# 1.0.0-beta.7 Release Review
Date: 2026-05-22
Scope: serialization and data-interchange foundation.
## Verdict
`1.0.0-beta.7` is a coherent beta follow-up slice. The release adds a narrow
runtime-backed JSON string quoting primitive, a source-authored `std/json.slo`
facade, explicit std/local example projects, and a `json-quote-loop` benchmark
scaffold without claiming full JSON parsing, recursive JSON values, maps,
schema validation, or streaming encoders.
## Review Notes
- The new compiler-known call is intentionally small:
`std.json.quote_string(value string) -> string` lowers to
`__glagol_json_quote_string` and is listed in unsupported-call diagnostics.
- The source facade composes existing string and numeric helpers and keeps
object/array helpers limited to small fixed arities over already-encoded JSON
fragments.
- The local and std import fixtures preserve the current explicit-import
standard-library discipline.
- The benchmark scaffold is suitable as a local regression/comparison harness,
but the whitepapers still treat the exp-123 nine-row table as the current
published numeric baseline until fresh full-suite timing is rerun.
- The hosted runtime escaping path covers quotes, backslashes, standard JSON
control escapes, and `\u00XX` for remaining control bytes.
## Remaining Deferred Work
- JSON parsing and recursive JSON value modeling.
- Maps/sets or generic collection-backed object construction.
- Streaming encoders and schema-oriented validation.
- Unicode normalization or code point policy beyond byte-preserving string
literal emission.

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.

448
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
documents.
Current release: `1.0.0-beta.6`.
Current release: `1.0.0-beta.25`.
## Repository Layout
@ -24,27 +24,215 @@ scripts/ local release and document tooling
## Beta Scope
`1.0.0-beta.6` 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`
runtime/resource foundation bundle, the `1.0.0-beta.3` standard-library
stabilization bundle, the `1.0.0-beta.4` language-usability diagnostics
bundle, the `1.0.0-beta.5` local package/workspace discipline bundle, and the
`1.0.0-beta.6` loopback networking foundation. The language baseline supports
practical local command-line programs and libraries with:
`1.0.0-beta.6` loopback networking foundation, plus the `1.0.0-beta.7`
serialization/data-interchange foundation and the `1.0.0-beta.8` concrete type
alias foundation, the `1.0.0-beta.9` collection alias unification and
generic reservation slice, the `1.0.0-beta.10` developer-experience API
discovery slice, and the `1.0.0-beta.11` local package API documentation
slice, plus the `1.0.0-beta.12` concrete vector query and prefix parity
slice, the `1.0.0-beta.13` diagnostic catalog and schema policy slice, the
`1.0.0-beta.14` benchmark suite catalog and metadata gate, and the
`1.0.0-beta.15` reserved generic collection boundary hardening and collection
ledger, the `1.0.0-beta.16` string scanning and token boundary foundation,
the `1.0.0-beta.17` JSON primitive scalar parsing foundation, the
`1.0.0-beta.18` JSON string token parsing foundation, the
`1.0.0-beta.19` test discovery and user-project conformance foundation, the
`1.0.0-beta.20` string search and ASCII trim foundation, and the
`1.0.0-beta.21` JSON document scalar parsing foundation, plus the
`1.0.0-beta.22` run manifest and execution report hardening slice, and the
`1.0.0-beta.23` standard-library stability tier ledger and catalog alignment
slice, and the `1.0.0-beta.24` package manifest identity and local dependency
diagnostic hardening slice, 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
- `new`, `check`, `fmt`, `test`, `doc`, and `build`
- `new`, `check`, `fmt`, `test`, `doc`, `symbols`, `build`, `run`, and
`clean`
- `i32`, `i64`, `u32`, `u64`, `f64`, `bool`, `string`, and internal `unit`
- structs, enums, fixed arrays, concrete vectors, option/result families, and
current `match`
- module-local transparent concrete type aliases
- explicit `std/*.slo` imports from `lib/std`, installed `share/slovo/std`, or
`SLOVO_STD_PATH`
- beta-scoped loopback TCP handles through `std.net`
- JSON string quoting, compact JSON text construction, primitive scalar token
parsing, ASCII JSON string-token parsing, and scalar JSON document parsing
through `std.json`
- byte-oriented string search and ASCII edge trimming through `std.string`
- hosted native builds through LLVM IR, Clang, and `runtime/runtime.c`
Still deferred before stable: generics, maps/sets, broad package registry
semantics, DNS/TLS/async networking, LSP/watch/debug-adapter guarantees,
stable ABI and layout, and a stable standard-library compatibility freeze.
The generated standard-library API catalog is a beta discovery aid: it lists
exported helper signatures from `lib/std/*.slo`, normalizes module-local
concrete aliases such as `VecI32` and `ResultU64` to their concrete public
types, and omits non-exported helpers and `(type ...)` aliases.
The companion
[`docs/language/STDLIB_TIERS.md`](docs/language/STDLIB_TIERS.md) ledger
defines the public tier labels `beta-supported`, `experimental`, and
`internal`, marks JSON, loopback networking, random/time, and filesystem
resource-handle helpers as experimental domains, and keeps concrete vector
modules as beta-supported concrete lanes rather than a generic collections
freeze.
`glagol symbols <file.slo|project|workspace>` emits deterministic
editor-facing S-expression metadata for modules, imports, exports, aliases,
structs, enums, functions, tests, source spans, and workspace package names.
`glagol doc <file|project|workspace> -o <dir>` now includes deterministic public
API sections for local package and module documentation: exact exported
function signatures, exported struct fields, exported enum variants and payload
types, non-export filtering, and module-local alias normalization.
The `1.0.0-beta.22` tooling slice adds an additive run-report block to
`glagol run --manifest` artifact manifests so local evidence can record the
program exit status, captured stdout, captured stderr, and forwarded program
arguments. This is beta CLI evidence hardening only: it does not add language
or stdlib features and does not freeze the artifact-manifest schema.
The `1.0.0-beta.12` vector parity slice adds source-authored helper coverage
only: `std.vec_i64` gains `count_of`, `starts_with`, `without_prefix`,
`ends_with`, and `without_suffix`, while `std.vec_f64` gains `count_of`.
The `1.0.0-beta.13` diagnostics slice documents the beta
`slovo.diagnostic` version `1` policy in
[`docs/language/DIAGNOSTICS.md`](docs/language/DIAGNOSTICS.md): the
S-expression/JSON relationship, required and optional fields, JSON-line
discipline, source-less diagnostics, manifest diagnostic metadata,
compatibility and migration classes, and the current golden diagnostic code
catalog.
The `1.0.0-beta.14` benchmark metadata slice documents the existing benchmark
suite catalog in [`benchmarks/README.md`](benchmarks/README.md). It explains
`python3 benchmarks/runner.py --suite-list` for the human-readable suite
inventory and `python3 benchmarks/runner.py --suite-list --json` for beta
tooling metadata, with required scaffold-file verification for each current
suite. Benchmark timings remain local-machine evidence only; the JSON field
set is not a stable public schema.
The `1.0.0-beta.15` collection ledger and reserved diagnostic hardening slice
adds
[`docs/language/COLLECTIONS.md`](docs/language/COLLECTIONS.md) as the
docs/design ledger for current concrete collection and value-family
boundaries. It links to the generated
[`docs/language/STDLIB_API.md`](docs/language/STDLIB_API.md) catalog for exact
public helper signatures, records design pressure from duplicated concrete
vector/option/result families, defines prerequisites before executable
generics, generic aliases, maps, sets, iterators, mutable vectors, or
slice/view APIs can be promoted, and treats current unsupported diagnostics as
boundaries. It rewords affected reserved-boundary diagnostics from
beta.9-specific text to current-beta wording while preserving diagnostic
codes, schema, spans, expected/found values, hints, and output shape. It
changes no source language, runtime, stdlib/API surface, benchmark metadata
schema, ABI/layout behavior, or performance claim.
The `1.0.0-beta.16` string scanning and token boundary foundation adds
source facades and explicit examples for `std.string.byte_at_result`,
`std.string.slice_result`, `std.string.starts_with`, and
`std.string.ends_with`. These helpers are byte-oriented over the current
NUL-terminated runtime string representation; invalid indexes and ranges return
`err 1`, and substring allocation failure follows the existing string
allocation trap policy. This release does not promise Unicode scalar,
grapheme, display-width, or locale semantics; does not add full JSON parsing,
object/array parsing, tokenizers, a language slice/view feature, or a stable
stdlib/API freeze.
The `1.0.0-beta.17` JSON primitive scalar parsing foundation adds
result-returning `std.json.parse_*_value_result` helpers for booleans,
concrete numeric primitives, and exact `null` only. Numeric and boolean parse
helpers consume one isolated JSON primitive token: no leading/trailing
whitespace, no leading `+`, no leading-zero integer form except `0`, and no
non-finite f64 values. This is not full JSON parsing: object/array parsing,
tokenizers, recursive `JsonValue`, document parsing beyond the beta21 scalar
document helpers, schema validation, streaming, Unicode escape handling,
stable ABI/layout, and a stable stdlib/API freeze remain deferred.
The `1.0.0-beta.18` JSON string token parsing foundation adds
`std.json.parse_string_value_result` as a thin source facade over the matching
promoted runtime name. It consumes one already-isolated ASCII JSON string token
with exact quotes and no leading/trailing whitespace, decodes the simple JSON
escapes `\"`, `\\`, `\/`, `\b`, `\f`, `\n`, `\r`, and `\t`, and returns
`err 1` for ordinary parse failure. Complete JSON document parsing beyond the
beta21 scalar document helpers, object/array parsing, tokenizer APIs, Unicode
escape decoding/normalization, embedded NUL policy, stable ABI/layout, and a
stable stdlib/API freeze remain deferred.
The `1.0.0-beta.19` test discovery and user-project conformance foundation
adds `glagol test --list <file|project|workspace>` plus legacy
`glagol --run-tests --list <file>` support for listing checked and discovered
tests without executing test bodies. The list mode preserves existing file,
project, and workspace test ordering, honors `--filter <substring>`, and
remains beta tooling rather than a stable output schema.
The `1.0.0-beta.20` string search and ASCII trim foundation adds
`std.string.contains`, `std.string.index_of_option`,
`std.string.last_index_of_option`, `std.string.trim_ascii_start`,
`std.string.trim_ascii_end`, and `std.string.trim_ascii` as ordinary source
helpers over the existing byte string primitives. Search is byte-oriented,
empty needles match at the first index and at `(len value)` for last search,
and ASCII trimming removes only bytes `9`, `10`, `11`, `12`, `13`, and `32`.
The `1.0.0-beta.21` JSON document scalar parsing foundation adds
source-authored `std.json.parse_*_document_result` helpers for string, bool,
`i32`, `u32`, `i64`, `u64`, `f64`, and exact `null` scalar JSON documents.
Each helper trims ASCII whitespace around the whole document with
`std.string.trim_ascii`, then delegates to the already released exact JSON
value-token parser for that scalar family. This scope does not add new
compiler-known runtime names, object/array parsing, recursive `JsonValue`,
tokenizer objects, maps/sets, streaming, Unicode escape decoding beyond the
existing string-token behavior, embedded NUL policy, stable ABI/layout, or a
stable stdlib/API freeze.
The `1.0.0-beta.22` run manifest and execution report hardening slice extends
`glagol run --manifest` artifact manifests with additive run-report evidence:
the executed program's exit status, captured stdout, captured stderr, and
forwarded user-program arguments. This release does not add source-language
syntax, standard-library helpers, compiler-known runtime names, runtime
capabilities, package behavior, stable artifact-manifest schema guarantees,
stable ABI/layout, or a stable stdlib/API freeze.
The `1.0.0-beta.23` standard-library stability tier ledger and catalog
alignment slice adds the public
[`STDLIB_TIERS.md`](docs/language/STDLIB_TIERS.md) maturity ledger beside the
generated [`STDLIB_API.md`](docs/language/STDLIB_API.md) signature catalog.
It is documentation/catalog tooling clarity only: no source-language syntax,
stdlib helpers, compiler-known runtime names, runtime behavior, stable manifest
schema, stable Markdown schema, stable ABI/layout, or stable stdlib/API freeze
changes. The generated catalog and release gate now expose and check tier
metadata.
The `1.0.0-beta.24` package manifest identity and local dependency discipline
slice tightens local manifest diagnostics only: duplicate package manifest
keys, invalid dependency keys, and duplicate dependency keys are explicit beta
package/workspace diagnostics. It does not add remote registries, lockfiles,
semantic-version solving, package publishing, optional/dev/target
dependencies, stable package ABI/layout, source-language changes, runtime
changes, or standard-library changes.
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
@ -164,6 +352,31 @@ package/dependency summary, new workspace templates declare
local-package rules. Remote registries, lockfiles, semantic-version solving,
package publishing, and stable package ABI/layout remain deferred.
## 1.0.0-beta.24 Package Manifest Identity And Dependency Discipline
The `1.0.0-beta.24` release keeps the local package model closed and local. It
hardens diagnostics for manifest identity/dependency mistakes only: duplicate
package manifest keys, invalid dependency keys, and duplicate dependency keys
are reported explicitly.
This scope does not add a remote registry, lockfile, semantic-version solver,
package publishing flow, optional/dev/target dependencies, stable package
ABI/layout, source-language behavior, runtime behavior, or standard-library
behavior.
## 1.0.0-beta.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
The `1.0.0-beta.6` release adds a narrow blocking loopback TCP foundation:
@ -177,12 +390,231 @@ This is not a general networking stack. DNS, TLS, UDP, non-loopback binding,
async IO, HTTP frameworks, rich host-error ADTs, stable socket ABI/layout, and
automatic resource ownership remain deferred.
## 1.0.0-beta.7 Serialization And Data Interchange
The `1.0.0-beta.7` release adds a narrow JSON text-construction foundation:
- compiler-known `std.json.quote_string` for deterministic compact JSON string
quoting
- `lib/std/json.slo` source helpers for scalar values, fields, small arrays,
and small objects
- explicit std/local JSON example projects and a `json-quote-loop` benchmark
scaffold
This is not a complete JSON library. Full parsing beyond primitive scalar
tokens and the ASCII JSON string-token helper, object/array parsing,
recursive JSON values, maps/sets, streaming encoders or decoders, schema
validation, Unicode normalization, and a stable data-interchange API freeze
remain deferred.
## 1.0.0-beta.8 Concrete Type Alias Foundation
The `1.0.0-beta.8` release adds transparent concrete type aliases:
```slo
(type JsonText string)
```
Aliases are module-local names for already supported concrete target types.
They may appear in local signatures and annotations, but they do not create new
runtime representations or stable ABI names. Project imports of functions that
use aliases see the resolved concrete target type. Alias exports, imports,
re-exports, generic aliases, parameterized aliases, maps/sets, and new
compiler-known runtime names remain out of scope.
## 1.0.0-beta.9 Collection Alias Unification And Generic Reservation
The `1.0.0-beta.9` release applies beta.8 concrete aliases inside the current
source-authored collection/value-family facades. The public helper names and
runtime behavior remain concrete: current vectors, options, and results are
still concrete families, and the local aliases are erased before lowering.
This release also reserves the generic/map/set direction through diagnostics
and documentation only. It does not implement executable generics, maps, sets,
traits, inference, monomorphization, iterators, stable ABI/layout promises, or
a stable standard-library API freeze.
## 1.0.0-beta.10 Developer Experience API Discovery
The `1.0.0-beta.10` release upgrades the generated standard-library API
catalog from exported helper names to exact exported helper signatures. The
renderer verifies that every exported helper has a matching `(fn ...)` form,
normalizes module-local concrete aliases from the public signatures, omits
non-exported helpers and `(type ...)` aliases, and keeps the catalog generated
from `lib/std/*.slo`.
It also adds `glagol symbols <file.slo|project|workspace>` as an
editor-facing metadata command. The output is deterministic S-expression text
using `slovo.symbols` schema version `1.0.0-beta.10`; it includes module paths,
source spans/ranges, imports, exports, aliases, structs, enums, functions,
tests, and workspace package labels.
This release is tooling, documentation, and API-discovery work. It does not
add executable generics, maps, sets, new runtime helpers, new compiler-known
runtime names, ABI/layout guarantees, an LSP server, watch mode, or a stable
standard-library API freeze.
## 1.0.0-beta.11 Local Package API Documentation
The `1.0.0-beta.11` release extends the beta.10 API discovery lane to local
package and module documentation. `glagol doc <file|project|workspace> -o <dir>`
includes deterministic exported/public API sections for local modules and
workspace packages.
Those sections list exact exported function signatures, exported struct fields,
and exported enum variants with payload types. They omit non-exported
functions, structs, enums, tests, and aliases from the public API surface, and
they normalize module-local concrete aliases before rendering public types.
This remains beta API discovery. It does not freeze the Markdown schema, create
a stable stdlib/API compatibility freeze, add LSP/watch behavior, define
SARIF or daemon protocols, set diagnostics schema policy, implement executable
generics, maps, or sets, add re-exports, globs, or hierarchical modules, or
define package registry semantics.
## 1.0.0-beta.12 Concrete Vector Query And Prefix Parity
The `1.0.0-beta.12` release is a source-authored stdlib/helper parity update
for concrete vectors. It adds `count_of`, `starts_with`, `without_prefix`,
`ends_with`, and `without_suffix` to `std.vec_i64`, and adds `count_of` to
`std.vec_f64`, matching the already staged concrete helper style.
This release does not change the source language, runtime, compiler-known
`std.vec.*` names, ABI/layout contract, or performance contract. Generics,
maps/sets, iterators, mutable vectors, slice/view APIs, new runtime helper
names, stable stdlib API freeze, and broader collection abstractions remain
deferred.
## 1.0.0-beta.13 Diagnostic Catalog And Schema Policy
The `1.0.0-beta.13` release is a docs/tooling policy update for the existing
diagnostic surface. It adds
[`docs/language/DIAGNOSTICS.md`](docs/language/DIAGNOSTICS.md) as the beta
policy for `slovo.diagnostic` version `1`.
The policy documents the S-expression and JSON encodings, required and optional
machine fields, severity/source/range/related-span semantics, JSON-line
discipline, source-less diagnostics, artifact-manifest diagnostic metadata,
compatibility and migration classes, and the current code catalog covered by
the golden diagnostics contract.
This release does not change the source language, runtime, stdlib API,
diagnostic output shape, compiler CLI, LSP/watch behavior, SARIF/daemon
protocols, stable Markdown schema, or stable `1.0.0` diagnostics freeze.
## 1.0.0-beta.16 String Scanning And Token Boundary Foundation
The `1.0.0-beta.16` release adds the first explicit byte-oriented string
scanning and token-boundary helpers to the `std.string` source facade:
`byte_at_result`, `slice_result`, `starts_with`, and `ends_with`.
The helpers operate over the current NUL-terminated runtime string bytes before
the trailing NUL. `byte_at_result` and `slice_result` return `err 1` for
ordinary invalid indexes or ranges. `slice_result` returns a runtime-owned
string on success; allocation failure may trap with the existing string
allocation policy.
This release does not add Unicode scalar, grapheme, display-width, or locale
semantics; full JSON parsing; object/array parsing; tokenizer APIs; a language
slice/view feature; mutable strings; stable ABI/layout; or a stable stdlib/API
freeze.
## 1.0.0-beta.20 String Search And ASCII Trim Foundation
The `1.0.0-beta.20` scope extends the source-authored `std.string` facade
with byte-oriented search and ASCII trim helpers:
`contains`, `index_of_option`, `last_index_of_option`, `trim_ascii_start`,
`trim_ascii_end`, and `trim_ascii`.
This scope adds no compiler-known runtime names. It does not claim Unicode
scalar, grapheme, case-folding, locale, regex, tokenizer, mutable string,
language slice/view, stable ABI/layout, or stable stdlib/API semantics.
## 1.0.0-beta.21 JSON Document Scalar Parsing Foundation
The `1.0.0-beta.21` scope extends `std.json` with source-authored
whole-document scalar helpers:
`parse_string_document_result`, `parse_bool_document_result`,
`parse_i32_document_result`, `parse_u32_document_result`,
`parse_i64_document_result`, `parse_u64_document_result`,
`parse_f64_document_result`, and `parse_null_document_result`.
The helpers accept leading and trailing ASCII whitespace around a single
scalar document by composing with `std.string.trim_ascii`; trailing
non-whitespace still fails through the existing exact value-token parsers.
This scope does not add object/array parsing, recursive JSON values, parser
objects, maps/sets, streaming, Unicode escape decoding beyond the existing
string-token behavior, embedded NUL policy, new compiler-known runtime names,
stable ABI/layout, or stable stdlib/API semantics.
## 1.0.0-beta.22 Run Manifest And Execution Report Hardening
The `1.0.0-beta.22` scope hardens `glagol run --manifest` evidence by adding
an additive run-report block to run-mode artifact manifests. The block records
the executed program's exit status, captured stdout, captured stderr, and
forwarded program arguments.
This is tooling/CLI evidence work only. It does not add language syntax,
stdlib helpers, compiler-known runtime names, runtime C capabilities, package
or import behavior, stable artifact-manifest schema guarantees, stable
ABI/layout, or stable stdlib/API semantics.
## 1.0.0-beta.15 Reserved Generic Collection Boundary Hardening And Collection Ledger
The `1.0.0-beta.15` release documents the current concrete collection and
value-family boundary. It adds
[`docs/language/COLLECTIONS.md`](docs/language/COLLECTIONS.md), which links to
the generated
[`docs/language/STDLIB_API.md`](docs/language/STDLIB_API.md) catalog instead
of duplicating generated helper counts.
The ledger inventories concrete vector, option, result, and related
option/result-returning facade surfaces; records the design pressure from
duplicated concrete vector/option/result helpers; and defines prerequisites
before executable generics, generic aliases, maps, sets, iterators, mutable
vectors, or slice/view APIs can be promoted.
Current unsupported diagnostics are documented as release boundaries, not as
new behavior. This release does not change the source language, runtime,
stdlib/API surface, diagnostic output shape/codes/schema, benchmark metadata
schema, compiler ABI/layout behavior, or performance claims, and it does not
create a stable stdlib/API freeze. It does reword affected reserved-boundary
diagnostic messages from beta.9-specific text to current-beta wording while
preserving diagnostic codes, schema, spans, expected/found values, hints, and
output shape.
## 1.0.0-beta.14 Benchmark Suite Catalog And Metadata Gate
The `1.0.0-beta.14` release documents the existing benchmark suite catalog as
beta-scoped metadata tooling. It adds
[`benchmarks/README.md`](benchmarks/README.md) with the current ten-suite
inventory, local evidence policy, suite-list commands, and exclusions.
The root suite catalog commands are:
```bash
python3 benchmarks/runner.py --suite-list
python3 benchmarks/runner.py --suite-list --json
```
The non-JSON listing is for local review. The JSON listing is beta tooling
metadata for local gates and adapters, not a stable public schema.
This release does not add benchmark kernels, publish timing numbers, define
performance thresholds, change the source language, runtime, stdlib/API
surface, diagnostic output, compiler ABI/layout behavior, or make cross-machine
performance claims.
## Documentation
- [Language Manifest](docs/language/MANIFEST.md)
- [Language Specification](docs/language/SPEC-v1.md)
- [Diagnostics Policy](docs/language/DIAGNOSTICS.md)
- [Local Package And Workspace Guide](docs/language/PACKAGES.md)
- [Standard Library API Catalog](docs/language/STDLIB_API.md)
- [Collection Ledger](docs/language/COLLECTIONS.md)
- [Benchmark Suite Catalog](benchmarks/README.md)
- [Compiler Manifest](docs/compiler/GLAGOL_COMPILER_MANIFEST.md)
- [Post-Beta Roadmap](docs/POST_BETA_ROADMAP.md)
- [Slovo Whitepaper](docs/papers/SLOVO_WHITEPAPER.md)

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.

1
benchmarks/json-quote-loop/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
build/

View File

@ -0,0 +1,71 @@
# JSON Quote Loop Benchmark Scaffold
Release: `1.0.0-beta.7`.
This benchmark compares compact JSON string quoting plus checksum accumulation
across Slovo, C, Rust, Python, Clojure, and Common Lisp/SBCL on the same
machine. It is same machine local evidence only.
It is not a published benchmark result, performance threshold, optimizer
claim, or cross-machine comparison.
## Files
- `src/main.slo`: Slovo project benchmark fixture
- `c/json_quote_loop.c`: C comparison implementation
- `rust/json_quote_loop.rs`: Rust comparison implementation
- `python/json_quote_loop.py`: Python comparison implementation
- `clojure/json_quote_loop.clj`: Clojure comparison implementation
- `common-lisp/json_quote_loop.lisp`: Common Lisp/SBCL comparison implementation
- `run.py`: build/run/timing harness
All implementations print checksum `15000001` for loop count `1000000` and
target string `slo"vo\path`. Hot-loop mode uses loop count `10000000` and
checksum `150000001`.
## Commands
Run from the repository root:
```bash
python3 benchmarks/json-quote-loop/run.py --list
python3 benchmarks/json-quote-loop/run.py --dry-run
python3 benchmarks/json-quote-loop/run.py --only python --repeats 3 --warmups 1
python3 benchmarks/json-quote-loop/run.py --mode hot-loop --only slovo --only c --only rust
```
To include Slovo, build or point at a Glagol binary and make sure host Clang is
available:
```bash
cargo build --manifest-path compiler/Cargo.toml --bin glagol
python3 benchmarks/json-quote-loop/run.py --glagol compiler/target/debug/glagol
```
The runner skips missing C/Rust/Slovo/Clojure/SBCL toolchains where possible.
Use `--only` multiple times to select implementations:
```bash
python3 benchmarks/json-quote-loop/run.py --only slovo --only c --only rust --only clojure --only common_lisp
```
Clojure is detected with `clojure` on PATH, `CLOJURE`, or `CLOJURE_JAR`.
Common Lisp is detected with `sbcl` on PATH, `SBCL`, or `--sbcl`.
## Comparison Method
- The runner builds each implementation once before timing. The reported
numbers measure execution only, not compile time.
- Slovo timings use `glagol build`, which currently lowers to LLVM and then
invokes host `clang -O2` with `runtime/runtime.c`.
- C timings use `clang -O2 -std=c11`.
- Rust timings use `rustc -C opt-level=3 -C debuginfo=0`.
- The measured loop quotes one runtime-supplied ASCII string containing a
quote and a backslash, then accumulates the quoted byte length.
Timing is cold-process local-machine evidence only. Clojure timings include
JVM and Clojure startup, while Common Lisp timings include SBCL script
startup.
Hot-loop mode is startup-amortized local evidence. It runs a larger loop count
and reports total time plus normalized time for the base `1000000` loop count.

View File

@ -0,0 +1,11 @@
{
"benchmark": "json-quote-loop",
"source_stem": "json_quote_loop",
"loop_count": 1000000,
"expected_checksum": "15000001",
"stdin": "1000000\n",
"hot_loop_count": 10000000,
"hot_expected_checksum": "150000001",
"hot_stdin": "10000000\n",
"run_args": ["slo\"vo\\path"]
}

View File

@ -0,0 +1,118 @@
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define LOOP_COUNT 1000000
#define EXPECTED_CHECKSUM 15000001
static int32_t configured_loop_count(void) {
int32_t value = LOOP_COUNT;
if (scanf("%d", &value) != 1 || value <= 0) {
return LOOP_COUNT;
}
return value;
}
static const char *configured_target(int argc, char **argv) {
return argc > 1 ? argv[1] : "slo\"vo\\path";
}
static char json_hex_digit(unsigned char value) {
return value < 10 ? (char)('0' + value) : (char)('A' + (value - 10));
}
static char *quote_json_string(const char *text) {
size_t len = 2;
for (const unsigned char *cursor = (const unsigned char *)text; *cursor != '\0'; cursor++) {
switch (*cursor) {
case '"':
case '\\':
case '\n':
case '\t':
case '\r':
case '\b':
case '\f':
len += 2;
break;
default:
len += *cursor < 0x20 ? 6 : 1;
break;
}
}
char *out = malloc(len + 1);
if (out == NULL) {
exit(2);
}
char *write = out;
*write++ = '"';
for (const unsigned char *cursor = (const unsigned char *)text; *cursor != '\0'; cursor++) {
switch (*cursor) {
case '"':
*write++ = '\\';
*write++ = '"';
break;
case '\\':
*write++ = '\\';
*write++ = '\\';
break;
case '\n':
*write++ = '\\';
*write++ = 'n';
break;
case '\t':
*write++ = '\\';
*write++ = 't';
break;
case '\r':
*write++ = '\\';
*write++ = 'r';
break;
case '\b':
*write++ = '\\';
*write++ = 'b';
break;
case '\f':
*write++ = '\\';
*write++ = 'f';
break;
default:
if (*cursor < 0x20) {
*write++ = '\\';
*write++ = 'u';
*write++ = '0';
*write++ = '0';
*write++ = json_hex_digit((unsigned char)(*cursor >> 4));
*write++ = json_hex_digit((unsigned char)(*cursor & 0x0F));
} else {
*write++ = (char)*cursor;
}
break;
}
}
*write++ = '"';
*write = '\0';
return out;
}
static int32_t json_quote_loop(int32_t limit, const char *target) {
int32_t i = 0;
int32_t acc = 1;
while (i < limit) {
char *quoted = quote_json_string(target);
acc += (int32_t)strlen(quoted);
free(quoted);
i += 1;
}
return acc;
}
int main(int argc, char **argv) {
int32_t result = json_quote_loop(configured_loop_count(), configured_target(argc, argv));
printf("%d\n", result);
return result == EXPECTED_CHECKSUM ? 0 : 1;
}

View File

@ -0,0 +1,42 @@
(set! *warn-on-reflection* true)
(set! *unchecked-math* :warn-on-boxed)
(def loop-count 1000000)
(def expected-checksum 15000001)
(defn configured-loop-count []
(try
(let [line (read-line)
value (Integer/parseInt (.trim ^String line))]
(if (pos? value) value loop-count))
(catch Exception _
loop-count)))
(defn configured-target []
(or (first *command-line-args*) "slo\"vo\\path"))
(defn quote-json-string [^String value]
(let [builder (StringBuilder.)]
(.append builder \")
(dotimes [index (.length value)]
(let [ch (.charAt value index)]
(case ch
\" (.append builder "\\\"")
\\ (.append builder "\\\\")
\newline (.append builder "\\n")
\tab (.append builder "\\t")
\return (.append builder "\\r")
(.append builder ch))))
(.append builder \")
(.toString builder)))
(defn json-quote-loop [limit target]
(loop [i 0
acc 1]
(if (< i limit)
(recur (inc i) (+ acc (.length ^String (quote-json-string target))))
acc)))
(let [result (json-quote-loop (configured-loop-count) (configured-target))]
(println result)
(System/exit (if (= result expected-checksum) 0 1)))

View File

@ -0,0 +1,47 @@
(declaim (optimize (speed 3) (safety 0) (debug 0)))
(defconstant +loop-count+ 1000000)
(defconstant +expected-checksum+ 15000001)
(declaim (ftype (function () fixnum) configured-loop-count))
(defun configured-loop-count ()
(handler-case
(let ((line (read-line *standard-input* nil nil)))
(if line
(let ((value (parse-integer line :junk-allowed t)))
(if (> value 0) value +loop-count+))
+loop-count+))
(error () +loop-count+)))
(declaim (ftype (function () string) configured-target))
(defun configured-target ()
(or (second sb-ext:*posix-argv*) "slo\"vo\\path"))
(declaim (ftype (function (string) string) quote-json-string))
(defun quote-json-string (value)
(with-output-to-string (out)
(write-char #\" out)
(loop for ch across value
do (case ch
(#\" (write-string "\\\"" out))
(#\\ (write-string "\\\\" out))
(#\Newline (write-string "\\n" out))
(#\Tab (write-string "\\t" out))
(#\Return (write-string "\\r" out))
(otherwise (write-char ch out))))
(write-char #\" out)))
(declaim (ftype (function (fixnum string) fixnum) json-quote-loop))
(defun json-quote-loop (limit target)
(declare (type fixnum limit)
(type string target))
(loop with i of-type fixnum = 0
with acc of-type fixnum = 1
while (< i limit)
do (setf acc (+ acc (length (quote-json-string target)))
i (+ i 1))
finally (return acc)))
(let ((result (json-quote-loop (configured-loop-count) (configured-target))))
(format t "~D~%" result)
(sb-ext:exit :code (if (= result +expected-checksum+) 0 1)))

View File

@ -0,0 +1,53 @@
import sys
LOOP_COUNT = 1_000_000
EXPECTED_CHECKSUM = 15_000_001
def configured_loop_count() -> int:
try:
value = int(input().strip())
except (EOFError, ValueError):
return LOOP_COUNT
return value if value > 0 else LOOP_COUNT
def configured_target() -> str:
return sys.argv[1] if len(sys.argv) > 1 else 'slo"vo\\path'
def quote_json_string(value: str) -> str:
return (
'"'
+ value.replace("\\", "\\\\")
.replace('"', '\\"')
.replace("\n", "\\n")
.replace("\t", "\\t")
.replace("\r", "\\r")
.replace("\b", "\\b")
.replace("\f", "\\f")
+ '"'
)
def json_quote_loop(limit: int, target: str) -> int:
i = 0
acc = 1
while i < limit:
acc += len(quote_json_string(target))
i += 1
return acc
def main() -> int:
result = json_quote_loop(configured_loop_count(), configured_target())
print(result)
return 0 if result == EXPECTED_CHECKSUM else 1
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -0,0 +1,13 @@
#!/usr/bin/env python3
"""Run the local json-quote-loop benchmark scaffold."""
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from runner import main
if __name__ == "__main__":
raise SystemExit(main(Path(__file__).resolve().parent, sys.argv[1:]))

View File

@ -0,0 +1,62 @@
use std::io::Read;
const LOOP_COUNT: i32 = 1_000_000;
const EXPECTED_CHECKSUM: i32 = 15_000_001;
fn configured_loop_count() -> i32 {
let mut input = String::new();
if std::io::stdin().read_to_string(&mut input).is_err() {
return LOOP_COUNT;
}
input
.trim()
.parse::<i32>()
.ok()
.filter(|value| *value > 0)
.unwrap_or(LOOP_COUNT)
}
fn configured_target() -> String {
std::env::args()
.nth(1)
.unwrap_or_else(|| "slo\"vo\\path".to_string())
}
fn quote_json_string(value: &str) -> String {
let mut out = String::with_capacity(value.len() + 2);
out.push('"');
for ch in value.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
'\r' => out.push_str("\\r"),
'\u{08}' => out.push_str("\\b"),
'\u{0c}' => out.push_str("\\f"),
_ => out.push(ch),
}
}
out.push('"');
out
}
fn json_quote_loop(limit: i32, target: &str) -> i32 {
let mut i = 0;
let mut acc = 1;
while i < limit {
acc += quote_json_string(target).len() as i32;
i += 1;
}
acc
}
fn main() {
let target = configured_target();
let result = json_quote_loop(configured_loop_count(), &target);
println!("{}", result);
std::process::exit(if result == EXPECTED_CHECKSUM { 0 } else { 1 });
}

View File

@ -0,0 +1,4 @@
[project]
name = "json-quote-loop"
source_root = "src"
entry = "main"

View File

@ -0,0 +1,56 @@
; Benchmark scaffold fixture for local-machine JSON string quoting comparisons only.
; Keep LOOP_COUNT and EXPECTED_CHECKSUM aligned with the C/Rust/Python fixtures.
; The runner supplies the target through argv and the loop count through stdin
; or argv so the quoting path stays runtime-configured.
(module main)
(fn loop_count () -> i32
1000000)
(fn expected_checksum () -> i32
15000001)
(fn parse_stdin_loop_count () -> (result i32 i32)
(let input (result string i32) (std.io.read_stdin_result))
(match input
((ok text)
(std.string.parse_i32_result text))
((err code)
(err i32 i32 code))))
(fn parse_arg_loop_count () -> (result i32 i32)
(std.string.parse_i32_result (std.process.arg 2)))
(fn configured_stdin_loop_count () -> i32
(let parsed_stdin (result i32 i32) (parse_stdin_loop_count))
(if (is_ok parsed_stdin)
(unwrap_ok parsed_stdin)
(loop_count)))
(fn configured_loop_count () -> i32
(let parsed_arg (result i32 i32) (parse_arg_loop_count))
(if (is_ok parsed_arg)
(unwrap_ok parsed_arg)
(configured_stdin_loop_count)))
(fn target_text () -> string
(std.process.arg 1))
(fn json_quote_loop ((limit i32) (target string)) -> i32
(var i i32 0)
(var acc i32 1)
(while (< i limit)
(set acc (+ acc (std.string.len (std.json.quote_string target))))
(set i (+ i 1)))
acc)
(fn main () -> i32
(let result i32 (json_quote_loop (configured_loop_count) (target_text)))
(std.io.print_i32 result)
(if (= result (expected_checksum))
0
1))
(test "json quote loop checksum is deterministic"
(= (json_quote_loop 3 "slo\"vo\\path") 46))

View File

@ -18,6 +18,12 @@ from typing import Callable
TIMING_SCOPE = "local-machine comparison only"
TIMING_MODES = ["cold-process", "hot-loop"]
SUITE_NAME = "glagol-local-benchmark-suite"
LOCAL_TIMING_DISCLAIMER = (
"Local timing comparison only; not a published benchmark result and not a cross-machine performance claim."
)
REQUIRED_BENCHMARK_FILES = ["benchmark.json", "run.py", "slovo.toml", "src/main.slo"]
EXPECTED_IMPLEMENTATION_NAMES = ["slovo", "c", "rust", "python", "clojure", "common_lisp"]
@dataclass(frozen=True)
@ -52,6 +58,14 @@ class Implementation:
def main(root: Path, argv: list[str]) -> int:
if any(arg == "--suite-list" for arg in argv):
return main_suite(root, argv)
if not (root / "benchmark.json").is_file():
parser = argparse.ArgumentParser(description="Shared local Glagol benchmark runner.")
parser.add_argument("--suite-list", action="store_true", help="list suite metadata and exit")
parser.add_argument("--json", action="store_true", help="emit JSON for --suite-list")
parser.error("run from a benchmark run.py, or pass --suite-list at the benchmark suite root")
spec = read_spec(root)
implementations = available_implementations(root, spec)
parser = argparse.ArgumentParser(description=f"Run local {spec.name} timing comparisons.")
@ -89,6 +103,25 @@ def main(root: Path, argv: list[str]) -> int:
return 1 if any(result["status"] == "failed" for result in results) else 0
def main_suite(root: Path, argv: list[str]) -> int:
parser = argparse.ArgumentParser(description="List local Glagol benchmark suite metadata.")
parser.add_argument("--suite-list", action="store_true", help="list suite metadata and exit")
parser.add_argument("--json", action="store_true", help="emit JSON for suite metadata")
args = parser.parse_args(argv)
if not args.suite_list:
parser.error("pass --suite-list to list benchmark suite metadata")
suite_root = resolve_suite_root(root)
emit_suite_list(build_suite_catalog(suite_root), args.json)
return 0
def resolve_suite_root(root: Path) -> Path:
if (root / "benchmark.json").is_file():
return root.parent
return root
def read_spec(root: Path) -> BenchmarkSpec:
data = json.loads((root / "benchmark.json").read_text(encoding="utf-8"))
loop_count = int(data["loop_count"])
@ -278,6 +311,143 @@ def select_implementations(implementations: list[Implementation], names: list[st
return [impl for impl in implementations if impl.name in selected_names]
def build_suite_catalog(suite_root: Path) -> dict[str, object]:
benchmarks: list[dict[str, object]] = []
implementation_slot_count = 0
missing_required_files: list[str] = []
missing_implementation_slots: list[str] = []
for root in suite_benchmark_roots(suite_root):
spec = read_spec(root)
implementations = available_implementations(root, spec)
implementation_slot_count += len(implementations)
benchmark = suite_benchmark_metadata(suite_root, root, spec, implementations)
benchmarks.append(benchmark)
directory = str(benchmark["directory"])
for required_file in benchmark["required_files"]:
assert isinstance(required_file, dict)
if required_file["status"] != "present":
missing_required_files.append(f"{directory}/{required_file['path']}")
present_implementations = {
str(implementation["name"])
for implementation in benchmark["implementation_slots"]
if isinstance(implementation, dict)
}
for expected in EXPECTED_IMPLEMENTATION_NAMES:
if expected not in present_implementations:
missing_implementation_slots.append(f"{directory}:{expected}")
return {
"suite": SUITE_NAME,
"timing_scope": TIMING_SCOPE,
"timing_modes": TIMING_MODES,
"timing_disclaimer": LOCAL_TIMING_DISCLAIMER,
"benchmark_count": len(benchmarks),
"benchmarks": benchmarks,
"verification": {
"status": "ok" if not missing_required_files and not missing_implementation_slots else "incomplete",
"benchmark_metadata_files": len(benchmarks),
"required_files": len(benchmarks) * len(REQUIRED_BENCHMARK_FILES),
"missing_required_files": missing_required_files,
"implementation_slots": implementation_slot_count,
"expected_implementation_slots": len(benchmarks) * len(EXPECTED_IMPLEMENTATION_NAMES),
"missing_implementation_slots": missing_implementation_slots,
},
}
def suite_benchmark_roots(suite_root: Path) -> list[Path]:
return sorted(
[path for path in suite_root.iterdir() if path.is_dir() and (path / "benchmark.json").is_file()],
key=lambda path: path.name,
)
def suite_benchmark_metadata(
suite_root: Path,
root: Path,
spec: BenchmarkSpec,
implementations: list[Implementation],
) -> dict[str, object]:
return {
"name": spec.name,
"directory": str(root.relative_to(suite_root)),
"source_stem": spec.source_stem,
"timing_modes": TIMING_MODES,
"loop_count_source": "stdin",
"loop_count": spec.loop_count,
"hot_loop_count": spec.hot_loop_count,
"expected_checksum": spec.expected_checksum,
"hot_expected_checksum": spec.hot_expected_checksum,
"required_files": [
{
"path": relative,
"status": "present" if (root / relative).is_file() else "missing",
}
for relative in REQUIRED_BENCHMARK_FILES
],
"checksum_metadata": {
"cold_process": {
"expected_checksum": spec.expected_checksum,
"stdin": spec.stdin_text,
},
"hot_loop": {
"expected_checksum": spec.hot_expected_checksum,
"stdin": spec.hot_stdin_text,
},
},
"run_args": spec.run_args,
"implementation_slots": [
{
"name": impl.name,
"language": impl.language,
"source": str(impl.source.relative_to(suite_root)),
}
for impl in implementations
],
}
def emit_suite_list(metadata: dict[str, object], as_json: bool) -> None:
if as_json:
print(json.dumps(metadata, indent=2, sort_keys=True))
return
print(f"{metadata['suite']}: {metadata['timing_scope']}")
print(str(metadata["timing_disclaimer"]))
print(f"benchmark_count={metadata['benchmark_count']}")
print(f"timing_modes={','.join(TIMING_MODES)}")
verification = metadata["verification"]
assert isinstance(verification, dict)
print(f"verification_status={verification['status']}")
print(f"required_files={verification['required_files']}")
print(f"implementation_slots={verification['implementation_slots']}")
print("benchmarks:")
for benchmark in metadata["benchmarks"]:
assert isinstance(benchmark, dict)
print(
" {name} ({directory}): loop_count={loop_count} hot_loop_count={hot_loop_count} "
"expected_checksum={expected_checksum} hot_expected_checksum={hot_expected_checksum}".format(
name=benchmark["name"],
directory=benchmark["directory"],
loop_count=benchmark["loop_count"],
hot_loop_count=benchmark["hot_loop_count"],
expected_checksum=benchmark["expected_checksum"],
hot_expected_checksum=benchmark["hot_expected_checksum"],
)
)
print(" required_files:")
for required_file in benchmark["required_files"]:
assert isinstance(required_file, dict)
print(f" {required_file['path']}: {required_file['status']}")
print(" implementations:")
for implementation in benchmark["implementation_slots"]:
assert isinstance(implementation, dict)
print(f" {implementation['name']}: {implementation['language']} ({implementation['source']})")
def emit_list(root: Path, spec: BenchmarkSpec, implementations: list[Implementation], as_json: bool) -> None:
metadata = {
"benchmark": spec.name,
@ -520,3 +690,7 @@ def shlex_quote(value: str) -> str:
if value and all(char.isalnum() or char in "/._:-" for char in value):
return value
return "'" + value.replace("'", "'\"'\"'") + "'"
if __name__ == "__main__":
raise SystemExit(main(Path(__file__).resolve().parent, sys.argv[1:]))

2
compiler/Cargo.lock generated
View File

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

View File

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

View File

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

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::{
ast::{
BinaryOp, EnumDecl, Expr, ExprKind, Function, MatchArm, MatchPatternKind, Program,
StructDecl, StructInitField, Test,
StructDecl, StructInitField, Test, TypeAliasDecl,
},
diag::Diagnostic,
lower, std_runtime,
lower,
reserved::{is_generic_type_parameter_name, unsupported_generic_type_parameter},
std_runtime,
token::Span,
types::Type,
unsafe_ops,
@ -294,13 +296,23 @@ pub fn check_program_with_imports(
fn check_program_inner(
file: &str,
program: Program,
mut program: Program,
external_functions: &[ExternalFunction],
external_structs: &[ExternalStruct],
external_enums: &[ExternalEnum],
project_resolution: bool,
) -> Result<CheckedProgram, Vec<Diagnostic>> {
let mut errors = Vec::new();
errors.extend(resolve_type_aliases(
file,
&mut program,
external_structs,
external_enums,
));
if !errors.is_empty() {
return Err(errors);
}
let mut functions = HashMap::new();
let mut structs = HashMap::new();
let mut enums = HashMap::new();
@ -580,6 +592,645 @@ fn is_reserved_callable_name(name: &str) -> bool {
std_runtime::is_reserved_name(name) || unsafe_ops::is_reserved_head(name)
}
fn resolve_type_aliases(
file: &str,
program: &mut Program,
external_structs: &[ExternalStruct],
external_enums: &[ExternalEnum],
) -> Vec<Diagnostic> {
if program.type_aliases.is_empty() {
return Vec::new();
}
let mut errors = Vec::new();
let mut struct_names = HashMap::new();
for struct_decl in &program.structs {
struct_names
.entry(struct_decl.name.clone())
.or_insert(struct_decl.name_span);
}
for struct_decl in external_structs {
struct_names
.entry(struct_decl.name.clone())
.or_insert(struct_decl.span);
}
let mut enum_names = HashMap::new();
for enum_decl in &program.enums {
enum_names
.entry(enum_decl.name.clone())
.or_insert(enum_decl.name_span);
}
for enum_decl in external_enums {
enum_names
.entry(enum_decl.name.clone())
.or_insert(enum_decl.span);
}
let mut function_names = HashMap::new();
for function in &program.functions {
function_names
.entry(function.name.clone())
.or_insert(function.span);
}
let mut c_import_names = HashMap::new();
for import in &program.c_imports {
c_import_names
.entry(import.name.clone())
.or_insert(import.name_span);
}
let mut aliases = HashMap::<String, &TypeAliasDecl>::new();
for alias in &program.type_aliases {
match aliases.entry(alias.name.clone()) {
Entry::Vacant(entry) => {
entry.insert(alias);
}
Entry::Occupied(entry) => errors.push(
Diagnostic::new(
file,
"DuplicateTypeAlias",
format!("duplicate type alias `{}`", alias.name),
)
.with_span(alias.name_span)
.related("original type alias", entry.get().name_span),
),
}
}
for alias in &program.type_aliases {
if is_reserved_type_name(&alias.name) {
errors.push(
Diagnostic::new(
file,
"TypeAliasNameConflict",
format!(
"type alias `{}` conflicts with a built-in type name",
alias.name
),
)
.with_span(alias.name_span)
.hint("choose an alias name distinct from built-in type names"),
);
}
if let Some(span) = struct_names.get(&alias.name) {
errors.push(
Diagnostic::new(
file,
"TypeAliasNameConflict",
format!("type alias `{}` conflicts with a struct", alias.name),
)
.with_span(alias.name_span)
.related("struct declaration", *span),
);
}
if let Some(span) = enum_names.get(&alias.name) {
errors.push(
Diagnostic::new(
file,
"TypeAliasNameConflict",
format!("type alias `{}` conflicts with an enum", alias.name),
)
.with_span(alias.name_span)
.related("enum declaration", *span),
);
}
if let Some(span) = function_names.get(&alias.name) {
errors.push(
Diagnostic::new(
file,
"TypeAliasNameConflict",
format!("type alias `{}` conflicts with a function", alias.name),
)
.with_span(alias.name_span)
.related("function declaration", *span),
);
}
if let Some(span) = c_import_names.get(&alias.name) {
errors.push(
Diagnostic::new(
file,
"TypeAliasNameConflict",
format!("type alias `{}` conflicts with a C import", alias.name),
)
.with_span(alias.name_span)
.related("C import declaration", *span),
);
}
if matches!(&alias.target, Type::Named(name) if name == &alias.name) {
errors.push(
Diagnostic::new(
file,
"SelfTypeAlias",
format!("type alias `{}` directly aliases itself", alias.name),
)
.with_span(alias.target_span)
.hint("point the alias at an existing concrete type"),
);
}
}
if !errors.is_empty() {
return errors;
}
let known_structs = struct_names.keys().cloned().collect::<HashSet<_>>();
let known_enums = enum_names.keys().cloned().collect::<HashSet<_>>();
let mut resolved = HashMap::new();
let mut failed = HashSet::new();
let mut reported_cycles = BTreeSet::new();
let mut names = aliases.keys().cloned().collect::<Vec<_>>();
names.sort();
for name in names {
resolve_alias_target(
file,
&name,
&aliases,
&known_structs,
&known_enums,
&mut resolved,
&mut failed,
&mut Vec::new(),
&mut reported_cycles,
&mut errors,
);
}
if !errors.is_empty() {
return errors;
}
rewrite_program_alias_types(program, &resolved);
Vec::new()
}
fn resolve_alias_target(
file: &str,
name: &str,
aliases: &HashMap<String, &TypeAliasDecl>,
structs: &HashSet<String>,
enums: &HashSet<String>,
resolved: &mut HashMap<String, Type>,
failed: &mut HashSet<String>,
stack: &mut Vec<String>,
reported_cycles: &mut BTreeSet<String>,
errors: &mut Vec<Diagnostic>,
) -> Option<Type> {
if let Some(ty) = resolved.get(name) {
return Some(ty.clone());
}
if failed.contains(name) {
return None;
}
if let Some(cycle_start) = stack.iter().position(|candidate| candidate == name) {
let cycle = stack[cycle_start..].to_vec();
let mut key = cycle.clone();
key.sort();
if reported_cycles.insert(key.join("|")) {
errors.push(type_alias_cycle(file, &cycle, aliases));
}
failed.extend(cycle);
return None;
}
let alias = aliases.get(name).copied()?;
stack.push(name.to_string());
let target = resolve_alias_target_type(
file,
&alias.target,
alias.target_span,
aliases,
structs,
enums,
resolved,
failed,
stack,
reported_cycles,
errors,
);
stack.pop();
if let Some(target) = target {
resolved.insert(name.to_string(), target.clone());
Some(target)
} else {
failed.insert(name.to_string());
None
}
}
fn resolve_alias_target_type(
file: &str,
ty: &Type,
span: Span,
aliases: &HashMap<String, &TypeAliasDecl>,
structs: &HashSet<String>,
enums: &HashSet<String>,
resolved: &mut HashMap<String, Type>,
failed: &mut HashSet<String>,
stack: &mut Vec<String>,
reported_cycles: &mut BTreeSet<String>,
errors: &mut Vec<Diagnostic>,
) -> Option<Type> {
match ty {
Type::Named(name) if aliases.contains_key(name) => resolve_alias_target(
file,
name,
aliases,
structs,
enums,
resolved,
failed,
stack,
reported_cycles,
errors,
),
Type::Named(name) if structs.contains(name) || enums.contains(name) => {
Some(Type::Named(name.clone()))
}
Type::Named(name) => {
if is_generic_type_parameter_name(name) {
errors.push(unsupported_generic_type_parameter(file, span, name));
return None;
}
errors.push(
Diagnostic::new(
file,
"UnknownTypeAliasTarget",
format!("type alias target `{}` is not a known concrete type", name),
)
.with_span(span)
.expected("built-in type, known struct, known enum, or known type alias")
.found(name.clone())
.hint("declare the target type or alias before checking this alias set"),
);
None
}
Type::Array(inner, len) => {
let resolved_inner = resolve_alias_target_type(
file,
inner,
span,
aliases,
structs,
enums,
resolved,
failed,
stack,
reported_cycles,
errors,
)?;
let target = Type::Array(Box::new(resolved_inner), *len);
if alias_target_type_supported(&target, structs, enums) {
Some(target)
} else {
errors.push(unsupported_type_alias_target(file, span, &target));
None
}
}
Type::Vec(inner) => {
let resolved_inner = resolve_alias_target_type(
file,
inner,
span,
aliases,
structs,
enums,
resolved,
failed,
stack,
reported_cycles,
errors,
)?;
let target = Type::Vec(Box::new(resolved_inner));
if alias_target_type_supported(&target, structs, enums) {
Some(target)
} else {
errors.push(unsupported_type_alias_target(file, span, &target));
None
}
}
Type::Option(inner) => {
let resolved_inner = resolve_alias_target_type(
file,
inner,
span,
aliases,
structs,
enums,
resolved,
failed,
stack,
reported_cycles,
errors,
)?;
let target = Type::Option(Box::new(resolved_inner));
if alias_target_type_supported(&target, structs, enums) {
Some(target)
} else {
errors.push(unsupported_type_alias_target(file, span, &target));
None
}
}
Type::Result(ok, err) => {
let resolved_ok = resolve_alias_target_type(
file,
ok,
span,
aliases,
structs,
enums,
resolved,
failed,
stack,
reported_cycles,
errors,
)?;
let resolved_err = resolve_alias_target_type(
file,
err,
span,
aliases,
structs,
enums,
resolved,
failed,
stack,
reported_cycles,
errors,
)?;
let target = Type::Result(Box::new(resolved_ok), Box::new(resolved_err));
if alias_target_type_supported(&target, structs, enums) {
Some(target)
} else {
errors.push(unsupported_type_alias_target(file, span, &target));
None
}
}
_ => {
if alias_target_type_supported(ty, structs, enums) {
Some(ty.clone())
} else {
errors.push(unsupported_type_alias_target(file, span, ty));
None
}
}
}
}
fn alias_target_type_supported(
ty: &Type,
structs: &HashSet<String>,
enums: &HashSet<String>,
) -> bool {
match ty {
Type::I32 | Type::I64 | Type::U32 | Type::U64 | Type::F64 | Type::Bool | Type::String => {
true
}
Type::Named(name) => structs.contains(name) || enums.contains(name),
Type::Array(inner, len) => {
*len > 0
&& (matches!(
&**inner,
Type::I32
| Type::I64
| Type::U32
| Type::U64
| Type::F64
| Type::Bool
| Type::String
) || matches!(&**inner, Type::Named(name) if structs.contains(name) || enums.contains(name)))
}
Type::Vec(inner) => matches!(
&**inner,
Type::I32 | Type::I64 | Type::F64 | Type::Bool | Type::String
),
Type::Option(inner) => option_payload_type_supported(inner),
Type::Result(ok, err) => result_payload_types_supported(ok, err),
Type::Unit | Type::Ptr(_) | Type::Slice(_) => false,
}
}
fn unsupported_type_alias_target(file: &str, span: Span, ty: &Type) -> Diagnostic {
Diagnostic::new(
file,
"UnsupportedTypeAliasTarget",
"type alias target type is not supported in the current beta",
)
.with_span(span)
.expected("i32, i64, u32, u64, f64, bool, string, known struct, known enum, supported array, supported option, supported result, or supported vec")
.found(ty.to_string())
.hint("aliases are transparent and may only target concrete types already supported in the target use positions")
}
fn type_alias_cycle(
file: &str,
cycle: &[String],
aliases: &HashMap<String, &TypeAliasDecl>,
) -> Diagnostic {
let first = &cycle[0];
let first_alias = aliases[first];
let mut diag = Diagnostic::new(
file,
"TypeAliasCycle",
format!("type alias cycle includes `{}`", first),
)
.with_span(first_alias.name_span)
.hint("type aliases must resolve to an existing non-alias concrete type");
for name in &cycle[1..] {
diag = diag.related(
format!("cycle also includes `{}`", name),
aliases[name].name_span,
);
}
diag
}
fn rewrite_program_alias_types(program: &mut Program, aliases: &HashMap<String, Type>) {
for enum_decl in &mut program.enums {
for variant in &mut enum_decl.variants {
if let Some(payload_ty) = &mut variant.payload_ty {
*payload_ty = resolve_use_type(payload_ty, aliases);
}
}
}
for struct_decl in &mut program.structs {
for field in &mut struct_decl.fields {
field.ty = resolve_use_type(&field.ty, aliases);
}
}
for import in &mut program.c_imports {
for param in &mut import.params {
param.ty = resolve_use_type(&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(
file: &str,
struct_decl: &StructDecl,
@ -2008,6 +2659,10 @@ fn check_local_type(
}
fn unknown_named_type(file: &str, span: Span, name: &str, context: &str) -> Diagnostic {
if is_generic_type_parameter_name(name) {
return unsupported_generic_type_parameter(file, span, name);
}
Diagnostic::new(
file,
"UnknownStructType",
@ -4858,6 +5513,12 @@ fn check_vector_type(file: &str, ty: &Type, span: Span) -> Result<(), Diagnostic
return Ok(());
};
if let Type::Named(name) = &**inner {
if is_generic_type_parameter_name(name) {
return Err(unsupported_generic_type_parameter(file, span, name));
}
}
if **inner != Type::I32
&& **inner != Type::I64
&& **inner != Type::F64

View File

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

View File

@ -1,4 +1,8 @@
use std::{fs, path::Path};
use std::{
collections::{BTreeMap, BTreeSet},
fs,
path::Path,
};
use crate::{
ast::Program,
@ -6,6 +10,7 @@ use crate::{
lexer, lower,
project::{self, ProjectArtifact, SourceFile, ToolFailure, WorkspaceArtifact},
sexpr::{Atom, SExpr, SExprKind},
types::Type,
};
pub fn generate(input: &str, output_dir: &str) -> Result<(), ToolFailure> {
@ -56,16 +61,23 @@ fn render_project(
artifact: &ProjectArtifact,
sources: &[SourceFile],
) -> Result<String, ToolFailure> {
let modules = sources
.iter()
.map(document_source)
.collect::<Result<Vec<_>, _>>()?;
let mut out = String::new();
out.push_str("# Project ");
out.push_str(&artifact.project_name);
out.push_str("\n\n");
if let Some(workspace) = &artifact.workspace {
render_workspace(&mut out, workspace);
render_workspace_package_public_api(&mut out, workspace, &modules);
} else {
render_project_package_public_api(&mut out, artifact, &modules);
}
for source in sources {
let module = document_source(source)?;
render_module(&mut out, &module);
for module in &modules {
render_module(&mut out, module);
}
Ok(out)
}
@ -120,27 +132,58 @@ fn document_source(source: &SourceFile) -> Result<DocModule, ToolFailure> {
sources: vec![source.clone()],
artifact: None,
})?;
let program = lower::lower_program(&source.path, &forms).ok();
let lowerable_forms = forms
.iter()
.filter(|form| !matches!(list_head(form), Some("import")))
.cloned()
.collect::<Vec<_>>();
let program = lower::lower_program(&source.path, &lowerable_forms).ok();
Ok(module_from_forms(&source.path, &forms, program.as_ref()))
}
struct DocModule {
path: String,
title: String,
imports: Vec<String>,
exports: Vec<String>,
structs: Vec<String>,
functions: Vec<String>,
tests: Vec<String>,
public_api: PublicApi,
}
#[derive(Default)]
struct PublicApi {
functions: Vec<DocFunction>,
structs: Vec<DocStruct>,
enums: Vec<DocEnum>,
}
struct DocFunction {
name: String,
signature: String,
}
struct DocStruct {
name: String,
fields: Vec<String>,
}
struct DocEnum {
name: String,
variants: Vec<String>,
}
fn module_from_forms(file: &str, forms: &[SExpr], program: Option<&Program>) -> DocModule {
let mut module = DocModule {
path: file.to_string(),
title: file.to_string(),
imports: Vec::new(),
exports: Vec::new(),
structs: Vec::new(),
functions: Vec::new(),
tests: Vec::new(),
public_api: PublicApi::default(),
};
for form in forms {
@ -205,6 +248,7 @@ fn module_from_forms(file: &str, forms: &[SExpr], program: Option<&Program>) ->
})
.collect();
module.tests = program.tests.iter().map(|test| test.name.clone()).collect();
module.public_api = public_api_from_program(program, &module.exports);
}
module.imports.sort();
@ -221,11 +265,120 @@ fn render_module(out: &mut String, module: &DocModule) {
out.push_str("\n\n");
render_list(out, "Imports", &module.imports);
render_list(out, "Exports", &module.exports);
render_public_api_section(out, "###", &module.public_api);
render_list(out, "Structs", &module.structs);
render_list(out, "Functions", &module.functions);
render_list(out, "Tests", &module.tests);
}
fn render_project_package_public_api(
out: &mut String,
artifact: &ProjectArtifact,
modules: &[DocModule],
) {
let local_root = Path::new(&artifact.project_root).join(&artifact.source_root);
let local_modules = modules
.iter()
.filter(|module| Path::new(&module.path).starts_with(&local_root))
.collect::<Vec<_>>();
render_package_public_api(out, &artifact.project_name, &local_modules);
}
fn render_workspace_package_public_api(
out: &mut String,
workspace: &WorkspaceArtifact,
modules: &[DocModule],
) {
let by_path = modules
.iter()
.map(|module| (module.path.as_str(), module))
.collect::<BTreeMap<_, _>>();
for package in &workspace.packages {
let local_root = Path::new(&package.root).join(&package.source_root);
let package_modules = package
.modules
.iter()
.filter(|module| Path::new(&module.path).starts_with(&local_root))
.filter_map(|module| by_path.get(module.path.as_str()).copied())
.collect::<Vec<_>>();
render_package_public_api(
out,
&format!("{} {}", package.name, package.version),
&package_modules,
);
}
}
fn render_package_public_api(out: &mut String, package_name: &str, modules: &[&DocModule]) {
out.push_str("## Package API ");
out.push_str(package_name);
out.push_str("\n\n");
let public_modules = modules
.iter()
.copied()
.filter(|module| !module.public_api.is_empty())
.collect::<Vec<_>>();
if public_modules.is_empty() {
out.push_str("None.\n\n");
return;
}
for module in public_modules {
out.push_str("### Module ");
out.push_str(&module.title);
out.push_str("\n\n");
render_public_api_body(out, "####", &module.public_api);
}
}
fn render_public_api_section(out: &mut String, heading: &str, public_api: &PublicApi) {
out.push_str(heading);
out.push_str(" Public API\n\n");
render_public_api_body(out, "####", public_api);
}
fn render_public_api_body(out: &mut String, heading: &str, public_api: &PublicApi) {
if public_api.is_empty() {
out.push_str("None.\n\n");
return;
}
if !public_api.functions.is_empty() {
out.push_str(heading);
out.push_str(" Functions\n");
for function in &public_api.functions {
render_code_bullet(out, &function.signature);
}
out.push('\n');
}
if !public_api.structs.is_empty() {
out.push_str(heading);
out.push_str(" Structs\n");
for struct_decl in &public_api.structs {
render_code_bullet(out, &format!("struct {}", struct_decl.name));
for field in &struct_decl.fields {
render_indented_code_bullet(out, field);
}
}
out.push('\n');
}
if !public_api.enums.is_empty() {
out.push_str(heading);
out.push_str(" Enums\n");
for enum_decl in &public_api.enums {
render_code_bullet(out, &format!("enum {}", enum_decl.name));
for variant in &enum_decl.variants {
render_indented_code_bullet(out, variant);
}
}
out.push('\n');
}
}
fn render_list(out: &mut String, title: &str, values: &[String]) {
out.push_str("### ");
out.push_str(title);
@ -242,6 +395,182 @@ fn render_list(out: &mut String, title: &str, values: &[String]) {
out.push('\n');
}
fn list_head(expr: &SExpr) -> Option<&str> {
list(expr).and_then(|items| items.first()).and_then(ident)
}
fn render_code_bullet(out: &mut String, value: &str) {
out.push_str("- `");
out.push_str(&value.replace('`', "\\`"));
out.push_str("`\n");
}
fn render_indented_code_bullet(out: &mut String, value: &str) {
out.push_str(" - `");
out.push_str(&value.replace('`', "\\`"));
out.push_str("`\n");
}
impl PublicApi {
fn is_empty(&self) -> bool {
self.functions.is_empty() && self.structs.is_empty() && self.enums.is_empty()
}
}
fn public_api_from_program(program: &Program, exports: &[String]) -> PublicApi {
let export_names = exports.iter().cloned().collect::<BTreeSet<_>>();
let aliases = alias_targets(program);
let mut functions = program
.functions
.iter()
.filter(|function| export_names.contains(&function.name))
.map(|function| DocFunction {
name: function.name.clone(),
signature: function_signature(
&function.name,
function
.params
.iter()
.map(|param| (param.name.as_str(), &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]> {
match &expr.kind {
SExprKind::List(items) => Some(items),

View File

@ -52,6 +52,16 @@ pub fn run_tests(
test_runner::run(file, &checked, filter)
}
pub fn list_tests(
file: &str,
source: &str,
filter: Option<&str>,
) -> Result<test_runner::TestRunSuccess, test_runner::TestRunFailure> {
let checked =
checked_program(file, source).map_err(test_runner::TestRunFailure::before_execution)?;
Ok(test_runner::list(&checked, filter))
}
fn checked_program(file: &str, source: &str) -> Result<check::CheckedProgram, Vec<Diagnostic>> {
let tokens = lexer::lex(file, source)?;
let forms = sexpr::parse(file, &tokens)?;

View File

@ -2,6 +2,11 @@ use std::collections::{HashMap, HashSet};
use crate::{
diag::Diagnostic,
reserved::{
is_unsupported_generic_standard_library_call, unsupported_generic_function,
unsupported_generic_standard_library_call, unsupported_generic_type_alias,
unsupported_reserved_type_diagnostic,
},
sexpr::{Atom, SExpr, SExprKind},
std_runtime,
token::Span,
@ -19,6 +24,7 @@ pub fn format(file: &str, source: &str, forms: &[SExpr]) -> Result<String, Vec<D
function_names: collect_function_names(forms),
struct_names: collect_struct_names(forms),
enum_names: collect_enum_names(forms),
type_alias_names: collect_type_alias_names(forms),
errors: comment_errors
.into_iter()
.map(|comment| unsupported_non_full_line_comment(file, comment.span))
@ -42,6 +48,7 @@ struct Formatter<'a> {
function_names: HashSet<String>,
struct_names: HashSet<String>,
enum_names: HashSet<String>,
type_alias_names: HashSet<String>,
errors: Vec<Diagnostic>,
}
@ -64,6 +71,7 @@ impl Formatter<'_> {
Some("module") => self.write_module(form),
Some("import") => self.write_import(form),
Some("import_c") => self.write_c_import(form),
Some("type") => self.write_type_alias(form),
Some("enum") => self.write_enum(form),
Some("struct") => self.write_struct(form),
Some("fn") => self.write_function(form),
@ -251,6 +259,67 @@ impl Formatter<'_> {
);
}
fn write_type_alias(&mut self, form: &SExpr) {
let Some(items) = expect_list(form) else {
self.errors.push(
Diagnostic::new(
self.file,
"MalformedTypeAlias",
"type alias form must be a list",
)
.with_span(form.span),
);
return;
};
if matches!(items.get(2).and_then(list_head), Some("type_params")) {
self.errors.push(unsupported_generic_type_alias(
self.file,
items.get(2).map_or(form.span, |item| item.span),
));
return;
}
if items.len() != 3 {
self.errors.push(
Diagnostic::new(
self.file,
"MalformedTypeAlias",
"type alias form must be `(type Alias TargetType)`",
)
.with_span(form.span),
);
return;
}
let Some(name) = expect_ident(&items[1]) else {
self.errors.push(
Diagnostic::new(
self.file,
"InvalidTypeAliasName",
"type alias name must be an identifier",
)
.with_span(items[1].span),
);
return;
};
let Some(target) = self.render_alias_target_type(&items[2]) else {
return;
};
self.reject_comments_before(
form.span.end,
"formatter does not support comments inside type alias forms",
);
self.output.push_str("(type ");
self.output.push_str(name);
self.output.push(' ');
self.output.push_str(&target);
self.output.push(')');
}
fn write_struct(&mut self, form: &SExpr) {
let Some(items) = expect_list(form) else {
self.errors.push(
@ -512,17 +581,17 @@ impl Formatter<'_> {
} else if is_ident(&variant_items[1], "string") {
"string".to_string()
} else if let Some(name) = expect_ident(&variant_items[1]) {
if self.struct_names.contains(name) {
if self.struct_names.contains(name) || self.type_alias_names.contains(name) {
name.to_string()
} else {
self.errors.push(
Diagnostic::new(
self.file,
"UnsupportedFormatterForm",
"formatter supports only unary direct i32, i64, f64, bool, string, and known non-recursive struct enum payload variants",
"formatter supports only unary direct i32, i64, f64, bool, string, known type aliases, and known non-recursive struct enum payload variants",
)
.with_span(variant_items[1].span)
.expected("i32, i64, f64, bool, string, or known non-recursive struct type"),
.expected("i32, i64, f64, bool, string, known type alias, or known non-recursive struct type"),
);
continue;
}
@ -531,10 +600,10 @@ impl Formatter<'_> {
Diagnostic::new(
self.file,
"UnsupportedFormatterForm",
"formatter supports only unary direct i32, i64, f64, bool, string, and known non-recursive struct enum payload variants",
"formatter supports only unary direct i32, i64, f64, bool, string, known type aliases, and known non-recursive struct enum payload variants",
)
.with_span(variant_items[1].span)
.expected("i32, i64, f64, bool, string, or known non-recursive struct type"),
.expected("i32, i64, f64, bool, string, known type alias, or known non-recursive struct type"),
);
continue;
};
@ -601,6 +670,12 @@ impl Formatter<'_> {
return;
}
if matches!(items.get(2).and_then(list_head), Some("type_params")) {
self.errors
.push(unsupported_generic_function(self.file, items[2].span));
return;
}
let Some(name) = expect_ident(&items[1]) else {
self.errors.push(
Diagnostic::new(
@ -847,8 +922,13 @@ impl Formatter<'_> {
} else if is_ident(&pair[1], "string") {
"string".to_string()
} else if let Some(name) = expect_ident(&pair[1]) {
if self.struct_names.contains(name) || self.enum_names.contains(name) {
if self.struct_names.contains(name)
|| self.enum_names.contains(name)
|| self.type_alias_names.contains(name)
{
name.to_string()
} else if self.push_reserved_type_error(&pair[1]) {
continue;
} else {
self.errors.push(
Diagnostic::new(
@ -861,14 +941,20 @@ impl Formatter<'_> {
continue;
}
} else if let Some(items) = expect_list(&pair[1]) {
if let Some(text) = render_option_result_type(items) {
if let Some(text) = render_option_result_type(items, &self.type_alias_names) {
text
} else if let Some(text) =
render_supported_array_type(items, &self.struct_names, &self.enum_names)
} else if let Some(text) = render_supported_array_type(
items,
&self.struct_names,
&self.enum_names,
&self.type_alias_names,
) {
text
} else if let Some(text) = render_supported_vec_type(items, &self.type_alias_names)
{
text
} else if let Some(text) = render_supported_vec_type(items) {
text
} else if self.push_reserved_type_error(&pair[1]) {
continue;
} else {
self.errors.push(
Diagnostic::new(
@ -946,11 +1032,18 @@ impl Formatter<'_> {
}
if let Some(name) = expect_ident(form) {
if self.struct_names.contains(name) || self.enum_names.contains(name) {
if self.struct_names.contains(name)
|| self.enum_names.contains(name)
|| self.type_alias_names.contains(name)
{
return Some(name.to_string());
}
}
if self.push_reserved_type_error(form) {
return None;
}
let Some(items) = expect_list(form) else {
self.errors.push(
Diagnostic::new(
@ -963,19 +1056,27 @@ impl Formatter<'_> {
return None;
};
if let Some(text) = render_option_result_type(items) {
if let Some(text) = render_option_result_type(items, &self.type_alias_names) {
return Some(text);
}
if let Some(text) = render_supported_array_type(items, &self.struct_names, &self.enum_names)
{
if let Some(text) = render_supported_array_type(
items,
&self.struct_names,
&self.enum_names,
&self.type_alias_names,
) {
return Some(text);
}
if let Some(text) = render_supported_vec_type(items) {
if let Some(text) = render_supported_vec_type(items, &self.type_alias_names) {
return Some(text);
}
if self.push_reserved_type_error(form) {
return None;
}
self.errors.push(
Diagnostic::new(
self.file,
@ -987,6 +1088,66 @@ impl Formatter<'_> {
None
}
fn render_alias_target_type(&mut self, form: &SExpr) -> Option<String> {
if let Some(text) = render_direct_type_atom(
form,
&self.struct_names,
&self.enum_names,
&self.type_alias_names,
) {
return Some(text);
}
if self.push_reserved_type_error(form) {
return None;
}
let Some(items) = expect_list(form) else {
self.errors.push(
Diagnostic::new(
self.file,
"InvalidTypeAliasTarget",
"type alias target type is invalid",
)
.with_span(form.span)
.expected("concrete type"),
);
return None;
};
if let Some(text) = render_option_result_type(items, &self.type_alias_names) {
return Some(text);
}
if let Some(text) = render_supported_array_type(
items,
&self.struct_names,
&self.enum_names,
&self.type_alias_names,
) {
return Some(text);
}
if let Some(text) = render_supported_vec_type(items, &self.type_alias_names) {
return Some(text);
}
if self.push_reserved_type_error(form) {
return None;
}
self.errors.push(
Diagnostic::new(
self.file,
"InvalidTypeAliasTarget",
"type alias target type is invalid",
)
.with_span(form.span)
.expected("concrete type"),
);
None
}
fn render_c_import_return_type(&mut self, form: &SExpr) -> Option<String> {
if is_ident(form, "i32") {
return Some("i32".to_string());
@ -1159,6 +1320,14 @@ impl Formatter<'_> {
Some(format!("({})", name))
}
}
other if is_unsupported_generic_standard_library_call(other) => {
self.errors.push(unsupported_generic_standard_library_call(
self.file,
items[0].span,
other,
));
None
}
other if std_runtime::is_standard_path(other) => {
self.errors
.push(std_runtime::unsupported_standard_library_call(
@ -1318,6 +1487,16 @@ impl Formatter<'_> {
is_array: false,
});
}
if self.type_alias_names.contains(name) {
return Some(RenderedType {
text: name.to_string(),
is_array: false,
});
}
}
if self.push_reserved_type_error(form) {
return None;
}
let Some(items) = expect_list(form) else {
@ -1332,20 +1511,24 @@ impl Formatter<'_> {
return None;
};
if let Some(text) = render_option_result_type(items) {
if let Some(text) = render_option_result_type(items, &self.type_alias_names) {
return Some(RenderedType {
text,
is_array: false,
});
}
if let Some(text) = render_supported_vec_type(items) {
if let Some(text) = render_supported_vec_type(items, &self.type_alias_names) {
return Some(RenderedType {
text,
is_array: false,
});
}
if self.push_reserved_type_error(form) {
return None;
}
if items.len() != 3 || !is_ident(&items[0], "array") {
self.errors.push(
Diagnostic::new(
@ -1362,7 +1545,11 @@ impl Formatter<'_> {
&items[1],
&self.struct_names,
&self.enum_names,
&self.type_alias_names,
) else {
if self.push_reserved_type_error(&items[1]) {
return None;
}
self.errors.push(
Diagnostic::new(
self.file,
@ -1422,11 +1609,18 @@ impl Formatter<'_> {
}
if let Some(name) = expect_ident(form) {
if self.struct_names.contains(name) || self.enum_names.contains(name) {
if self.struct_names.contains(name)
|| self.enum_names.contains(name)
|| self.type_alias_names.contains(name)
{
return Some(name.to_string());
}
}
if self.push_reserved_type_error(form) {
return None;
}
let Some(items) = expect_list(form) else {
self.errors.push(
Diagnostic::new(
@ -1439,19 +1633,27 @@ impl Formatter<'_> {
return None;
};
if let Some(text) = render_option_result_type(items) {
if let Some(text) = render_option_result_type(items, &self.type_alias_names) {
return Some(text);
}
if let Some(text) = render_supported_array_type(items, &self.struct_names, &self.enum_names)
{
if let Some(text) = render_supported_array_type(
items,
&self.struct_names,
&self.enum_names,
&self.type_alias_names,
) {
return Some(text);
}
if let Some(text) = render_supported_vec_type(items) {
if let Some(text) = render_supported_vec_type(items, &self.type_alias_names) {
return Some(text);
}
if self.push_reserved_type_error(form) {
return None;
}
self.errors.push(
Diagnostic::new(
self.file,
@ -2051,6 +2253,7 @@ impl Formatter<'_> {
&items[1],
&self.struct_names,
&self.enum_names,
&self.type_alias_names,
) else {
self.errors.push(
Diagnostic::new(
@ -2113,21 +2316,10 @@ impl Formatter<'_> {
return None;
}
let payload_type = if is_ident(&items[1], "i32") {
"i32"
} else if is_ident(&items[1], "i64") {
"i64"
} else if is_ident(&items[1], "u32") {
"u32"
} else if is_ident(&items[1], "u64") {
"u64"
} else if is_ident(&items[1], "f64") {
"f64"
} else if is_ident(&items[1], "bool") {
"bool"
} else if is_ident(&items[1], "string") {
"string"
} else {
let Some(payload_type) = render_payload_type_atom(&items[1], &self.type_alias_names) else {
if self.push_reserved_type_error(&items[1]) {
return None;
}
self.errors.push(
Diagnostic::new(
self.file,
@ -2169,21 +2361,24 @@ impl Formatter<'_> {
return None;
}
let result_type = if is_ident(&items[1], "i32") && is_ident(&items[2], "i32") {
"i32 i32"
} else if is_ident(&items[1], "i64") && is_ident(&items[2], "i32") {
"i64 i32"
} else if is_ident(&items[1], "u32") && is_ident(&items[2], "i32") {
"u32 i32"
} else if is_ident(&items[1], "u64") && is_ident(&items[2], "i32") {
"u64 i32"
} else if is_ident(&items[1], "f64") && is_ident(&items[2], "i32") {
"f64 i32"
} else if is_ident(&items[1], "bool") && is_ident(&items[2], "i32") {
"bool i32"
} else if is_ident(&items[1], "string") && is_ident(&items[2], "i32") {
"string i32"
} else {
let Some(ok_ty) = render_payload_type_atom(&items[1], &self.type_alias_names) else {
if self.push_reserved_type_error(&items[1]) {
return None;
}
self.errors.push(
Diagnostic::new(
self.file,
"UnsupportedFormatterForm",
"formatter supports only `(result i32 i32)`, `(result i64 i32)`, `(result u32 i32)`, `(result u64 i32)`, `(result f64 i32)`, `(result bool i32)`, and `(result string i32)` constructors",
)
.with_span(span),
);
return None;
};
let Some(err_ty) = render_result_err_type_atom(&items[2], &self.type_alias_names) else {
if self.push_reserved_type_error(&items[2]) {
return None;
}
self.errors.push(
Diagnostic::new(
self.file,
@ -2196,7 +2391,7 @@ impl Formatter<'_> {
};
let value = self.render_expr(&items[3], env)?;
Some(format!("({} {} {})", name, result_type, value))
Some(format!("({} {} {} {})", name, ok_ty, err_ty, value))
}
fn render_call(
@ -2234,6 +2429,15 @@ impl Formatter<'_> {
Some(output)
}
fn push_reserved_type_error(&mut self, form: &SExpr) -> bool {
if let Some(diagnostic) = unsupported_reserved_type_diagnostic(self.file, form) {
self.errors.push(diagnostic);
true
} else {
false
}
}
fn write_indented_rendered(&mut self, indent: &str, rendered: &str) {
push_indented(&mut self.output, indent, rendered);
}
@ -2355,6 +2559,19 @@ fn collect_enum_names(forms: &[SExpr]) -> HashSet<String> {
.collect()
}
fn collect_type_alias_names(forms: &[SExpr]) -> HashSet<String> {
forms
.iter()
.filter_map(|form| {
let items = expect_list(form)?;
if !is_ident(items.first()?, "type") {
return None;
}
expect_ident(items.get(1)?).map(str::to_string)
})
.collect()
}
fn qualified_enum_name(name: &str) -> Option<(&str, &str)> {
let (enum_name, variant) = name.split_once('.')?;
if enum_name.is_empty() || variant.is_empty() || variant.contains('.') {
@ -2467,89 +2684,19 @@ fn list_head(form: &SExpr) -> Option<&str> {
expect_ident(first)
}
fn render_option_result_type(items: &[SExpr]) -> Option<String> {
if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "i32") {
return Some("(option i32)".to_string());
fn render_option_result_type(
items: &[SExpr],
type_alias_names: &HashSet<String>,
) -> Option<String> {
if items.len() == 2 && is_ident(&items[0], "option") {
let payload = render_payload_type_atom(&items[1], type_alias_names)?;
return Some(format!("(option {})", payload));
}
if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "i64") {
return Some("(option i64)".to_string());
}
if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "u32") {
return Some("(option u32)".to_string());
}
if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "u64") {
return Some("(option u64)".to_string());
}
if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "f64") {
return Some("(option f64)".to_string());
}
if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "bool") {
return Some("(option bool)".to_string());
}
if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "string") {
return Some("(option string)".to_string());
}
if items.len() == 3
&& is_ident(&items[0], "result")
&& is_ident(&items[1], "i32")
&& is_ident(&items[2], "i32")
{
return Some("(result i32 i32)".to_string());
}
if items.len() == 3
&& is_ident(&items[0], "result")
&& is_ident(&items[1], "i64")
&& is_ident(&items[2], "i32")
{
return Some("(result i64 i32)".to_string());
}
if items.len() == 3
&& is_ident(&items[0], "result")
&& is_ident(&items[1], "u32")
&& is_ident(&items[2], "i32")
{
return Some("(result u32 i32)".to_string());
}
if items.len() == 3
&& is_ident(&items[0], "result")
&& is_ident(&items[1], "u64")
&& is_ident(&items[2], "i32")
{
return Some("(result u64 i32)".to_string());
}
if items.len() == 3
&& is_ident(&items[0], "result")
&& is_ident(&items[1], "f64")
&& is_ident(&items[2], "i32")
{
return Some("(result f64 i32)".to_string());
}
if items.len() == 3
&& is_ident(&items[0], "result")
&& is_ident(&items[1], "bool")
&& is_ident(&items[2], "i32")
{
return Some("(result bool i32)".to_string());
}
if items.len() == 3
&& is_ident(&items[0], "result")
&& is_ident(&items[1], "string")
&& is_ident(&items[2], "i32")
{
return Some("(result string i32)".to_string());
if items.len() == 3 && is_ident(&items[0], "result") {
let ok = render_payload_type_atom(&items[1], type_alias_names)?;
let err = render_result_err_type_atom(&items[2], type_alias_names)?;
return Some(format!("(result {} {})", ok, err));
}
None
@ -2559,42 +2706,27 @@ fn render_supported_array_constructor_type(
form: &SExpr,
struct_names: &HashSet<String>,
enum_names: &HashSet<String>,
type_alias_names: &HashSet<String>,
) -> Option<String> {
if is_ident(form, "i32") {
Some("i32".to_string())
} else if is_ident(form, "i64") {
Some("i64".to_string())
} else if is_ident(form, "u32") {
Some("u32".to_string())
} else if is_ident(form, "u64") {
Some("u64".to_string())
} else if is_ident(form, "f64") {
Some("f64".to_string())
} else if is_ident(form, "bool") {
Some("bool".to_string())
} else if is_ident(form, "string") {
Some("string".to_string())
} else if let Some(name) = expect_ident(form) {
if struct_names.contains(name) || enum_names.contains(name) {
Some(name.to_string())
} else {
None
}
} else {
None
}
render_direct_type_atom(form, struct_names, enum_names, type_alias_names)
}
fn render_supported_array_type(
items: &[SExpr],
struct_names: &HashSet<String>,
enum_names: &HashSet<String>,
type_alias_names: &HashSet<String>,
) -> Option<String> {
if items.len() != 3 || !is_ident(&items[0], "array") {
return None;
}
let elem_ty = render_supported_array_constructor_type(&items[1], struct_names, enum_names)?;
let elem_ty = render_supported_array_constructor_type(
&items[1],
struct_names,
enum_names,
type_alias_names,
)?;
let len = expect_int(&items[2])?;
if len <= 0 {
return None;
@ -2603,7 +2735,10 @@ fn render_supported_array_type(
Some(format!("(array {} {})", elem_ty, len))
}
fn render_supported_vec_type(items: &[SExpr]) -> Option<String> {
fn render_supported_vec_type(
items: &[SExpr],
type_alias_names: &HashSet<String>,
) -> Option<String> {
if items.len() == 2 && is_ident(&items[0], "vec") {
if is_ident(&items[1], "i32") {
return Some("(vec i32)".to_string());
@ -2620,11 +2755,66 @@ fn render_supported_vec_type(items: &[SExpr]) -> Option<String> {
if is_ident(&items[1], "string") {
return Some("(vec string)".to_string());
}
if let Some(name) = expect_ident(&items[1]) {
if type_alias_names.contains(name) {
return Some(format!("(vec {})", name));
}
}
}
None
}
fn render_payload_type_atom(form: &SExpr, type_alias_names: &HashSet<String>) -> Option<String> {
if is_ident(form, "i32") {
Some("i32".to_string())
} else if is_ident(form, "i64") {
Some("i64".to_string())
} else if is_ident(form, "u32") {
Some("u32".to_string())
} else if is_ident(form, "u64") {
Some("u64".to_string())
} else if is_ident(form, "f64") {
Some("f64".to_string())
} else if is_ident(form, "bool") {
Some("bool".to_string())
} else if is_ident(form, "string") {
Some("string".to_string())
} else if let Some(name) = expect_ident(form) {
type_alias_names.contains(name).then(|| name.to_string())
} else {
None
}
}
fn render_result_err_type_atom(form: &SExpr, type_alias_names: &HashSet<String>) -> Option<String> {
if is_ident(form, "i32") {
Some("i32".to_string())
} else if let Some(name) = expect_ident(form) {
type_alias_names.contains(name).then(|| name.to_string())
} else {
None
}
}
fn render_direct_type_atom(
form: &SExpr,
struct_names: &HashSet<String>,
enum_names: &HashSet<String>,
type_alias_names: &HashSet<String>,
) -> Option<String> {
if let Some(text) = render_payload_type_atom(form, type_alias_names) {
return Some(text);
}
let name = expect_ident(form)?;
if struct_names.contains(name) || enum_names.contains(name) {
Some(name.to_string())
} else {
None
}
}
fn expect_list(form: &SExpr) -> Option<&[SExpr]> {
match &form.kind {
SExprKind::List(items) => Some(items),

View File

@ -26,12 +26,24 @@ pub fn emit(_file: &str, program: &CheckedProgram) -> Result<String, Vec<Diagnos
out.push_str("declare void @print_bool(i1)\n\n");
out.push_str("declare i32 @string_len(ptr)\n\n");
out.push_str("declare ptr @__glagol_string_concat(ptr, ptr)\n\n");
out.push_str("declare i64 @__glagol_string_byte_at_result(ptr, i32)\n\n");
out.push_str("declare ptr @__glagol_string_slice_result(ptr, i32, i32)\n\n");
out.push_str("declare i1 @__glagol_string_starts_with(ptr, ptr)\n\n");
out.push_str("declare i1 @__glagol_string_ends_with(ptr, ptr)\n\n");
out.push_str("declare i64 @__glagol_string_parse_i32_result(ptr)\n\n");
out.push_str("declare i32 @__glagol_string_parse_i64_result(ptr, ptr)\n\n");
out.push_str("declare i64 @__glagol_string_parse_u32_result(ptr)\n\n");
out.push_str("declare i32 @__glagol_string_parse_u64_result(ptr, ptr)\n\n");
out.push_str("declare i32 @__glagol_string_parse_f64_result(ptr, ptr)\n\n");
out.push_str("declare i32 @__glagol_string_parse_bool_result(ptr, ptr)\n\n");
out.push_str("declare ptr @__glagol_json_quote_string(ptr)\n\n");
out.push_str("declare ptr @__glagol_json_parse_string_value_result(ptr)\n\n");
out.push_str("declare i32 @__glagol_json_parse_bool_value_result(ptr, ptr)\n\n");
out.push_str("declare i64 @__glagol_json_parse_i32_value_result(ptr)\n\n");
out.push_str("declare i64 @__glagol_json_parse_u32_value_result(ptr)\n\n");
out.push_str("declare i32 @__glagol_json_parse_i64_value_result(ptr, ptr)\n\n");
out.push_str("declare i32 @__glagol_json_parse_u64_value_result(ptr, ptr)\n\n");
out.push_str("declare i32 @__glagol_json_parse_f64_value_result(ptr, ptr)\n\n");
out.push_str("declare ptr @__glagol_num_i32_to_string(i32)\n\n");
out.push_str("declare ptr @__glagol_num_i64_to_string(i64)\n\n");
out.push_str("declare ptr @__glagol_num_u32_to_string(i32)\n\n");
@ -1670,6 +1682,8 @@ impl FunctionGen<'_> {
| "__glagol_fs_read_open_text_result"
| "__glagol_net_tcp_read_all_result"
| "__glagol_io_read_stdin_result"
| "__glagol_string_slice_result"
| "__glagol_json_parse_string_value_result"
) {
return self.emit_string_result_host_call(expr, callee, &arg_values);
}
@ -1690,27 +1704,39 @@ impl FunctionGen<'_> {
|| callee == "__glagol_net_tcp_bound_port_result"
|| callee == "__glagol_net_tcp_accept_result"
|| callee == "__glagol_string_parse_i32_result"
|| callee == "__glagol_json_parse_i32_value_result"
|| callee == "__glagol_string_byte_at_result"
{
return self.emit_i32_result_encoded_i64_call(expr, callee, &arg_values);
}
if callee == "__glagol_string_parse_u32_result" {
if callee == "__glagol_string_parse_u32_result"
|| callee == "__glagol_json_parse_u32_value_result"
{
return self.emit_i32_result_encoded_i64_call(expr, callee, &arg_values);
}
if callee == "__glagol_string_parse_i64_result" {
if callee == "__glagol_string_parse_i64_result"
|| callee == "__glagol_json_parse_i64_value_result"
{
return self.emit_i64_result_out_param_call(expr, callee, &arg_values);
}
if callee == "__glagol_string_parse_u64_result" {
if callee == "__glagol_string_parse_u64_result"
|| callee == "__glagol_json_parse_u64_value_result"
{
return self.emit_i64_result_out_param_call(expr, callee, &arg_values);
}
if callee == "__glagol_string_parse_f64_result" {
if callee == "__glagol_string_parse_f64_result"
|| callee == "__glagol_json_parse_f64_value_result"
{
return self.emit_f64_result_out_param_call(expr, callee, &arg_values);
}
if callee == "__glagol_string_parse_bool_result" {
if callee == "__glagol_string_parse_bool_result"
|| callee == "__glagol_json_parse_bool_value_result"
{
return self.emit_bool_result_out_param_call(expr, callee, &arg_values);
}

View File

@ -5,9 +5,14 @@ use crate::{
ast::{
BinaryOp, CImportDecl, EnumDecl, EnumVariantDecl, Expr, ExprKind, Function, MatchArm,
MatchPattern, MatchPatternKind, Param, Program, StructDecl, StructField, StructInitField,
Test,
Test, TypeAliasDecl,
},
diag::Diagnostic,
reserved::{
is_unsupported_generic_standard_library_call, unsupported_generic_function,
unsupported_generic_standard_library_call, unsupported_generic_type_alias,
unsupported_reserved_type_diagnostic,
},
sexpr::{Atom, SExpr, SExprKind},
token::Span,
types::Type,
@ -23,6 +28,8 @@ pub fn lower_program_with_imported_names(
imported_names: &[String],
) -> Result<Program, Vec<Diagnostic>> {
let mut module = None;
let mut type_aliases = Vec::new();
let mut alias_names = HashMap::new();
let mut enums = Vec::new();
let mut enum_names = imported_names.iter().cloned().collect::<HashSet<_>>();
let mut structs = Vec::new();
@ -49,10 +56,67 @@ pub fn lower_program_with_imported_names(
}
Err(mut errs) => errors.append(&mut errs),
},
Some("type") => match lower_type_alias(file, form) {
Ok(alias) => match alias_names.entry(alias.name.clone()) {
Entry::Vacant(entry) => {
entry.insert(alias.name_span);
type_aliases.push(alias);
}
Entry::Occupied(entry) => errors.push(
Diagnostic::new(
file,
"DuplicateTypeAlias",
format!("duplicate type alias `{}`", alias.name),
)
.with_span(alias.name_span)
.related("original type alias", *entry.get()),
),
},
Err(mut errs) => errors.append(&mut errs),
},
_ => {}
}
}
for alias in &type_aliases {
if is_reserved_type_name(&alias.name) {
errors.push(
Diagnostic::new(
file,
"TypeAliasNameConflict",
format!(
"type alias `{}` conflicts with a built-in type name",
alias.name
),
)
.with_span(alias.name_span)
.hint("choose an alias name distinct from built-in type names"),
);
}
if let Some(struct_decl) = structs.iter().find(|decl| decl.name == alias.name) {
errors.push(
Diagnostic::new(
file,
"TypeAliasNameConflict",
format!("type alias `{}` conflicts with a struct", alias.name),
)
.with_span(alias.name_span)
.related("struct declaration", struct_decl.name_span),
);
}
if let Some(enum_decl) = enums.iter().find(|decl| decl.name == alias.name) {
errors.push(
Diagnostic::new(
file,
"TypeAliasNameConflict",
format!("type alias `{}` conflicts with an enum", alias.name),
)
.with_span(alias.name_span)
.related("enum declaration", enum_decl.name_span),
);
}
}
for form in forms {
match list_head(form) {
Some("module") => match lower_module(file, form) {
@ -61,6 +125,7 @@ pub fn lower_program_with_imported_names(
},
Some("enum") => {}
Some("struct") => {}
Some("type") => {}
Some("import_c") => match lower_c_import(file, form) {
Ok(import) => c_imports.push(import),
Err(mut errs) => errors.append(&mut errs),
@ -106,6 +171,7 @@ pub fn lower_program_with_imported_names(
if errors.is_empty() {
Ok(Program {
module: module.unwrap_or_else(|| "main".to_string()),
type_aliases,
enums,
structs,
c_imports,
@ -124,6 +190,14 @@ pub fn print_program(program: &Program) -> String {
output.push_str(&program.module);
output.push('\n');
for alias in &program.type_aliases {
output.push_str(" type ");
output.push_str(&alias.name);
output.push_str(" = ");
output.push_str(&alias.target.to_string());
output.push('\n');
}
for enum_decl in &program.enums {
output.push_str(" enum ");
output.push_str(&enum_decl.name);
@ -543,6 +617,77 @@ fn lower_module(file: &str, form: &SExpr) -> Result<String, Diagnostic> {
})
}
fn lower_type_alias(file: &str, form: &SExpr) -> Result<TypeAliasDecl, Vec<Diagnostic>> {
let mut errors = Vec::new();
let Some(items) = expect_list(form) else {
return Err(vec![Diagnostic::new(
file,
"MalformedTypeAlias",
"type alias form must be a list",
)
.with_span(form.span)]);
};
if matches!(items.get(2).and_then(list_head), Some("type_params")) {
return Err(vec![unsupported_generic_type_alias(
file,
items.get(2).map_or(form.span, |item| item.span),
)]);
}
if items.len() != 3 {
return Err(vec![Diagnostic::new(
file,
"MalformedTypeAlias",
"type alias form must be `(type Alias TargetType)`",
)
.with_span(form.span)
.expected("(type Alias TargetType)")]);
}
let name = match expect_ident(&items[1]) {
Some(name) => name.to_string(),
None => {
errors.push(
Diagnostic::new(
file,
"InvalidTypeAliasName",
"type alias name must be an identifier",
)
.with_span(items[1].span),
);
"<error>".to_string()
}
};
let target = match lower_type(&items[2]) {
Some(ty) => ty,
None => {
errors.push(invalid_type_diagnostic(
file,
&items[2],
"InvalidTypeAliasTarget",
"type alias target type is invalid",
Some("concrete type"),
None,
));
Type::Unit
}
};
if errors.is_empty() {
Ok(TypeAliasDecl {
name,
name_span: items[1].span,
target,
target_span: items[2].span,
span: form.span,
})
} else {
Err(errors)
}
}
fn lower_struct(file: &str, form: &SExpr) -> Result<StructDecl, Vec<Diagnostic>> {
let mut errors = Vec::new();
let Some(items) = expect_list(form) else {
@ -617,10 +762,14 @@ fn lower_struct(file: &str, form: &SExpr) -> Result<StructDecl, Vec<Diagnostic>>
};
let Some(ty) = lower_type(&pair[1]) else {
errors.push(
Diagnostic::new(file, "InvalidStructFieldType", "invalid struct field type")
.with_span(pair[1].span),
);
errors.push(invalid_type_diagnostic(
file,
&pair[1],
"InvalidStructFieldType",
"invalid struct field type",
None,
None,
));
continue;
};
@ -724,15 +873,14 @@ fn lower_enum(file: &str, form: &SExpr) -> Result<EnumDecl, Vec<Diagnostic>> {
};
let Some(payload_ty) = lower_type(&variant_items[1]) else {
errors.push(
Diagnostic::new(
file,
"InvalidEnumVariant",
"enum variant payload type is invalid",
)
.with_span(variant_items[1].span)
.expected("i32"),
);
errors.push(invalid_type_diagnostic(
file,
&variant_items[1],
"InvalidEnumVariant",
"enum variant payload type is invalid",
Some("i32"),
None,
));
continue;
};
@ -784,6 +932,10 @@ fn lower_function(
.hint("expected `(fn name ((arg Type) ...) -> ReturnType body...)`")]);
}
if matches!(items.get(2).and_then(list_head), Some("type_params")) {
return Err(vec![unsupported_generic_function(file, items[2].span)]);
}
let name = match expect_ident(&items[1]) {
Some(name) => name.to_string(),
None => {
@ -821,10 +973,14 @@ fn lower_function(
}
Some(ty) => ty,
None => {
errors.push(
Diagnostic::new(file, "InvalidReturnType", "invalid return type")
.with_span(items[4].span),
);
errors.push(invalid_type_diagnostic(
file,
&items[4],
"InvalidReturnType",
"invalid return type",
None,
None,
));
Type::Unit
}
};
@ -921,10 +1077,14 @@ fn lower_c_import(file: &str, form: &SExpr) -> Result<CImportDecl, Vec<Diagnosti
let return_type = match lower_type(&items[4]) {
Some(ty) => ty,
None => {
errors.push(
Diagnostic::new(file, "MalformedCImport", "invalid C import return type")
.with_span(items[4].span),
);
errors.push(invalid_type_diagnostic(
file,
&items[4],
"MalformedCImport",
"invalid C import return type",
None,
None,
));
Type::Unit
}
};
@ -1030,10 +1190,14 @@ fn lower_c_import_params(file: &str, form: &SExpr) -> Result<Vec<Param>, Vec<Dia
);
}
let Some(ty) = lower_type(&pair[1]) else {
errors.push(
Diagnostic::new(file, "MalformedCImport", "invalid C import parameter type")
.with_span(pair[1].span),
);
errors.push(invalid_type_diagnostic(
file,
&pair[1],
"MalformedCImport",
"invalid C import parameter type",
None,
None,
));
continue;
};
params.push(Param {
@ -1198,10 +1362,14 @@ fn lower_params(file: &str, form: &SExpr) -> Result<Vec<Param>, Vec<Diagnostic>>
};
let Some(ty) = lower_type(&pair[1]) else {
errors.push(
Diagnostic::new(file, "InvalidParamType", "invalid parameter type")
.with_span(pair[1].span),
);
errors.push(invalid_type_diagnostic(
file,
&pair[1],
"InvalidParamType",
"invalid parameter type",
None,
None,
));
continue;
};
@ -1249,6 +1417,28 @@ fn unsupported_unit_return_signature(file: &str, span: crate::token::Span) -> Di
.hint("`unit` is reserved for compiler/runtime unit-producing forms")
}
fn is_reserved_type_name(name: &str) -> bool {
matches!(
name,
"i32"
| "i64"
| "u32"
| "u64"
| "f64"
| "bool"
| "unit"
| "string"
| "ptr"
| "slice"
| "option"
| "result"
| "array"
| "vec"
| "map"
| "set"
)
}
fn lower_type(form: &SExpr) -> Option<Type> {
match &form.kind {
SExprKind::Atom(Atom::Ident(name)) => match name.as_str() {
@ -1556,6 +1746,9 @@ fn lower_expr(
span: form.span,
})
}
name if is_unsupported_generic_standard_library_call(name) => Err(
unsupported_generic_standard_library_call(file, items[0].span, name),
),
name => {
let mut args = Vec::new();
for item in &items[1..] {
@ -1930,13 +2123,14 @@ fn lower_array_init(
}
let Some(elem_ty) = lower_type(&items[1]) else {
return Err(Diagnostic::new(
return Err(invalid_type_diagnostic(
file,
&items[1],
"InvalidArrayElementType",
"array constructor element type is invalid",
)
.with_span(items[1].span)
.hint("fixed arrays use direct scalar `i32`, `i64`, `f64`, `bool`, `string`, known enum, or known non-recursive struct elements"));
None,
Some("fixed arrays use direct scalar `i32`, `i64`, `f64`, `bool`, `string`, known enum, or known non-recursive struct elements"),
));
};
let mut elements = Vec::new();
@ -1972,13 +2166,14 @@ fn lower_option_some(
}
let Some(payload_ty) = lower_type(&items[1]) else {
return Err(Diagnostic::new(
return Err(invalid_type_diagnostic(
file,
&items[1],
"InvalidOptionPayloadType",
"option constructor payload type is invalid",
)
.with_span(items[1].span)
.hint("first-pass options use `i32` payloads"));
None,
Some("first-pass options use `i32` payloads"),
));
};
Ok(Expr {
@ -2003,13 +2198,14 @@ fn lower_option_none(file: &str, form: &SExpr, items: &[SExpr]) -> Result<Expr,
}
let Some(payload_ty) = lower_type(&items[1]) else {
return Err(Diagnostic::new(
return Err(invalid_type_diagnostic(
file,
&items[1],
"InvalidOptionPayloadType",
"option constructor payload type is invalid",
)
.with_span(items[1].span)
.hint("first-pass options use `i32` payloads"));
None,
Some("first-pass options use `i32` payloads"),
));
};
Ok(Expr {
@ -2039,22 +2235,24 @@ fn lower_result_ok(
}
let Some(ok_ty) = lower_type(&items[1]) else {
return Err(Diagnostic::new(
return Err(invalid_type_diagnostic(
file,
&items[1],
"InvalidResultPayloadType",
"result constructor ok type is invalid",
)
.with_span(items[1].span)
.hint("first-pass results use `i32` payloads"));
None,
Some("first-pass results use `i32` payloads"),
));
};
let Some(err_ty) = lower_type(&items[2]) else {
return Err(Diagnostic::new(
return Err(invalid_type_diagnostic(
file,
&items[2],
"InvalidResultPayloadType",
"result constructor err type is invalid",
)
.with_span(items[2].span)
.hint("first-pass results use `i32` payloads"));
None,
Some("first-pass results use `i32` payloads"),
));
};
Ok(Expr {
@ -2087,22 +2285,24 @@ fn lower_result_err(
}
let Some(ok_ty) = lower_type(&items[1]) else {
return Err(Diagnostic::new(
return Err(invalid_type_diagnostic(
file,
&items[1],
"InvalidResultPayloadType",
"result constructor ok type is invalid",
)
.with_span(items[1].span)
.hint("first-pass results use `i32` payloads"));
None,
Some("first-pass results use `i32` payloads"),
));
};
let Some(err_ty) = lower_type(&items[2]) else {
return Err(Diagnostic::new(
return Err(invalid_type_diagnostic(
file,
&items[2],
"InvalidResultPayloadType",
"result constructor err type is invalid",
)
.with_span(items[2].span)
.hint("first-pass results use `i32` payloads"));
None,
Some("first-pass results use `i32` payloads"),
));
};
Ok(Expr {
@ -2224,10 +2424,14 @@ fn lower_local(
};
let Some(ty) = lower_type(&items[2]) else {
return Err(
Diagnostic::new(file, "InvalidLocalType", "invalid local type")
.with_span(items[2].span),
);
return Err(invalid_type_diagnostic(
file,
&items[2],
"InvalidLocalType",
"invalid local type",
None,
None,
));
};
Ok(Expr {
@ -2314,6 +2518,29 @@ fn lower_while(
})
}
fn invalid_type_diagnostic(
file: &str,
form: &SExpr,
fallback_code: &'static str,
fallback_message: impl Into<String>,
fallback_expected: Option<&str>,
fallback_hint: Option<&str>,
) -> Diagnostic {
if let Some(diagnostic) = unsupported_reserved_type_diagnostic(file, form) {
return diagnostic;
}
let mut diagnostic =
Diagnostic::new(file, fallback_code, fallback_message).with_span(form.span);
if let Some(expected) = fallback_expected {
diagnostic = diagnostic.expected(expected);
}
if let Some(hint) = fallback_hint {
diagnostic = diagnostic.hint(hint);
}
diagnostic
}
fn list_head(form: &SExpr) -> Option<&str> {
let items = expect_list(form)?;
let first = items.first()?;

View File

@ -8,9 +8,11 @@ mod lexer;
mod llvm;
mod lower;
mod project;
mod reserved;
mod scaffold;
mod sexpr;
mod std_runtime;
mod symbols;
mod test_runner;
mod token;
mod types;
@ -22,8 +24,11 @@ use std::{
panic::{self, AssertUnwindSafe},
path::{Path, PathBuf},
process::{self, Command as ProcessCommand},
thread,
};
const TEST_RUNNER_THREAD_STACK_SIZE: usize = 16 * 1024 * 1024;
fn main() {
let raw_args = env::args().collect::<Vec<_>>();
let command_line = raw_args.join(" ");
@ -49,9 +54,29 @@ fn run_invocation_guarded(invocation: Invocation) -> ! {
panic::set_hook(Box::new(|_| {}));
let run_invocation = invocation.clone();
let result = panic::catch_unwind(AssertUnwindSafe(move || {
run_invocation_inner(run_invocation);
}));
let result = if invocation.mode == Mode::RunTests {
let thread_invocation = run_invocation.clone();
match thread::Builder::new()
.name("glagol-test-runner".to_string())
.stack_size(TEST_RUNNER_THREAD_STACK_SIZE)
.spawn(move || {
panic::catch_unwind(AssertUnwindSafe(move || {
run_invocation_inner(thread_invocation);
}))
}) {
Ok(handle) => match handle.join() {
Ok(result) => result,
Err(payload) => Err(payload),
},
Err(_) => panic::catch_unwind(AssertUnwindSafe(move || {
run_invocation_inner(run_invocation);
})),
}
} else {
panic::catch_unwind(AssertUnwindSafe(move || {
run_invocation_inner(run_invocation);
}))
};
panic::set_hook(previous_hook);
@ -94,19 +119,26 @@ fn run_invocation_inner(invocation: Invocation) -> ! {
if invocation.mode == Mode::Doc {
run_doc(invocation);
}
if invocation.mode == Mode::Symbols && project::is_project_input(&invocation.path) {
run_project_symbols(invocation);
}
if invocation.mode == Mode::Format && invocation.fmt_action != FmtAction::Stdout {
run_fmt_action(invocation);
}
let project_capable_mode = matches!(invocation.mode, Mode::Check | Mode::Build | Mode::Run)
|| (invocation.mode == Mode::RunTests && invocation.manifest_mode_name == "test");
let project_capable_mode = matches!(
invocation.mode,
Mode::Check | Mode::Build | Mode::Run | Mode::Symbols
) || (invocation.mode == Mode::RunTests
&& invocation.manifest_mode_name == "test");
if project_capable_mode && project::is_project_input(&invocation.path) {
match invocation.mode {
Mode::Check => run_project_check(invocation),
Mode::RunTests => run_project_test(invocation),
Mode::Build => run_project_build(invocation),
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) -> ! {
match project::run_tests(&invocation.path, invocation.test_filter.as_deref()) {
let result = if invocation.test_list {
project::list_tests(&invocation.path, invocation.test_filter.as_deref())
} else {
project::run_tests(&invocation.path, invocation.test_filter.as_deref())
};
match result {
Ok(success) => {
let output = success.output;
let primary_output = if let Some(output_path) = invocation.output_path.as_deref() {
@ -211,6 +249,25 @@ fn run_project_run(invocation: Invocation) -> ! {
run_native_from_llvm(invocation, output.text, Some(output.artifact), Vec::new());
}
fn run_project_symbols(invocation: Invocation) -> ! {
let loaded = match project::load_project_sources_for_tools(&invocation.path) {
Ok(loaded) => loaded,
Err(failure) => exit_tool_failure(invocation, failure),
};
let output = match symbols::render_project(&loaded) {
Ok(output) => output,
Err(diagnostics) => exit_tool_failure(
invocation.clone(),
project::ToolFailure {
diagnostics,
sources: loaded.sources.clone(),
artifact: Some(loaded.artifact.clone()),
},
),
};
finish_symbols_output(invocation, output, Some(&loaded.artifact));
}
fn exit_project_failure(invocation: Invocation, failure: project::ProjectTestFailure) -> ! {
let rendered = render_source_diagnostics_multi(
&failure.diagnostics,
@ -233,11 +290,52 @@ fn exit_project_failure(invocation: Invocation, failure: project::ProjectTestFai
process::exit(ExitCode::SourceFailure.code());
}
fn exit_tool_failure(invocation: Invocation, failure: project::ToolFailure) -> ! {
let rendered = render_source_diagnostics_multi(
&failure.diagnostics,
&failure.sources,
invocation.diagnostics,
);
eprint!("{}", rendered.stderr);
write_manifest_if_requested_with_project(
&invocation,
false,
PrimaryOutput::Diagnostics {
text: &rendered.machine_text,
},
None,
None,
failure.artifact.as_ref(),
);
process::exit(ExitCode::SourceFailure.code());
}
fn run_text_mode(invocation: Invocation, mode: Mode, source: &str) -> ! {
if mode == Mode::RunTests {
run_test_mode(invocation, source);
}
if mode == Mode::Symbols {
match symbols::render_file(&invocation.path, source) {
Ok(output) => finish_symbols_output(invocation, output, None),
Err(diagnostics) => {
let rendered =
render_source_diagnostics(&diagnostics, source, invocation.diagnostics);
eprint!("{}", rendered.stderr);
write_manifest_if_requested(
&invocation,
false,
PrimaryOutput::Diagnostics {
text: &rendered.machine_text,
},
None,
None,
);
process::exit(ExitCode::SourceFailure.code());
}
}
}
let foreign_imports = c_imports_for_manifest(&invocation.path, source);
let result = match mode {
Mode::EmitLlvm => driver::compile_to_llvm(&invocation.path, source),
@ -247,6 +345,7 @@ fn run_text_mode(invocation: Invocation, mode: Mode, source: &str) -> ! {
Mode::InspectLoweringSurface => driver::inspect_lowering_surface(&invocation.path, source),
Mode::InspectLoweringChecked => driver::inspect_lowering_checked(&invocation.path, source),
Mode::CheckTests => driver::check_tests(&invocation.path, source),
Mode::Symbols => unreachable!("symbols mode is handled separately"),
Mode::RunTests => unreachable!("test mode is handled separately"),
Mode::Build => unreachable!("build is handled separately"),
Mode::Run => unreachable!("run is handled separately"),
@ -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) -> ! {
match scaffold::create_project(
&invocation.path,
@ -544,7 +685,13 @@ fn finish_formatted_source(
fn run_test_mode(invocation: Invocation, source: &str) -> ! {
let foreign_imports = c_imports_for_manifest(&invocation.path, source);
match driver::run_tests(&invocation.path, source, invocation.test_filter.as_deref()) {
let result = if invocation.test_list {
driver::list_tests(&invocation.path, source, invocation.test_filter.as_deref())
} else {
driver::run_tests(&invocation.path, source, invocation.test_filter.as_deref())
};
match result {
Ok(result) => {
let output = result.output;
let primary_output = if let Some(output_path) = invocation.output_path.as_deref() {
@ -745,14 +892,15 @@ fn run_native_from_llvm(
let _ = io::stdout().write_all(&run_output.stdout);
let _ = io::stderr().write_all(&run_output.stderr);
let stdout = String::from_utf8_lossy(&run_output.stdout).to_string();
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,
run_output.status.success(),
PrimaryOutput::Stdout {
kind: Mode::Run.output_kind(),
text: &stdout,
},
None,
Some(BuildInfo {
clang: &native.clang,
runtime: &native.runtime,
@ -760,8 +908,14 @@ fn run_native_from_llvm(
}),
&foreign_imports,
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 {
@ -966,6 +1120,7 @@ struct Invocation {
project_template: scaffold::ProjectTemplate,
link_c_paths: Vec<String>,
test_filter: Option<String>,
test_list: bool,
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 link_c_paths = Vec::new();
let mut test_filter = None;
let mut test_list = false;
let mut run_args = Vec::new();
let mut no_color = false;
let command_line = raw_args.join(" ");
@ -1210,7 +1366,18 @@ fn parse_args(raw_args: &[String]) -> Result<Args, ParseError> {
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() =>
{
let next = match arg.as_str() {
@ -1222,6 +1389,7 @@ fn parse_args(raw_args: &[String]) -> Result<Args, ParseError> {
"clean" => Mode::Clean,
"new" => Mode::New,
"doc" => Mode::Doc,
"symbols" => Mode::Symbols,
_ => unreachable!(),
};
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 {
return parse_error(
"`--` program arguments are only supported with `run`",
@ -1379,6 +1556,7 @@ fn parse_args(raw_args: &[String]) -> Result<Args, ParseError> {
project_template,
link_c_paths,
test_filter,
test_list,
run_args,
}))
}
@ -1452,6 +1630,7 @@ fn exit_parse_error(err: ParseError, command_line: &str) -> ! {
project_template: scaffold::ProjectTemplate::Binary,
link_c_paths: Vec::new(),
test_filter: None,
test_list: false,
run_args: Vec::new(),
};
write_manifest_or_exit(
@ -1464,6 +1643,7 @@ fn exit_parse_error(err: ParseError, command_line: &str) -> ! {
PrimaryOutput::Diagnostics { text: &stderr },
None,
None,
None,
&[],
None,
err.diagnostics,
@ -1489,6 +1669,7 @@ enum Mode {
Clean,
New,
Doc,
Symbols,
}
impl Mode {
@ -1507,6 +1688,7 @@ impl Mode {
Self::Clean => "clean",
Self::New => "new",
Self::Doc => "doc",
Self::Symbols => "symbols",
}
}
@ -1523,6 +1705,7 @@ impl Mode {
Self::Clean => "no-output",
Self::New => "no-output",
Self::Doc => "documentation",
Self::Symbols => "symbols",
}
}
@ -1714,6 +1897,13 @@ struct TestSummary {
filter: Option<String>,
}
struct RunReport<'a> {
exit_status: Option<i32>,
stdout: &'a str,
stderr: &'a str,
args: &'a [String],
}
fn test_summary_from_report(report: test_runner::TestReport) -> TestSummary {
TestSummary {
total_discovered: report.total_discovered,
@ -1816,6 +2006,34 @@ fn write_manifest_if_requested_with_foreign_imports(
success,
primary_output,
test_summary,
None,
build_info,
foreign_imports,
project_info,
invocation.diagnostics,
);
write_manifest_or_exit(manifest_path, &manifest);
}
}
fn write_manifest_if_requested_with_run_report(
invocation: &Invocation,
success: bool,
primary_output: PrimaryOutput<'_>,
build_info: Option<BuildInfo<'_>>,
foreign_imports: &[project::ProjectArtifactCImport],
project_info: Option<&project::ProjectArtifact>,
run_report: RunReport<'_>,
) {
if let Some(manifest_path) = invocation.manifest_path.as_deref() {
let manifest = render_manifest(
Some(&invocation.path),
&invocation.command_line,
Some(invocation.manifest_mode_name.as_str()),
success,
primary_output,
None,
Some(run_report),
build_info,
foreign_imports,
project_info,
@ -1839,6 +2057,7 @@ fn render_manifest(
success: bool,
primary_output: PrimaryOutput<'_>,
test_summary: Option<TestSummary>,
run_report: Option<RunReport<'_>>,
build_info: Option<BuildInfo<'_>>,
foreign_imports: &[project::ProjectArtifactCImport],
project_info: Option<&project::ProjectArtifact>,
@ -1862,7 +2081,10 @@ fn render_manifest(
" (success {})\n",
if success { "true" } else { "false" }
));
out.push_str(" (diagnostics-schema-version 1)\n");
out.push_str(&format!(
" (diagnostics-schema-version {})\n",
diag::DIAGNOSTIC_SCHEMA_VERSION
));
out.push_str(&format!(
" (diagnostics-encoding {})\n",
match diagnostics {
@ -1937,6 +2159,34 @@ fn render_manifest(
out.push_str(" )");
}
if let Some(report) = run_report {
out.push('\n');
out.push_str(" (run-report\n");
match report.exit_status {
Some(status) => out.push_str(&format!(" (exit-status {})\n", status)),
None => out.push_str(" (exit-status null)\n"),
}
out.push_str(&format!(
" (stdout {})\n",
diag::render_string(report.stdout)
));
out.push_str(&format!(
" (stderr {})\n",
diag::render_string(report.stderr)
));
out.push_str(" (args");
if report.args.is_empty() {
out.push_str(")\n");
} else {
out.push('\n');
for arg in report.args {
out.push_str(&format!(" (arg {})\n", diag::render_string(arg)));
}
out.push_str(" )\n");
}
out.push_str(" )");
}
if let Some(build) = build_info {
out.push('\n');
out.push_str(" (hosted-build\n");
@ -2425,6 +2675,6 @@ fn normalized_output_path(path: &str) -> Option<PathBuf> {
fn print_usage() {
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_structs: HashMap<String, DeclInfo>,
local_enums: HashMap<String, DeclInfo>,
local_aliases: HashMap<String, DeclInfo>,
}
#[derive(Debug, Clone)]
@ -204,6 +205,7 @@ enum DeclKind {
CImport,
Struct,
Enum,
TypeAlias,
}
#[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 {
program: CheckedProgram,
sources: Vec<SourceFile>,
@ -1102,7 +1124,16 @@ fn parse_manifest(path: PathBuf, source: String) -> Result<Manifest, Vec<Diagnos
};
match key {
"name" => set_manifest_key(&file, &mut errors, &mut name, parsed, line.span, "name"),
"name" => set_manifest_key(
&file,
&mut errors,
&mut name,
parsed,
line.span,
"name",
"ProjectManifestInvalid",
"project",
),
"source_root" => set_manifest_key(
&file,
&mut errors,
@ -1110,8 +1141,19 @@ fn parse_manifest(path: PathBuf, source: String) -> Result<Manifest, Vec<Diagnos
parsed,
line.span,
"source_root",
"ProjectManifestInvalid",
"project",
),
"entry" => set_manifest_key(
&file,
&mut errors,
&mut entry,
parsed,
line.span,
"entry",
"ProjectManifestInvalid",
"project",
),
"entry" => set_manifest_key(&file, &mut errors, &mut entry, parsed, line.span, "entry"),
_ => errors.push(
Diagnostic::new(
&file,
@ -1390,6 +1432,7 @@ fn parse_package_manifest(
let mut source_root = None::<String>;
let mut entry = None::<String>;
let mut dependencies = Vec::new();
let mut dependency_keys = BTreeSet::<String>::new();
for line in manifest_lines(&source) {
let trimmed = line.text.trim();
@ -1467,6 +1510,8 @@ fn parse_package_manifest(
parsed,
line.span,
"name",
"PackageManifestInvalid",
"package",
),
"version" => set_manifest_key(
&file,
@ -1475,6 +1520,8 @@ fn parse_package_manifest(
parsed,
line.span,
"version",
"PackageManifestInvalid",
"package",
),
"source_root" => set_manifest_key(
&file,
@ -1483,6 +1530,8 @@ fn parse_package_manifest(
parsed,
line.span,
"source_root",
"PackageManifestInvalid",
"package",
),
"entry" => set_manifest_key(
&file,
@ -1491,6 +1540,8 @@ fn parse_package_manifest(
parsed,
line.span,
"entry",
"PackageManifestInvalid",
"package",
),
other => errors.push(
Diagnostic::new(
@ -1502,21 +1553,54 @@ fn parse_package_manifest(
),
}
}
"dependencies" => match parse_dependency_path(value) {
Some(path) => dependencies.push(PackageDependency {
key: key.to_string(),
path,
span: line.span,
}),
None => errors.push(
Diagnostic::new(
&file,
"UnsupportedDependency",
"workspace dependencies must use `{ path = \"...\" }` local path records only",
)
.with_span(line.span),
),
},
"dependencies" => {
let valid_key = if is_project_name(key) {
true
} else {
errors.push(
Diagnostic::new(
&file,
"InvalidPackageDependencyName",
format!(
"package dependency name `{}` must start with `a-z` and contain only `a-z`, `0-9`, and `-`",
key
),
)
.with_span(line.span),
);
false
};
let unique_key = if dependency_keys.insert(key.to_string()) {
true
} else {
errors.push(
Diagnostic::new(
&file,
"DuplicatePackageDependencyName",
format!("duplicate package dependency name `{}`", key),
)
.with_span(line.span),
);
false
};
match parse_dependency_path(value) {
Some(path) if valid_key && unique_key => dependencies.push(PackageDependency {
key: key.to_string(),
path,
span: line.span,
}),
Some(_) => {}
None => errors.push(
Diagnostic::new(
&file,
"UnsupportedDependency",
"workspace dependencies must use `{ path = \"...\" }` local path records only",
)
.with_span(line.span),
),
}
}
"" | "project" => errors.push(
Diagnostic::new(
&file,
@ -1672,13 +1756,15 @@ fn set_manifest_key(
value: String,
span: Span,
key: &str,
code: &'static str,
manifest_kind: &str,
) {
if slot.replace(value).is_some() {
errors.push(
Diagnostic::new(
file,
"ProjectManifestInvalid",
format!("duplicate project manifest key `{}`", key),
code,
format!("duplicate {} manifest key `{}`", manifest_kind, key),
)
.with_span(span),
);
@ -2304,6 +2390,7 @@ fn parse_module(path: &Path, file: String, source: String) -> Result<ModuleUnit,
errors.append(&mut errs);
Program {
module: name.clone(),
type_aliases: Vec::new(),
enums: Vec::new(),
structs: Vec::new(),
c_imports: Vec::new(),
@ -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);
errors.append(&mut duplicate_errors);
@ -2327,6 +2414,7 @@ fn parse_module(path: &Path, file: String, source: String) -> Result<ModuleUnit,
local_functions,
local_structs,
local_enums,
local_aliases,
})
} else {
Err(errors)
@ -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(
file: &str,
program: &Program,
@ -2628,14 +2754,44 @@ fn local_declarations(
HashMap<String, DeclInfo>,
HashMap<String, DeclInfo>,
HashMap<String, DeclInfo>,
HashMap<String, DeclInfo>,
Vec<Diagnostic>,
) {
let mut all = HashMap::<String, DeclInfo>::new();
let mut functions = HashMap::new();
let mut structs = HashMap::new();
let mut enums = HashMap::new();
let mut aliases = HashMap::new();
let mut errors = Vec::new();
for alias in &program.type_aliases {
let decl = DeclInfo {
span: alias.name_span,
kind: DeclKind::TypeAlias,
};
if let Some(original) = all.insert(alias.name.clone(), decl.clone()) {
if original.kind == DeclKind::CImport {
errors.push(
Diagnostic::new(
file,
"DuplicateTopLevelName",
format!("duplicate top-level name `{}`", alias.name),
)
.with_span(alias.name_span)
.related("original declaration", original.span),
);
} else {
errors.push(duplicate_name(
file,
&alias.name,
alias.name_span,
original.span,
));
}
}
aliases.insert(alias.name.clone(), decl);
}
for function in &program.functions {
let decl = DeclInfo {
span: function.span,
@ -2767,7 +2923,7 @@ fn local_declarations(
enums.insert(enum_decl.name.clone(), decl);
}
(functions, structs, enums, errors)
(functions, structs, enums, aliases, errors)
}
fn validate_workspace_packages(
@ -3028,9 +3184,20 @@ fn resolve_and_check_workspace(
let mut external_enums = Vec::new();
for (name, binding) in imports {
let provider = &packages[binding.provider_package].modules[binding.provider_module];
let checked_provider =
checked_by_module.get(&(binding.provider_package, provider.name.clone()));
match binding.kind {
DeclKind::Function => {
if let Some(function) =
if let Some(function) = checked_provider
.and_then(|program| program.functions.iter().find(|f| f.name == *name))
{
external_functions.push(ExternalFunction {
name: name.clone(),
params: function.params.iter().map(|(_, ty)| ty.clone()).collect(),
return_type: function.return_type.clone(),
foreign: false,
});
} else if let Some(function) =
provider.program.functions.iter().find(|f| f.name == *name)
{
external_functions.push(ExternalFunction {
@ -3042,7 +3209,16 @@ fn resolve_and_check_workspace(
}
}
DeclKind::CImport => {
if let Some(import) =
if let Some(import) = checked_provider
.and_then(|program| program.c_imports.iter().find(|f| f.name == *name))
{
external_functions.push(ExternalFunction {
name: name.clone(),
params: import.params.iter().map(|(_, ty)| ty.clone()).collect(),
return_type: import.return_type.clone(),
foreign: true,
});
} else if let Some(import) =
provider.program.c_imports.iter().find(|f| f.name == *name)
{
external_functions.push(ExternalFunction {
@ -3054,7 +3230,15 @@ fn resolve_and_check_workspace(
}
}
DeclKind::Struct => {
if let Some(struct_decl) =
if let Some(struct_decl) = checked_provider
.and_then(|program| program.structs.iter().find(|s| s.name == *name))
{
external_structs.push(ExternalStruct {
name: name.clone(),
fields: struct_decl.fields.clone(),
span: struct_decl.span,
});
} else if let Some(struct_decl) =
provider.program.structs.iter().find(|s| s.name == *name)
{
external_structs.push(ExternalStruct {
@ -3069,10 +3253,16 @@ fn resolve_and_check_workspace(
}
}
DeclKind::Enum => {
if let Some(enum_decl) = external_enum_from_module(provider, name) {
if let Some(enum_decl) = checked_provider
.and_then(|program| {
external_enum_from_checked_module(provider, program, name)
})
.or_else(|| external_enum_from_module(provider, name))
{
external_enums.push(enum_decl);
}
}
DeclKind::TypeAlias => {}
}
}
@ -3243,9 +3433,19 @@ fn resolve_and_check(
let mut external_enums = Vec::new();
for (name, binding) in imports {
let provider = &modules[by_name[&binding.provider]];
let checked_provider = checked_by_module.get(&binding.provider);
match binding.kind {
DeclKind::Function => {
if let Some(function) =
if let Some(function) = checked_provider
.and_then(|program| program.functions.iter().find(|f| f.name == *name))
{
external_functions.push(ExternalFunction {
name: name.clone(),
params: function.params.iter().map(|(_, ty)| ty.clone()).collect(),
return_type: function.return_type.clone(),
foreign: false,
});
} else if let Some(function) =
provider.program.functions.iter().find(|f| f.name == *name)
{
external_functions.push(ExternalFunction {
@ -3257,7 +3457,16 @@ fn resolve_and_check(
}
}
DeclKind::CImport => {
if let Some(import) =
if let Some(import) = checked_provider
.and_then(|program| program.c_imports.iter().find(|f| f.name == *name))
{
external_functions.push(ExternalFunction {
name: name.clone(),
params: import.params.iter().map(|(_, ty)| ty.clone()).collect(),
return_type: import.return_type.clone(),
foreign: true,
});
} else if let Some(import) =
provider.program.c_imports.iter().find(|f| f.name == *name)
{
external_functions.push(ExternalFunction {
@ -3269,7 +3478,15 @@ fn resolve_and_check(
}
}
DeclKind::Struct => {
if let Some(struct_decl) =
if let Some(struct_decl) = checked_provider
.and_then(|program| program.structs.iter().find(|s| s.name == *name))
{
external_structs.push(ExternalStruct {
name: name.clone(),
fields: struct_decl.fields.clone(),
span: struct_decl.span,
});
} else if let Some(struct_decl) =
provider.program.structs.iter().find(|s| s.name == *name)
{
external_structs.push(ExternalStruct {
@ -3284,10 +3501,16 @@ fn resolve_and_check(
}
}
DeclKind::Enum => {
if let Some(enum_decl) = external_enum_from_module(provider, name) {
if let Some(enum_decl) = checked_provider
.and_then(|program| {
external_enum_from_checked_module(provider, program, name)
})
.or_else(|| external_enum_from_module(provider, name))
{
external_enums.push(enum_decl);
}
}
DeclKind::TypeAlias => {}
}
}
@ -3381,6 +3604,22 @@ fn build_export_maps(
Some(kind) => {
exports.insert(export.name.clone(), kind);
}
None if module.local_aliases.contains_key(&export.name) => diagnostics.push(
Diagnostic::new(
&module.file,
"Visibility",
format!(
"type alias `{}` is module-local and cannot be exported",
export.name
),
)
.with_span(export.span)
.related(
"type alias declaration",
module.local_aliases[&export.name].span,
)
.hint("export functions, structs, or enums; imported signatures see concrete target types"),
),
None => diagnostics.push(
Diagnostic::new(
&module.file,
@ -3428,6 +3667,7 @@ fn resolve_imports(
.get(&name.name)
.or_else(|| module.local_structs.get(&name.name))
.or_else(|| module.local_enums.get(&name.name))
.or_else(|| module.local_aliases.get(&name.name))
{
diagnostics.push(duplicate_name(
&module.file,
@ -3470,7 +3710,8 @@ fn resolve_imports(
.local_functions
.get(&name.name)
.or_else(|| provider.local_structs.get(&name.name))
.or_else(|| provider.local_enums.get(&name.name));
.or_else(|| provider.local_enums.get(&name.name))
.or_else(|| provider.local_aliases.get(&name.name));
let exported = export_maps[provider_index].get(&name.name).copied();
match (provider_local, exported) {
(_, Some(kind)) => {
@ -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(
Diagnostic::new(
&module.file,
@ -3557,13 +3815,14 @@ fn resolve_workspace_imports(
&packages[provider_package_index].modules[provider_module_index];
for name in &import.names {
if let Some(local) = module
.local_functions
.get(&name.name)
.or_else(|| module.local_structs.get(&name.name))
.or_else(|| module.local_enums.get(&name.name))
{
diagnostics.push(duplicate_name(
&module.file,
.local_functions
.get(&name.name)
.or_else(|| module.local_structs.get(&name.name))
.or_else(|| module.local_enums.get(&name.name))
.or_else(|| module.local_aliases.get(&name.name))
{
diagnostics.push(duplicate_name(
&module.file,
&name.name,
name.span,
local.span,
@ -3608,7 +3867,8 @@ fn resolve_workspace_imports(
.local_functions
.get(&name.name)
.or_else(|| provider.local_structs.get(&name.name))
.or_else(|| provider.local_enums.get(&name.name));
.or_else(|| provider.local_enums.get(&name.name))
.or_else(|| provider.local_aliases.get(&name.name));
let exported = export_maps[provider_package_index]
[provider_module_index]
.get(&name.name)
@ -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(
Diagnostic::new(
&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 STRING_PARAM: &[RuntimeType] = &[RuntimeType::String];
const STRING_STRING_PARAMS: &[RuntimeType] = &[RuntimeType::String, RuntimeType::String];
const STRING_I32_PARAMS: &[RuntimeType] = &[RuntimeType::String, RuntimeType::I32];
const STRING_I32_I32_PARAMS: &[RuntimeType] =
&[RuntimeType::String, RuntimeType::I32, RuntimeType::I32];
const I32_STRING_PARAMS: &[RuntimeType] = &[RuntimeType::I32, RuntimeType::String];
const VEC_I32_PARAM: &[RuntimeType] = &[RuntimeType::VecI32];
const VEC_I32_I32_PARAMS: &[RuntimeType] = &[RuntimeType::VecI32, RuntimeType::I32];
@ -176,6 +179,34 @@ pub const FUNCTIONS: &[RuntimeFunction] = &[
return_type: RuntimeType::String,
promoted: true,
},
RuntimeFunction {
source_name: "std.string.byte_at_result",
runtime_symbol: "__glagol_string_byte_at_result",
params: STRING_I32_PARAMS,
return_type: RuntimeType::ResultI32I32,
promoted: true,
},
RuntimeFunction {
source_name: "std.string.slice_result",
runtime_symbol: "__glagol_string_slice_result",
params: STRING_I32_I32_PARAMS,
return_type: RuntimeType::ResultStringI32,
promoted: true,
},
RuntimeFunction {
source_name: "std.string.starts_with",
runtime_symbol: "__glagol_string_starts_with",
params: STRING_STRING_PARAMS,
return_type: RuntimeType::Bool,
promoted: true,
},
RuntimeFunction {
source_name: "std.string.ends_with",
runtime_symbol: "__glagol_string_ends_with",
params: STRING_STRING_PARAMS,
return_type: RuntimeType::Bool,
promoted: true,
},
RuntimeFunction {
source_name: "std.string.parse_i32_result",
runtime_symbol: "__glagol_string_parse_i32_result",
@ -218,6 +249,62 @@ pub const FUNCTIONS: &[RuntimeFunction] = &[
return_type: RuntimeType::ResultBoolI32,
promoted: true,
},
RuntimeFunction {
source_name: "std.json.quote_string",
runtime_symbol: "__glagol_json_quote_string",
params: STRING_PARAM,
return_type: RuntimeType::String,
promoted: true,
},
RuntimeFunction {
source_name: "std.json.parse_string_value_result",
runtime_symbol: "__glagol_json_parse_string_value_result",
params: STRING_PARAM,
return_type: RuntimeType::ResultStringI32,
promoted: true,
},
RuntimeFunction {
source_name: "std.json.parse_bool_value_result",
runtime_symbol: "__glagol_json_parse_bool_value_result",
params: STRING_PARAM,
return_type: RuntimeType::ResultBoolI32,
promoted: true,
},
RuntimeFunction {
source_name: "std.json.parse_i32_value_result",
runtime_symbol: "__glagol_json_parse_i32_value_result",
params: STRING_PARAM,
return_type: RuntimeType::ResultI32I32,
promoted: true,
},
RuntimeFunction {
source_name: "std.json.parse_u32_value_result",
runtime_symbol: "__glagol_json_parse_u32_value_result",
params: STRING_PARAM,
return_type: RuntimeType::ResultU32I32,
promoted: true,
},
RuntimeFunction {
source_name: "std.json.parse_i64_value_result",
runtime_symbol: "__glagol_json_parse_i64_value_result",
params: STRING_PARAM,
return_type: RuntimeType::ResultI64I32,
promoted: true,
},
RuntimeFunction {
source_name: "std.json.parse_u64_value_result",
runtime_symbol: "__glagol_json_parse_u64_value_result",
params: STRING_PARAM,
return_type: RuntimeType::ResultU64I32,
promoted: true,
},
RuntimeFunction {
source_name: "std.json.parse_f64_value_result",
runtime_symbol: "__glagol_json_parse_f64_value_result",
params: STRING_PARAM,
return_type: RuntimeType::ResultF64I32,
promoted: true,
},
RuntimeFunction {
source_name: "std.io.eprint",
runtime_symbol: "__glagol_io_eprint",
@ -740,6 +827,18 @@ const RESERVED_HELPER_SYMBOLS: &[&str] = &[
"__glagol_string_parse_u64_result",
"__glagol_string_parse_f64_result",
"__glagol_string_parse_bool_result",
"__glagol_string_byte_at_result",
"__glagol_string_slice_result",
"__glagol_string_starts_with",
"__glagol_string_ends_with",
"__glagol_json_quote_string",
"__glagol_json_parse_string_value_result",
"__glagol_json_parse_bool_value_result",
"__glagol_json_parse_i32_value_result",
"__glagol_json_parse_u32_value_result",
"__glagol_json_parse_i64_value_result",
"__glagol_json_parse_u64_value_result",
"__glagol_json_parse_f64_value_result",
"__glagol_num_u32_to_string",
"__glagol_num_u64_to_string",
"__glagol_num_f64_to_string",
@ -781,7 +880,7 @@ pub fn unsupported_standard_library_call(file: &str, span: Span, source_name: &s
format!("standard library call `{}` is not supported", source_name),
)
.with_span(span)
.expected("std.io.print_i32, std.io.print_u32, std.io.print_i64, std.io.print_u64, std.io.print_f64, std.io.print_string, std.io.print_bool, std.io.eprint, std.io.read_stdin_result, std.string.len, std.string.concat, std.string.parse_i32_result, std.string.parse_u32_result, std.string.parse_i64_result, std.string.parse_u64_result, std.string.parse_f64_result, std.string.parse_bool_result, std.process.argc, std.process.arg, std.process.arg_result, std.env.get, std.env.get_result, std.fs.read_text, std.fs.read_text_result, std.fs.write_text, std.fs.write_text_result, std.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)
.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) {
output.push_str(&format!(
" (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() {
output.push_str(", filter ");
write_test_name(filter, output);
} else {
output.push_str(", filter none");
}
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> {
let bytes = text.as_bytes();
if !is_ascii_decimal_f64(bytes) {
@ -739,6 +976,252 @@ fn format_f64_to_string(value: f64) -> String {
text
}
fn quote_json_string_value(value: &str) -> String {
let mut quoted = String::with_capacity(value.len() + 2);
quoted.push('"');
for byte in value.bytes() {
match byte {
b'"' => quoted.push_str("\\\""),
b'\\' => quoted.push_str("\\\\"),
b'\n' => quoted.push_str("\\n"),
b'\t' => quoted.push_str("\\t"),
b'\r' => quoted.push_str("\\r"),
0x08 => quoted.push_str("\\b"),
0x0c => quoted.push_str("\\f"),
0x00..=0x1f => quoted.push_str(&format!("\\u{byte:04X}")),
_ => quoted.push(byte as char),
}
}
quoted.push('"');
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(
file: &str,
expr: &TExpr,
@ -2108,6 +2591,68 @@ fn eval_expr(
};
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" {
let Some(arg) = args.first() else {
return Err(unsupported_test_expr(
file,
expr,
"malformed `std.json.quote_string` 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.quote_string` on non-string values",
));
};
return Ok(Value::String(quote_json_string_value(value)));
}
if runtime_symbol == "__glagol_string_parse_i32_result" {
let Some(arg) = args.first() else {
return Err(unsupported_test_expr(
@ -2216,6 +2761,132 @@ fn eval_expr(
};
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" {
let argc = i32::try_from(env::args().count()).map_err(|_| {
Diagnostic::new(

View File

@ -154,6 +154,7 @@ fn benchmark_roots() -> Vec<PathBuf> {
root.join("math-loop"),
root.join("branch-loop"),
root.join("parse-loop"),
root.join("json-quote-loop"),
root.join("array-index-loop"),
root.join("string-eq-loop"),
root.join("array-struct-field-loop"),

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",
},
DiagnosticCase {
name: "malformed-type-alias",
source: r#"
(module main)
(type Count)
"#,
snapshot: "../tests/malformed-type-alias.diag",
},
DiagnosticCase {
name: "duplicate-type-alias",
source: r#"
(module main)
(type Count i32)
(type Count i64)
"#,
snapshot: "../tests/duplicate-type-alias.diag",
},
DiagnosticCase {
name: "type-alias-name-conflict",
source: r#"
(module main)
(struct Count
(value i32))
(type Count i32)
"#,
snapshot: "../tests/type-alias-name-conflict.diag",
},
DiagnosticCase {
name: "type-alias-value-name-conflict",
source: r#"
(module main)
(import_c c_add ((value i32)) -> i32)
(type main i32)
(type c_add i32)
(fn main () -> i32
0)
"#,
snapshot: "../tests/type-alias-value-name-conflict.diag",
},
DiagnosticCase {
name: "unknown-type-alias-target",
source: r#"
(module main)
(type Count Missing)
"#,
snapshot: "../tests/unknown-type-alias-target.diag",
},
DiagnosticCase {
name: "unsupported-type-alias-target",
source: r#"
(module main)
(type BadUnit unit)
(type BadPtr (ptr i32))
(type BadSlice (slice i32))
(type BadVec (vec u32))
(type BadResult (result i32 string))
"#,
snapshot: "../tests/unsupported-type-alias-target.diag",
},
DiagnosticCase {
name: "self-type-alias",
source: r#"
(module main)
(type Count Count)
"#,
snapshot: "../tests/self-type-alias.diag",
},
DiagnosticCase {
name: "cyclic-type-alias",
source: r#"
(module main)
(type A B)
(type B A)
"#,
snapshot: "../tests/cyclic-type-alias.diag",
},
DiagnosticCase {
name: "unsupported-generic-function",
source: r#"
(module main)
(fn id (type_params T) ((value T)) -> T
value)
"#,
snapshot: "../tests/unsupported-generic-function.diag",
},
DiagnosticCase {
name: "unsupported-generic-type-alias",
source: r#"
(module main)
(type VecOf (type_params T) (vec T))
"#,
snapshot: "../tests/unsupported-generic-type-alias.diag",
},
DiagnosticCase {
name: "unsupported-generic-type-parameter",
source: r#"
(module main)
(fn main () -> i32
(let xs (vec T) (std.vec.i32.empty))
0)
"#,
snapshot: "../tests/unsupported-generic-type-parameter.diag",
},
DiagnosticCase {
name: "unsupported-map-type",
source: r#"
(module main)
(fn main () -> (map string i32)
0)
"#,
snapshot: "../tests/unsupported-map-type.diag",
},
DiagnosticCase {
name: "unsupported-set-type",
source: r#"
(module main)
(fn main () -> (set string)
0)
"#,
snapshot: "../tests/unsupported-set-type.diag",
},
DiagnosticCase {
name: "enum-empty",
source: r#"
@ -894,6 +1036,250 @@ const CASES: &[DiagnosticCase] = &[
"#,
snapshot: "../tests/std-string-concat-unsupported-string-container.diag",
},
DiagnosticCase {
name: "std-string-byte-at-result-arity",
source: r#"
(module main)
(fn main () -> (result i32 i32)
(std.string.byte_at_result "abc"))
"#,
snapshot: "../tests/std-string-byte-at-result-arity.diag",
},
DiagnosticCase {
name: "std-string-byte-at-result-type",
source: r#"
(module main)
(fn main () -> (result i32 i32)
(std.string.byte_at_result "abc" "0"))
"#,
snapshot: "../tests/std-string-byte-at-result-type.diag",
},
DiagnosticCase {
name: "std-string-byte-at-result-context",
source: r#"
(module main)
(fn main () -> i32
(std.string.byte_at_result "abc" 0))
"#,
snapshot: "../tests/std-string-byte-at-result-context.diag",
},
DiagnosticCase {
name: "std-string-byte-at-result-bool-context",
source: r#"
(module main)
(fn main () -> i32
(if (std.string.byte_at_result "abc" 0) 1 0))
"#,
snapshot: "../tests/std-string-byte-at-result-bool-context.diag",
},
DiagnosticCase {
name: "std-string-byte-at-result-name-shadow",
source: r#"
(module main)
(fn std.string.byte_at_result ((text string) (index i32)) -> (result i32 i32)
(err i32 i32 1))
(fn main () -> i32
0)
"#,
snapshot: "../tests/std-string-byte-at-result-name-shadow.diag",
},
DiagnosticCase {
name: "std-string-byte-at-result-helper-shadow",
source: r#"
(module main)
(fn __glagol_string_byte_at_result ((text string) (index i32)) -> (result i32 i32)
(err i32 i32 1))
(fn main () -> i32
0)
"#,
snapshot: "../tests/std-string-byte-at-result-helper-shadow.diag",
},
DiagnosticCase {
name: "std-string-slice-result-arity",
source: r#"
(module main)
(fn main () -> (result string i32)
(std.string.slice_result "abc" 0))
"#,
snapshot: "../tests/std-string-slice-result-arity.diag",
},
DiagnosticCase {
name: "std-string-slice-result-type",
source: r#"
(module main)
(fn main () -> (result string i32)
(std.string.slice_result "abc" 0 "1"))
"#,
snapshot: "../tests/std-string-slice-result-type.diag",
},
DiagnosticCase {
name: "std-string-slice-result-context",
source: r#"
(module main)
(fn main () -> string
(std.string.slice_result "abc" 0 1))
"#,
snapshot: "../tests/std-string-slice-result-context.diag",
},
DiagnosticCase {
name: "std-string-slice-result-bool-context",
source: r#"
(module main)
(fn main () -> i32
(if (std.string.slice_result "abc" 0 1) 1 0))
"#,
snapshot: "../tests/std-string-slice-result-bool-context.diag",
},
DiagnosticCase {
name: "std-string-slice-result-name-shadow",
source: r#"
(module main)
(fn std.string.slice_result ((text string) (start i32) (count i32)) -> (result string i32)
(err string i32 1))
(fn main () -> i32
0)
"#,
snapshot: "../tests/std-string-slice-result-name-shadow.diag",
},
DiagnosticCase {
name: "std-string-slice-result-helper-shadow",
source: r#"
(module main)
(fn __glagol_string_slice_result ((text string) (start i32) (count i32)) -> (result string i32)
(err string i32 1))
(fn main () -> i32
0)
"#,
snapshot: "../tests/std-string-slice-result-helper-shadow.diag",
},
DiagnosticCase {
name: "std-string-starts-with-arity",
source: r#"
(module main)
(fn main () -> bool
(std.string.starts_with "abc"))
"#,
snapshot: "../tests/std-string-starts-with-arity.diag",
},
DiagnosticCase {
name: "std-string-starts-with-type",
source: r#"
(module main)
(fn main () -> bool
(std.string.starts_with "abc" 1))
"#,
snapshot: "../tests/std-string-starts-with-type.diag",
},
DiagnosticCase {
name: "std-string-starts-with-context",
source: r#"
(module main)
(fn main () -> i32
(std.string.starts_with "abc" "a"))
"#,
snapshot: "../tests/std-string-starts-with-context.diag",
},
DiagnosticCase {
name: "std-string-starts-with-name-shadow",
source: r#"
(module main)
(fn std.string.starts_with ((text string) (prefix string)) -> bool
false)
(fn main () -> i32
0)
"#,
snapshot: "../tests/std-string-starts-with-name-shadow.diag",
},
DiagnosticCase {
name: "std-string-starts-with-helper-shadow",
source: r#"
(module main)
(fn __glagol_string_starts_with ((text string) (prefix string)) -> bool
false)
(fn main () -> i32
0)
"#,
snapshot: "../tests/std-string-starts-with-helper-shadow.diag",
},
DiagnosticCase {
name: "std-string-ends-with-arity",
source: r#"
(module main)
(fn main () -> bool
(std.string.ends_with "abc"))
"#,
snapshot: "../tests/std-string-ends-with-arity.diag",
},
DiagnosticCase {
name: "std-string-ends-with-type",
source: r#"
(module main)
(fn main () -> bool
(std.string.ends_with "abc" 1))
"#,
snapshot: "../tests/std-string-ends-with-type.diag",
},
DiagnosticCase {
name: "std-string-ends-with-context",
source: r#"
(module main)
(fn main () -> i32
(std.string.ends_with "abc" "c"))
"#,
snapshot: "../tests/std-string-ends-with-context.diag",
},
DiagnosticCase {
name: "std-string-ends-with-name-shadow",
source: r#"
(module main)
(fn std.string.ends_with ((text string) (suffix string)) -> bool
false)
(fn main () -> i32
0)
"#,
snapshot: "../tests/std-string-ends-with-name-shadow.diag",
},
DiagnosticCase {
name: "std-string-ends-with-helper-shadow",
source: r#"
(module main)
(fn __glagol_string_ends_with ((text string) (suffix string)) -> bool
false)
(fn main () -> i32
0)
"#,
snapshot: "../tests/std-string-ends-with-helper-shadow.diag",
},
DiagnosticCase {
name: "std-string-parse-i32-result-arity",
source: r#"
@ -1388,6 +1774,16 @@ const CASES: &[DiagnosticCase] = &[
"#,
snapshot: "../tests/std-string-index-unsupported.diag",
},
DiagnosticCase {
name: "std-string-byte-at-unsupported",
source: r#"
(module main)
(fn main () -> i32
(std.string.byte_at "42" 0))
"#,
snapshot: "../tests/std-string-byte-at-unsupported.diag",
},
DiagnosticCase {
name: "std-string-slice-unsupported",
source: r#"
@ -1399,6 +1795,39 @@ const CASES: &[DiagnosticCase] = &[
"#,
snapshot: "../tests/std-string-slice-unsupported.diag",
},
DiagnosticCase {
name: "std-string-contains-unsupported",
source: r#"
(module main)
(fn main () -> i32
(std.string.contains "slovo" "lo")
0)
"#,
snapshot: "../tests/std-string-contains-unsupported.diag",
},
DiagnosticCase {
name: "std-string-find-result-unsupported",
source: r#"
(module main)
(fn main () -> i32
(std.string.find_result "slovo" "lo")
0)
"#,
snapshot: "../tests/std-string-find-result-unsupported.diag",
},
DiagnosticCase {
name: "std-string-split-unsupported",
source: r#"
(module main)
(fn main () -> i32
(std.string.split "a,b" ",")
0)
"#,
snapshot: "../tests/std-string-split-unsupported.diag",
},
DiagnosticCase {
name: "std-string-tokenize-unsupported",
source: r#"
@ -2051,11 +2480,22 @@ const CASES: &[DiagnosticCase] = &[
(module main)
(fn main () -> i32
(std.result.map (ok string i32 "a"))
(std.result.map (ok string i32 "a") mapper)
0)
"#,
snapshot: "../tests/std-result-map-unsupported.diag",
},
DiagnosticCase {
name: "std-vec-empty-generic-unsupported",
source: r#"
(module main)
(fn main () -> i32
(std.vec.empty i32)
0)
"#,
snapshot: "../tests/std-vec-empty-generic-unsupported.diag",
},
DiagnosticCase {
name: "std-package-load-unsupported",
source: r#"

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]
fn formatter_preserves_full_line_comments_inside_function_bodies() {
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",
checked_snapshot: "../tests/top-level-test.checked.lower",
},
LoweringFixture {
name: "type-aliases",
source: "../tests/type-aliases.slo",
surface_snapshot: "../tests/type-aliases.surface.lower",
checked_snapshot: "../tests/type-aliases.checked.lower",
},
LoweringFixture {
name: "comments",
source: "../tests/comments.slo",

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]
fn project_diagnostic_families_have_json_golden_coverage() {
let duplicate = write_project(

View File

@ -13,11 +13,13 @@ const DIAGNOSTIC_SNAPSHOTS: &[&str] = &[
"array-return-type-mismatch.diag",
"arity-mismatch.diag",
"cyclic-struct-fields.diag",
"cyclic-type-alias.diag",
"duplicate-local.diag",
"duplicate-match-arm.diag",
"duplicate-struct-constructor-field.diag",
"duplicate-struct-field.diag",
"duplicate-struct.diag",
"duplicate-type-alias.diag",
"empty-array.diag",
"enum-constructor-arity.diag",
"enum-container-values.diag",
@ -65,6 +67,7 @@ const DIAGNOSTIC_SNAPSHOTS: &[&str] = &[
"malformed-result-err-unwrap.diag",
"malformed-result-ok-unwrap.diag",
"malformed-result-constructor.diag",
"malformed-type-alias.diag",
"malformed-unsafe-form.diag",
"malformed-while.diag",
"field-access-on-non-struct.diag",
@ -82,6 +85,7 @@ const DIAGNOSTIC_SNAPSHOTS: &[&str] = &[
"set-immutable-local.diag",
"set-parameter.diag",
"single-file-main-i64-return.diag",
"self-type-alias.diag",
"set-type-mismatch.diag",
"set-unknown-local.diag",
"std-abi-layout-unsupported.diag",
@ -162,11 +166,26 @@ const DIAGNOSTIC_SNAPSHOTS: &[&str] = &[
"std-random-string-unsupported.diag",
"std-random-uuid-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-helper-shadow.diag",
"std-string-concat-result-context.diag",
"std-string-concat-type.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-index-unsupported.diag",
"std-string-len-type.diag",
@ -215,7 +234,19 @@ const DIAGNOSTIC_SNAPSHOTS: &[&str] = &[
"std-string-parse-i32-whitespace-unsupported.diag",
"std-string-parse-string-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-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-terminal-clear-unsupported.diag",
"std-terminal-echo-unsupported.diag",
@ -280,7 +311,11 @@ const DIAGNOSTIC_SNAPSHOTS: &[&str] = &[
"test-invalid-name.diag",
"test-non-bool.diag",
"type-mismatch.diag",
"type-alias-name-conflict.diag",
"type-alias-value-name-conflict.diag",
"unclosed-list.diag",
"unknown-type-alias-target.diag",
"unsupported-type-alias-target.diag",
"unknown-function.diag",
"unknown-struct-local.diag",
"unknown-struct-parameter.diag",
@ -316,8 +351,13 @@ const DIAGNOSTIC_SNAPSHOTS: &[&str] = &[
"unsupported-array-print.diag",
"unsupported-array-signature-element-type.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-map-type.diag",
"unsupported-signature-type.diag",
"unsupported-set-type.diag",
"unsupported-result-parameter-payload-type.diag",
"unsupported-result-return-payload-type.diag",
"unsupported-unit-parameter-signature.diag",
@ -465,6 +505,8 @@ const LOWERING_INSPECTOR_FIXTURES: &[&str] = &[
"time-sleep.surface.lower",
"top-level-test.checked.lower",
"top-level-test.surface.lower",
"type-aliases.checked.lower",
"type-aliases.surface.lower",
"u32-numeric-primitive.checked.lower",
"u32-numeric-primitive.surface.lower",
"u64-numeric-primitive.checked.lower",
@ -490,6 +532,11 @@ const LOWERING_INSPECTOR_CASES: &[LoweringInspectorCase] = &[
surface_snapshot: "top-level-test.surface.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 {
source: "tests/comments.slo",
surface_snapshot: "comments.surface.lower",
@ -853,6 +900,7 @@ const GLAGOL_TEST_FIXTURES: &[&str] = &[
"time-sleep.slo",
"top-level-test.fmt",
"top-level-test.slo",
"type-aliases.slo",
"u32-numeric-primitive.slo",
"u64-numeric-primitive.slo",
"unsafe.slo",
@ -920,6 +968,7 @@ const SLOVO_FORMATTER_FIXTURES: &[&str] = &[
"struct.slo",
"time-sleep.slo",
"top-level-test.slo",
"type-aliases.slo",
"u32-numeric-primitive.slo",
"u64-numeric-primitive.slo",
"unsafe.slo",
@ -1163,6 +1212,95 @@ const STANDARD_NET_RUNTIME_NAMES: &[&str] = &[
"std.net.tcp_close_result",
];
const STANDARD_JSON_SOURCE_FACADE_ALPHA: &[&str] = &[
"quote_string",
"null_value",
"bool_value",
"i32_value",
"u32_value",
"i64_value",
"u64_value",
"f64_value",
"parse_string_value_result",
"parse_bool_value_result",
"parse_i32_value_result",
"parse_u32_value_result",
"parse_i64_value_result",
"parse_u64_value_result",
"parse_f64_value_result",
"parse_null_value_result",
"parse_string_document_result",
"parse_bool_document_result",
"parse_i32_document_result",
"parse_u32_document_result",
"parse_i64_document_result",
"parse_u64_document_result",
"parse_f64_document_result",
"parse_null_document_result",
"field_string",
"field_bool",
"field_i32",
"field_u32",
"field_i64",
"field_u64",
"field_f64",
"field_null",
"array0",
"array1",
"array2",
"array3",
"object0",
"object1",
"object2",
"object3",
];
const STANDARD_JSON_RUNTIME_NAMES: &[&str] = &[
"std.json.quote_string",
"std.string.concat",
"std.num.i32_to_string",
"std.num.u32_to_string",
"std.num.i64_to_string",
"std.num.u64_to_string",
"std.num.f64_to_string",
"std.json.parse_string_value_result",
"std.json.parse_bool_value_result",
"std.json.parse_i32_value_result",
"std.json.parse_u32_value_result",
"std.json.parse_i64_value_result",
"std.json.parse_u64_value_result",
"std.json.parse_f64_value_result",
];
const STANDARD_JSON_ALLOWED_STD_NAMES: &[&str] = &[
"(import std.string (trim_ascii))",
"std.json.quote_string",
"std.string.concat",
"std.num.i32_to_string",
"std.num.u32_to_string",
"std.num.i64_to_string",
"std.num.u64_to_string",
"std.num.f64_to_string",
"std.json.parse_string_value_result",
"std.json.parse_bool_value_result",
"std.json.parse_i32_value_result",
"std.json.parse_u32_value_result",
"std.json.parse_i64_value_result",
"std.json.parse_u64_value_result",
"std.json.parse_f64_value_result",
];
const STANDARD_JSON_DOCUMENT_SCALAR_BETA21: &[&str] = &[
"parse_string_document_result",
"parse_bool_document_result",
"parse_i32_document_result",
"parse_u32_document_result",
"parse_i64_document_result",
"parse_u64_document_result",
"parse_f64_document_result",
"parse_null_document_result",
];
const STANDARD_PROCESS_SOURCE_FACADE_ALPHA: &[&str] = &[
"argc",
"arg",
@ -1200,6 +1338,16 @@ const STANDARD_PROCESS_SOURCE_FACADE_ALPHA: &[&str] = &[
const STANDARD_STRING_SOURCE_FACADE_ALPHA: &[&str] = &[
"len",
"concat",
"byte_at_result",
"slice_result",
"starts_with",
"ends_with",
"contains",
"index_of_option",
"last_index_of_option",
"trim_ascii_start",
"trim_ascii_end",
"trim_ascii",
"parse_i32_result",
"parse_i32_option",
"parse_u32_result",
@ -1408,9 +1556,14 @@ const STANDARD_VEC_I64_SOURCE_FACADE_ALPHA: &[&str] = &[
"index_of_option",
"last_index_of_option",
"contains",
"count_of",
"sum",
"concat",
"take",
"starts_with",
"without_prefix",
"ends_with",
"without_suffix",
"drop",
"reverse",
"subvec",
@ -1442,9 +1595,14 @@ const STANDARD_VEC_F64_SOURCE_FACADE_ALPHA: &[&str] = &[
"index_of_option",
"last_index_of_option",
"contains",
"count_of",
"sum",
"concat",
"take",
"starts_with",
"without_prefix",
"ends_with",
"without_suffix",
"drop",
"reverse",
"subvec",
@ -1738,6 +1896,7 @@ fn promotion_gate_artifacts_are_aligned() {
let glagol_project_std_layout_local_env = repo.join("examples/projects/std-layout-local-env");
let glagol_project_std_layout_local_fs = repo.join("examples/projects/std-layout-local-fs");
let glagol_project_std_layout_local_net = repo.join("examples/projects/std-layout-local-net");
let glagol_project_std_layout_local_json = repo.join("examples/projects/std-layout-local-json");
let glagol_project_std_layout_local_io = repo.join("examples/projects/std-layout-local-io");
let glagol_project_std_layout_local_cli = repo.join("examples/projects/std-layout-local-cli");
let glagol_project_std_layout_local_vec_i32 =
@ -1758,6 +1917,7 @@ fn promotion_gate_artifacts_are_aligned() {
let glagol_project_std_import_env = repo.join("examples/projects/std-import-env");
let glagol_project_std_import_fs = repo.join("examples/projects/std-import-fs");
let glagol_project_std_import_net = repo.join("examples/projects/std-import-net");
let glagol_project_std_import_json = repo.join("examples/projects/std-import-json");
let glagol_project_std_import_process = repo.join("examples/projects/std-import-process");
let glagol_project_std_import_string = repo.join("examples/projects/std-import-string");
let glagol_project_std_import_num = repo.join("examples/projects/std-import-num");
@ -1772,6 +1932,7 @@ fn promotion_gate_artifacts_are_aligned() {
let glagol_benchmark_math_loop = repo.join("benchmarks/math-loop");
let glagol_benchmark_branch_loop = repo.join("benchmarks/branch-loop");
let glagol_benchmark_parse_loop = repo.join("benchmarks/parse-loop");
let glagol_benchmark_json_quote_loop = repo.join("benchmarks/json-quote-loop");
let glagol_benchmark_array_index_loop = repo.join("benchmarks/array-index-loop");
let glagol_benchmark_string_eq_loop = repo.join("benchmarks/string-eq-loop");
let glagol_benchmark_array_struct_field_loop = repo.join("benchmarks/array-struct-field-loop");
@ -2110,6 +2271,7 @@ fn promotion_gate_artifacts_are_aligned() {
assert_project_std_import_env_tooling_matches_fixture(&glagol_project_std_import_env);
assert_project_std_import_fs_tooling_matches_fixture(&glagol_project_std_import_fs);
assert_project_std_import_net_tooling_matches_fixture(&glagol_project_std_import_net);
assert_project_std_import_json_tooling_matches_fixture(&glagol_project_std_import_json);
assert_project_std_import_process_tooling_matches_fixture(&glagol_project_std_import_process);
assert_project_std_import_string_tooling_matches_fixture(&glagol_project_std_import_string);
assert_project_std_import_num_tooling_matches_fixture(&glagol_project_std_import_num);
@ -2151,6 +2313,9 @@ fn promotion_gate_artifacts_are_aligned() {
assert_project_std_layout_local_net_tooling_matches_fixture(
&glagol_project_std_layout_local_net,
);
assert_project_std_layout_local_json_tooling_matches_fixture(
&glagol_project_std_layout_local_json,
);
assert_project_std_layout_local_io_tooling_matches_fixture(&glagol_project_std_layout_local_io);
assert_project_std_layout_local_cli_tooling_matches_fixture(
&glagol_project_std_layout_local_cli,
@ -2181,6 +2346,11 @@ fn promotion_gate_artifacts_are_aligned() {
"parse-loop",
"parse_loop",
);
assert_named_benchmark_scaffold_is_promotable(
&glagol_benchmark_json_quote_loop,
"json-quote-loop",
"json_quote_loop",
);
assert_named_benchmark_scaffold_is_promotable(
&glagol_benchmark_array_index_loop,
"array-index-loop",
@ -3337,6 +3507,7 @@ fn assert_slovo_std_source_layout_alpha(repo: &Path, std_dir: &Path) {
std_dir.join("env.slo"),
std_dir.join("fs.slo"),
std_dir.join("io.slo"),
std_dir.join("json.slo"),
std_dir.join("math.slo"),
std_dir.join("net.slo"),
std_dir.join("num.slo"),
@ -3368,6 +3539,7 @@ fn assert_slovo_std_source_layout_alpha(repo: &Path, std_dir: &Path) {
"fs.slo",
"math.slo",
"net.slo",
"json.slo",
"num.slo",
"option.slo",
"process.slo",
@ -3385,6 +3557,7 @@ fn assert_slovo_std_source_layout_alpha(repo: &Path, std_dir: &Path) {
&& file != "env.slo"
&& file != "fs.slo"
&& file != "io.slo"
&& file != "json.slo"
&& file != "process.slo"
&& file != "string.slo"
&& file != "vec_i32.slo"
@ -3532,6 +3705,10 @@ fn assert_slovo_std_source_layout_alpha(repo: &Path, std_dir: &Path) {
"std.string.parse_i64_result",
"std.string.parse_u32_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.len",
],
@ -4274,6 +4451,47 @@ fn assert_slovo_std_source_layout_alpha(repo: &Path, std_dir: &Path) {
helper
);
}
let local_json = repo.join("examples/projects/std-layout-local-json/src/json.slo");
let slovo_json = read(&std_dir.join("json.slo"));
let glagol_json = read(&local_json);
assert!(
slovo_json.contains("(module json") && glagol_json.contains("(module json"),
"both Slovo std/json.slo and the Glagol local fixture should use explicit `json` module source shape"
);
for runtime_name in STANDARD_JSON_RUNTIME_NAMES {
assert!(
slovo_json.contains(runtime_name) && glagol_json.contains(runtime_name),
"standard json facade must stay backed by `{}`",
runtime_name
);
}
assert_std_only_contains(
&slovo_json,
STANDARD_JSON_ALLOWED_STD_NAMES,
"Slovo std/json.slo must not introduce other compiler-known std names",
);
assert_std_only_contains(
&glagol_json,
STANDARD_JSON_ALLOWED_STD_NAMES,
"Glagol local json fixture must not introduce other compiler-known std names",
);
for source in [&slovo_json, &glagol_json] {
assert_deferred_json_surface_absent(source, "standard json facade");
assert_json_document_scalar_helpers_are_source_authored(source, "standard json facade");
}
for helper in STANDARD_JSON_SOURCE_FACADE_ALPHA {
assert!(
slovo_json.contains(&format!("(fn {} ", helper)),
"Slovo std/json.slo is missing beta7 facade `{}`",
helper
);
assert!(
glagol_json.contains(&format!("(fn {} ", helper)),
"Glagol local json fixture is missing beta7 facade `{}`",
helper
);
}
}
fn assert_source_shaped_file(path: &Path) {
@ -7107,6 +7325,29 @@ fn assert_project_std_import_net_tooling_matches_fixture(project: &Path) {
);
}
fn assert_project_std_import_json_tooling_matches_fixture(project: &Path) {
assert_project_std_import_host_facade_tooling_matches_fixture(
project,
"json",
STANDARD_JSON_SOURCE_FACADE_ALPHA,
concat!(
"test \"explicit std json quote escapes facade\" ... ok\n",
"test \"explicit std json scalar values facade\" ... ok\n",
"test \"explicit std json primitive scalar parse success facade\" ... ok\n",
"test \"explicit std json primitive scalar parse failure facade\" ... ok\n",
"test \"explicit std json string token parse success facade\" ... ok\n",
"test \"explicit std json string token parse failure facade\" ... ok\n",
"test \"explicit std json document parse trimmed success facade\" ... ok\n",
"test \"explicit std json document parse plain success facade\" ... ok\n",
"test \"explicit std json document parse trailing failure facade\" ... ok\n",
"test \"explicit std json fields facade\" ... ok\n",
"test \"explicit std json arrays objects facade\" ... ok\n",
"test \"explicit std json facade all\" ... ok\n",
"12 test(s) passed\n",
),
);
}
fn assert_project_std_import_process_tooling_matches_fixture(project: &Path) {
assert_project_std_import_host_facade_shape(
project,
@ -7155,13 +7396,18 @@ fn assert_project_std_import_string_tooling_matches_fixture(project: &Path) {
STANDARD_STRING_SOURCE_FACADE_ALPHA,
concat!(
"test \"explicit std string len concat\" ... ok\n",
"test \"explicit std string byte_at_result wrapper\" ... ok\n",
"test \"explicit std string slice_result wrapper\" ... ok\n",
"test \"explicit std string boundary wrappers\" ... ok\n",
"test \"explicit std string parse result wrappers\" ... ok\n",
"test \"explicit std string parse option wrappers\" ... ok\n",
"test \"explicit std string parse integer fallbacks\" ... ok\n",
"test \"explicit std string parse float bool fallbacks\" ... ok\n",
"test \"explicit std string parse custom fallbacks\" ... ok\n",
"test \"explicit std string search helpers\" ... ok\n",
"test \"explicit std string ascii trim helpers\" ... ok\n",
"test \"explicit std string helpers all\" ... ok\n",
"7 test(s) passed\n",
"12 test(s) passed\n",
),
);
}
@ -7443,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 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 subvec helper\" ... ok\n",
"test \"explicit std vec_i64 insert helper\" ... ok\n",
@ -7453,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 real program helpers\" ... 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",
);
@ -7537,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 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 ends_with helper\" ... ok\n",
"test \"explicit std vec_f64 without_suffix helper\" ... ok\n",
@ -7551,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 real program helpers\" ... 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",
);
@ -8626,13 +8878,18 @@ fn assert_project_std_layout_local_string_tooling_matches_fixture(project: &Path
test,
concat!(
"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 option wrappers\" ... 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 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",
"7 test(s) passed\n",
"12 test(s) passed\n",
),
"std layout local string project test",
);
@ -8663,17 +8920,21 @@ fn assert_standard_string_source_fallback_helpers_alpha(project: &Path) {
"std.string.parse_i64_result",
"std.string.parse_u32_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.len",
],
"standard string source helper fixture must use only existing std.string runtime names",
);
assert!(
!string.contains("trim")
&& !string.contains("locale")
!string.contains("locale")
&& !string.contains("unicode")
&& !string.contains("bytes")
&& !string.contains("case_insensitive")
&& !string.contains("regex")
&& !string.contains("host_error"),
"standard string source helper fixture must not claim deferred parsing or richer error APIs"
);
@ -9271,6 +9532,84 @@ fn assert_standard_net_source_facade_alpha(project: &Path) {
}
}
fn assert_project_std_layout_local_json_tooling_matches_fixture(project: &Path) {
assert!(project.join("slovo.toml").is_file());
assert!(project.join("src/json.slo").is_file());
assert!(project.join("src/main.slo").is_file());
assert_standard_json_source_facade_alpha(project);
let check = run_glagol([OsStr::new("check"), project.as_os_str()]);
assert_success_stdout(check, "", "std layout local json project check");
let test = run_glagol([OsStr::new("test"), project.as_os_str()]);
assert_success_stdout(
test,
concat!(
"test \"explicit local json quote escapes facade\" ... ok\n",
"test \"explicit local json scalar values facade\" ... ok\n",
"test \"explicit local json primitive scalar parse success facade\" ... ok\n",
"test \"explicit local json primitive scalar parse failure facade\" ... ok\n",
"test \"explicit local json string token parse success facade\" ... ok\n",
"test \"explicit local json string token parse failure facade\" ... ok\n",
"test \"explicit local json document parse trimmed success facade\" ... ok\n",
"test \"explicit local json document parse plain success facade\" ... ok\n",
"test \"explicit local json document parse trailing failure facade\" ... ok\n",
"test \"explicit local json fields facade\" ... ok\n",
"test \"explicit local json arrays objects facade\" ... ok\n",
"test \"explicit local json facade all\" ... ok\n",
"12 test(s) passed\n",
),
"std layout local json project test",
);
}
fn assert_standard_json_source_facade_alpha(project: &Path) {
let json_source = read(&project.join("src/json.slo"));
let main = read(&project.join("src/main.slo"));
assert!(
json_source.starts_with("(module json (export "),
"local json fixture must stay an explicitly exported local module"
);
assert!(
main.starts_with("(module main)\n\n(import json ("),
"local json fixture must stay an explicit local import"
);
assert!(
!main.contains("(import std") && !main.contains("(import slovo.std"),
"standard json source facade fixture must not use automatic std imports"
);
for runtime_name in STANDARD_JSON_RUNTIME_NAMES {
assert!(
json_source.contains(runtime_name),
"standard json source facade fixture must wrap or compose `{}`",
runtime_name
);
}
assert_std_only_contains(
&json_source,
STANDARD_JSON_ALLOWED_STD_NAMES,
"standard json source facade fixture must use only approved std runtime names directly",
);
assert!(
!main.contains("std."),
"standard json source facade fixture main must remain local"
);
assert_deferred_json_surface_absent(&json_source, "standard json source facade fixture");
for helper in STANDARD_JSON_SOURCE_FACADE_ALPHA {
assert!(
json_source.contains(&format!("(fn {} ", helper)),
"local json fixture is missing facade `{}`",
helper
);
assert!(
main.contains(helper),
"main fixture import/use is missing facade `{}`",
helper
);
}
}
fn assert_project_std_layout_local_io_tooling_matches_fixture(project: &Path) {
assert!(project.join("slovo.toml").is_file());
assert!(project.join("src/io.slo").is_file());
@ -9569,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 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 subvec helper\" ... ok\n",
"test \"explicit local vec_i64 insert helper\" ... ok\n",
@ -9579,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 real program helpers\" ... 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",
);
@ -9603,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 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 ends_with helper\" ... ok\n",
"test \"explicit local vec_f64 without_suffix helper\" ... ok\n",
@ -9617,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 real program helpers\" ... 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",
);
@ -10385,6 +10730,55 @@ fn assert_std_only_contains(source: &str, allowed: &[&str], context: &str) {
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 {
Path::new(env!("CARGO_MANIFEST_DIR"))
.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},
process::{Command, Output, Stdio},
sync::atomic::{AtomicUsize, Ordering},
time::{SystemTime, UNIX_EPOCH},
};
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"))
0)
"#,
"UnsupportedStandardLibraryCall",
"UnsupportedGenericStandardLibraryCall",
),
(
"promoted-name-shadow",
@ -951,11 +952,16 @@ fn write_fixture(name: &str, source: &str) -> PathBuf {
}
fn temp_root(name: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock before Unix epoch")
.as_nanos();
env::temp_dir().join(format!(
"glagol-exp10-host-result-{}-{}-{}",
"glagol-exp10-host-result-{}-{}-{}-{}",
name,
std::process::id(),
NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed)
NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed),
nanos
))
}

View File

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

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!(
"test \"explicit std string len concat\" ... ok\n",
"test \"explicit std string byte_at_result wrapper\" ... ok\n",
"test \"explicit std string slice_result wrapper\" ... ok\n",
"test \"explicit std string boundary wrappers\" ... ok\n",
"test \"explicit std string parse result wrappers\" ... ok\n",
"test \"explicit std string parse option wrappers\" ... ok\n",
"test \"explicit std string parse integer fallbacks\" ... ok\n",
"test \"explicit std string parse float bool fallbacks\" ... ok\n",
"test \"explicit std string parse custom fallbacks\" ... ok\n",
"test \"explicit std string search helpers\" ... ok\n",
"test \"explicit std string ascii trim helpers\" ... ok\n",
"test \"explicit std string helpers all\" ... ok\n",
"7 test(s) passed\n",
"12 test(s) passed\n",
);
const EXPECTED_STD_NUM_OUTPUT: &str = concat!(
@ -32,6 +37,16 @@ fn explicit_std_string_import_loads_repo_root_standard_source() {
&[
"len",
"concat",
"byte_at_result",
"slice_result",
"starts_with",
"ends_with",
"contains",
"index_of_option",
"last_index_of_option",
"trim_ascii_start",
"trim_ascii_end",
"trim_ascii",
"parse_i32_result",
"parse_i32_option",
"parse_u32_result",

View File

@ -0,0 +1,226 @@
use std::{
env,
ffi::OsStr,
fs,
path::{Path, PathBuf},
process::{Command, Output},
sync::atomic::{AtomicUsize, Ordering},
};
static NEXT_FIXTURE_ID: AtomicUsize = AtomicUsize::new(0);
#[test]
fn standard_json_lowers_to_private_runtime_helper() {
let fixture = write_fixture(
"lowering",
r#"
(module main)
(fn main () -> i32
(if (= (std.json.quote_string "slo\"vo") "\"slo\\\"vo\"")
0
1))
"#,
);
let output = run_glagol([fixture.as_os_str()]);
assert_success("compile standard json lowering", &output);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("declare ptr @__glagol_json_quote_string(ptr)")
&& stdout.contains("call ptr @__glagol_json_quote_string(")
&& !stdout.contains("@std.json.quote_string"),
"standard json LLVM shape drifted\nstdout:\n{}",
stdout
);
}
#[test]
fn test_runner_reports_deterministic_json_quoting() {
let fixture = write_fixture(
"test-runner",
r#"
(module main)
(test "quote plain string"
(= (std.json.quote_string "slovo") "\"slovo\""))
(test "quote embedded quote"
(= (std.json.quote_string "slo\"vo") "\"slo\\\"vo\""))
(test "quote backslash"
(= (std.json.quote_string "slo\\vo") "\"slo\\\\vo\""))
(test "quote newline tab"
(= (std.json.quote_string "line\n\tnext") "\"line\\n\\tnext\""))
"#,
);
let output = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
assert_success("run standard json tests", &output);
assert_eq!(
String::from_utf8_lossy(&output.stdout),
concat!(
"test \"quote plain string\" ... ok\n",
"test \"quote embedded quote\" ... ok\n",
"test \"quote backslash\" ... ok\n",
"test \"quote newline tab\" ... ok\n",
"4 test(s) passed\n",
),
"standard json test runner stdout drifted"
);
}
#[test]
fn standard_json_diagnostics_cover_promoted_and_deferred_names() {
let cases = [
(
"quote-arity",
r#"
(module main)
(fn main () -> i32
(std.json.quote_string))
"#,
"ArityMismatch",
),
(
"quote-type",
r#"
(module main)
(fn main () -> i32
(std.json.quote_string 42)
0)
"#,
"TypeMismatch",
),
(
"parse-object-deferred",
r#"
(module main)
(fn main () -> i32
(std.json.parse_object_result "{}"))
"#,
"UnsupportedStandardLibraryCall",
),
(
"promoted-shadow",
r#"
(module main)
(fn std.json.quote_string ((value string)) -> string
value)
(fn main () -> i32
(if (= (std.json.quote_string "x") "\"x\"") 0 1))
"#,
"DuplicateFunction",
),
];
for (name, source, diagnostic) in cases {
let fixture = write_fixture(name, source);
let output = run_glagol([fixture.as_os_str()]);
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!output.status.success(),
"compiler unexpectedly accepted `{}`\nstdout:\n{}\nstderr:\n{}",
name,
stdout,
stderr
);
assert!(
stdout.is_empty(),
"rejected compile wrote stdout:\n{}",
stdout
);
assert!(
stderr.contains(diagnostic),
"diagnostic `{}` was not reported for `{}`\nstderr:\n{}",
diagnostic,
name,
stderr
);
}
}
#[test]
fn hosted_json_quote_smoke_when_clang_is_available() {
if !clang_is_available() {
eprintln!("skipping standard json runtime smoke: set GLAGOL_CLANG or install clang");
return;
}
let fixture = write_fixture(
"hosted",
r#"
(module main)
(fn main () -> i32
(if (= (std.json.quote_string "line\nnext") "\"line\\nnext\"")
0
1))
"#,
);
let binary = fixture.with_extension(env::consts::EXE_EXTENSION);
let build = run_glagol([
OsStr::new("build"),
fixture.as_os_str(),
OsStr::new("-o"),
binary.as_os_str(),
]);
assert_success("build standard json hosted smoke", &build);
let run = Command::new(&binary)
.output()
.unwrap_or_else(|err| panic!("run `{}`: {}", binary.display(), err));
assert_success("run standard json hosted smoke", &run);
}
fn write_fixture(name: &str, source: &str) -> PathBuf {
let id = NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed);
let dir = env::temp_dir().join(format!("glagol-standard-json-{id}-{name}"));
fs::create_dir_all(&dir).unwrap_or_else(|err| panic!("create `{}`: {}", dir.display(), err));
let path = dir.join("main.slo");
fs::write(&path, source).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err));
path
}
fn clang_is_available() -> bool {
if env::var_os("GLAGOL_CLANG").is_some() {
return true;
}
Command::new("clang")
.arg("--version")
.output()
.is_ok_and(|output| output.status.success())
}
fn run_glagol<I, S>(args: I) -> Output
where
I: IntoIterator<Item = S>,
S: AsRef<std::ffi::OsStr>,
{
Command::new(env!("CARGO_BIN_EXE_glagol"))
.args(args)
.current_dir(Path::new(env!("CARGO_MANIFEST_DIR")))
.output()
.expect("run glagol")
}
fn assert_success(context: &str, output: &Output) {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"{} failed\nstdout:\n{}\nstderr:\n{}",
context,
stdout,
stderr
);
assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr);
}

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

@ -0,0 +1,350 @@
use std::{
ffi::OsStr,
fs,
path::Path,
process::{Command, Output},
};
const EXPECTED_LOCAL_TEST_OUTPUT: &str = concat!(
"test \"explicit local json quote escapes facade\" ... ok\n",
"test \"explicit local json scalar values facade\" ... ok\n",
"test \"explicit local json primitive scalar parse success facade\" ... ok\n",
"test \"explicit local json primitive scalar parse failure facade\" ... ok\n",
"test \"explicit local json string token parse success facade\" ... ok\n",
"test \"explicit local json string token parse failure facade\" ... ok\n",
"test \"explicit local json document parse trimmed success facade\" ... ok\n",
"test \"explicit local json document parse plain success facade\" ... ok\n",
"test \"explicit local json document parse trailing failure facade\" ... ok\n",
"test \"explicit local json fields facade\" ... ok\n",
"test \"explicit local json arrays objects facade\" ... ok\n",
"test \"explicit local json facade all\" ... ok\n",
"12 test(s) passed\n",
);
const EXPECTED_STD_IMPORT_TEST_OUTPUT: &str = concat!(
"test \"explicit std json quote escapes facade\" ... ok\n",
"test \"explicit std json scalar values facade\" ... ok\n",
"test \"explicit std json primitive scalar parse success facade\" ... ok\n",
"test \"explicit std json primitive scalar parse failure facade\" ... ok\n",
"test \"explicit std json string token parse success facade\" ... ok\n",
"test \"explicit std json string token parse failure facade\" ... ok\n",
"test \"explicit std json document parse trimmed success facade\" ... ok\n",
"test \"explicit std json document parse plain success facade\" ... ok\n",
"test \"explicit std json document parse trailing failure facade\" ... ok\n",
"test \"explicit std json fields facade\" ... ok\n",
"test \"explicit std json arrays objects facade\" ... ok\n",
"test \"explicit std json facade all\" ... ok\n",
"12 test(s) passed\n",
);
const STANDARD_JSON_SOURCE_FACADE_ALPHA: &[&str] = &[
"quote_string",
"null_value",
"bool_value",
"i32_value",
"u32_value",
"i64_value",
"u64_value",
"f64_value",
"parse_string_value_result",
"parse_bool_value_result",
"parse_i32_value_result",
"parse_u32_value_result",
"parse_i64_value_result",
"parse_u64_value_result",
"parse_f64_value_result",
"parse_null_value_result",
"parse_string_document_result",
"parse_bool_document_result",
"parse_i32_document_result",
"parse_u32_document_result",
"parse_i64_document_result",
"parse_u64_document_result",
"parse_f64_document_result",
"parse_null_document_result",
"field_string",
"field_bool",
"field_i32",
"field_u32",
"field_i64",
"field_u64",
"field_f64",
"field_null",
"array0",
"array1",
"array2",
"array3",
"object0",
"object1",
"object2",
"object3",
];
const STANDARD_JSON_RUNTIME_NAMES: &[&str] = &[
"std.json.quote_string",
"std.string.concat",
"std.num.i32_to_string",
"std.num.u32_to_string",
"std.num.i64_to_string",
"std.num.u64_to_string",
"std.num.f64_to_string",
"std.json.parse_string_value_result",
"std.json.parse_bool_value_result",
"std.json.parse_i32_value_result",
"std.json.parse_u32_value_result",
"std.json.parse_i64_value_result",
"std.json.parse_u64_value_result",
"std.json.parse_f64_value_result",
];
const STANDARD_JSON_ALLOWED_STD_NAMES: &[&str] = &[
"(import std.string (trim_ascii))",
"std.json.quote_string",
"std.string.concat",
"std.num.i32_to_string",
"std.num.u32_to_string",
"std.num.i64_to_string",
"std.num.u64_to_string",
"std.num.f64_to_string",
"std.json.parse_string_value_result",
"std.json.parse_bool_value_result",
"std.json.parse_i32_value_result",
"std.json.parse_u32_value_result",
"std.json.parse_i64_value_result",
"std.json.parse_u64_value_result",
"std.json.parse_f64_value_result",
];
const STANDARD_JSON_DOCUMENT_SCALAR_BETA21: &[&str] = &[
"parse_string_document_result",
"parse_bool_document_result",
"parse_i32_document_result",
"parse_u32_document_result",
"parse_i64_document_result",
"parse_u64_document_result",
"parse_f64_document_result",
"parse_null_document_result",
];
#[test]
fn standard_json_source_facade_project_checks_formats_and_tests() {
let project =
Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/projects/std-layout-local-json");
assert_local_json_fixture_is_source_authored(&project);
let fmt = run_glagol([
OsStr::new("fmt"),
OsStr::new("--check"),
project.as_os_str(),
]);
assert_success("std layout local json fmt --check", &fmt);
let check = run_glagol([OsStr::new("check"), project.as_os_str()]);
assert_success_stdout(check, "", "std layout local json check");
let test = run_glagol([OsStr::new("test"), project.as_os_str()]);
assert_success_stdout(
test,
EXPECTED_LOCAL_TEST_OUTPUT,
"std layout local json test output",
);
}
#[test]
fn standard_json_std_import_project_checks_formats_and_tests() {
let project =
Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/projects/std-import-json");
assert_std_import_json_fixture_uses_repo_std(&project);
let fmt = run_glagol([
OsStr::new("fmt"),
OsStr::new("--check"),
project.as_os_str(),
]);
assert_success("std import json fmt --check", &fmt);
let check = run_glagol([OsStr::new("check"), project.as_os_str()]);
assert_success_stdout(check, "", "std import json check");
let test = run_glagol([OsStr::new("test"), project.as_os_str()]);
assert_success_stdout(
test,
EXPECTED_STD_IMPORT_TEST_OUTPUT,
"std import json test output",
);
}
fn assert_local_json_fixture_is_source_authored(project: &Path) {
let json = read(&project.join("src/json.slo"));
let main = read(&project.join("src/main.slo"));
assert!(
json.starts_with("(module json (export "),
"json.slo must stay an explicit local module export"
);
assert!(
main.starts_with("(module main)\n\n(import json ("),
"main.slo must stay an explicit local json import"
);
assert!(
!main.contains("(import std") && !main.contains("(import slovo.std"),
"json fixture must not depend on automatic or package std imports"
);
assert_json_source_shape(&json, &main, "local json fixture");
assert!(
!main.contains("std."),
"local json main fixture must use only local imports"
);
}
fn assert_std_import_json_fixture_uses_repo_std(project: &Path) {
let std_json = read(&Path::new(env!("CARGO_MANIFEST_DIR")).join("../lib/std/json.slo"));
let main = read(&project.join("src/main.slo"));
assert!(
!project.join("src/json.slo").exists(),
"std import json fixture must use repo-root std/json.slo, not a local copy"
);
assert!(
main.starts_with("(module main)\n\n(import std.json ("),
"std import json fixture must use explicit `std.json` import syntax"
);
assert_json_source_shape(&std_json, &main, "repo std.json fixture");
}
fn assert_json_source_shape(json: &str, main: &str, context: &str) {
for runtime_name in STANDARD_JSON_RUNTIME_NAMES {
assert!(
json.contains(runtime_name),
"{} must wrap or compose `{}`",
context,
runtime_name
);
}
assert_std_only_contains(json, STANDARD_JSON_ALLOWED_STD_NAMES, context);
assert_deferred_json_surface_absent(json, main, context);
assert_json_document_scalar_helpers_are_source_authored(json, context);
for helper in STANDARD_JSON_SOURCE_FACADE_ALPHA {
assert!(
json.contains(&format!("(fn {} ", helper)),
"{} is missing source facade `{}`",
context,
helper
);
assert!(
main.contains(helper),
"{} main fixture import/use is missing `{}`",
context,
helper
);
}
}
fn assert_json_document_scalar_helpers_are_source_authored(json: &str, context: &str) {
for helper in STANDARD_JSON_DOCUMENT_SCALAR_BETA21 {
assert!(
!json.contains(&format!("std.json.{}", helper)),
"{} must keep `{}` source-authored, not compiler-known",
context,
helper
);
}
for private_prefix in [
"__glagol_json_parse_string_document",
"__glagol_json_parse_bool_document",
"__glagol_json_parse_i32_document",
"__glagol_json_parse_u32_document",
"__glagol_json_parse_i64_document",
"__glagol_json_parse_u64_document",
"__glagol_json_parse_f64_document",
"__glagol_json_parse_null_document",
] {
assert!(
!json.contains(private_prefix),
"{} must not introduce private JSON document runtime symbol `{}`",
context,
private_prefix
);
}
}
fn assert_deferred_json_surface_absent(json: &str, main: &str, context: &str) {
for deferred in [
"parse_object",
"parse_array",
"parse_value",
"tokenize",
"tokenizer",
"schema",
"stream",
"unicode",
"map",
] {
assert!(
!json.contains(deferred) && !main.contains(deferred),
"{} must not claim deferred JSON `{}` policies",
context,
deferred
);
}
}
fn assert_std_only_contains(source: &str, allowed: &[&str], context: &str) {
let mut remaining = source.to_string();
for name in allowed {
remaining = remaining.replace(name, "");
}
assert!(
!remaining.contains("std."),
"{} introduced unexpected compiler-known std names",
context
);
}
fn run_glagol<I, S>(args: I) -> Output
where
I: IntoIterator<Item = S>,
S: AsRef<std::ffi::OsStr>,
{
Command::new(env!("CARGO_BIN_EXE_glagol"))
.args(args)
.current_dir(Path::new(env!("CARGO_MANIFEST_DIR")))
.output()
.expect("run glagol")
}
fn read(path: &Path) -> String {
fs::read_to_string(path).unwrap_or_else(|err| panic!("read `{}`: {}", path.display(), err))
}
fn assert_success(context: &str, output: &Output) {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"{} failed\nstdout:\n{}\nstderr:\n{}",
context,
stdout,
stderr
);
assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr);
}
fn assert_success_stdout(output: Output, expected: &str, context: &str) {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"{} failed\nstdout:\n{}\nstderr:\n{}",
context,
stdout,
stderr
);
assert_eq!(stdout, expected, "{} stdout drifted", context);
assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr);
}

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!(
"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 option wrappers\" ... 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 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",
"7 test(s) passed\n",
"12 test(s) passed\n",
);
const STANDARD_STRING_SOURCE_FALLBACK_HELPERS_ALPHA: &[&str] = &[
"len",
"concat",
"byte_at_result",
"slice_result",
"starts_with",
"ends_with",
"contains",
"index_of_option",
"last_index_of_option",
"trim_ascii_start",
"trim_ascii_end",
"trim_ascii",
"parse_i32_result",
"parse_i32_option",
"parse_u32_result",
@ -100,6 +115,10 @@ fn assert_local_string_fixture_is_source_authored(project: &Path) {
"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",
] {
@ -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"
);
assert!(
!string.contains("trim")
&& !string.contains("locale")
!string.contains("locale")
&& !string.contains("unicode")
&& !string.contains("bytes")
&& !string.contains("case_insensitive")
&& !string.contains("regex")
&& !string.contains("host_error"),
"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 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 ends_with 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 real program helpers\" ... 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] = &[
@ -48,6 +49,7 @@ const STANDARD_VEC_F64_SOURCE_FACADE_ALPHA: &[&str] = &[
"index_of_option",
"last_index_of_option",
"contains",
"count_of",
"sum",
"concat",
"take",
@ -184,6 +186,7 @@ fn assert_local_vec_f64_fixture_is_source_authored(project: &Path) {
"index_of_option_loop",
"last_index_of_option_loop",
"contains_loop",
"count_of_loop",
"sum_loop",
"concat_loop",
"take_loop",
@ -196,6 +199,43 @@ fn assert_local_vec_f64_fixture_is_source_authored(project: &Path) {
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

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 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 ends_with 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 real program helpers\" ... 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] = &[
@ -48,6 +49,7 @@ const STANDARD_VEC_F64_SOURCE_FACADE_ALPHA: &[&str] = &[
"index_of_option",
"last_index_of_option",
"contains",
"count_of",
"sum",
"concat",
"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 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 subvec 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 real program helpers\" ... 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] = &[
@ -44,9 +49,14 @@ const STANDARD_VEC_I64_SOURCE_FACADE_ALPHA: &[&str] = &[
"index_of_option",
"last_index_of_option",
"contains",
"count_of",
"sum",
"concat",
"take",
"starts_with",
"without_prefix",
"ends_with",
"without_suffix",
"drop",
"reverse",
"subvec",
@ -176,6 +186,7 @@ fn assert_local_vec_i64_fixture_is_source_authored(project: &Path) {
"index_of_option_loop",
"last_index_of_option_loop",
"contains_loop",
"count_of_loop",
"sum_loop",
"concat_loop",
"take_loop",
@ -188,6 +199,43 @@ fn assert_local_vec_i64_fixture_is_source_authored(project: &Path) {
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

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 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 subvec 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 real program helpers\" ... 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] = &[
@ -44,9 +49,14 @@ const STANDARD_VEC_I64_SOURCE_FACADE_ALPHA: &[&str] = &[
"index_of_option",
"last_index_of_option",
"contains",
"count_of",
"sum",
"concat",
"take",
"starts_with",
"without_prefix",
"ends_with",
"without_suffix",
"drop",
"reverse",
"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)
);
}

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