Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## Summary

Describe the change in 2-5 bullets.

## Validation

List the commands, checks, or CI evidence that validate this PR.

## Notes

Add any context that helps reviewers focus on the important details.
72 changes: 72 additions & 0 deletions .github/workflows/ci-pr-checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: "CI - PR Checks"

on:
pull_request:
types: [opened, edited, synchronize, reopened]

permissions:
contents: read
pull-requests: read

concurrency:
group: pr-checks-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
title:
name: PR title
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Check PR title
uses: zrr1999/zendev-actions/validate-title@9013ad848b32d287d0258fdbc9f51e350b8eca33
with:
text: ${{ github.event.pull_request.title }}

body:
name: PR body
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Check PR body headings
env:
GITHUB_EVENT_PATH: ${{ github.event_path }}
run: |
python - <<'PY'
import json
import os
import re
import sys
from pathlib import Path

def extract_h2_headings(text: str) -> list[str]:
in_fence = False
headings = []
for line in text.splitlines():
stripped = line.strip()
if stripped.startswith("```"):
in_fence = not in_fence
continue
if in_fence:
continue
if re.match(r"^##\s+\S", stripped):
headings.append(re.sub(r"^##\s+", "", stripped))
return headings

payload = json.loads(Path(os.environ["GITHUB_EVENT_PATH"]).read_text(encoding="utf-8"))
body = (payload.get("pull_request") or {}).get("body") or ""
template = Path(".github/pull_request_template.md").read_text(encoding="utf-8")

expected = extract_h2_headings(template)
actual = extract_h2_headings(body)

if actual != expected:
print("PR body headings do not match the repository template.", file=sys.stderr)
print(f"Expected headings: {expected}", file=sys.stderr)
print(f"Actual headings: {actual}", file=sys.stderr)
raise SystemExit(1)

print("PR body headings match the repository template.")
PY
27 changes: 27 additions & 0 deletions .github/workflows/ci-static-checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: "CI - Static Checks"

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

concurrency:
group: ci-static-${{ github.ref }}
cancel-in-progress: true

jobs:
prek:
name: prek
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
components: clippy, rustfmt

- uses: j178/prek-action@v2
93 changes: 93 additions & 0 deletions .github/workflows/ci-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
name: "CI - Tests"

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

concurrency:
group: ci-tests-${{ github.ref }}
cancel-in-progress: true

jobs:
host:
name: host
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable

- uses: Swatinem/rust-cache@v2
with:
workspaces: host

- name: Build host crate
run: cargo build --manifest-path host/Cargo.toml

- name: Run host tests
run: cargo test --manifest-path host/Cargo.toml

spore:
name: spore
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- uses: actions/checkout@v6
with:
repository: spore-lang/spore
ref: ef627c785e355a70551af5e4f4d85230996ce8d9
path: _spore

- uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable

- uses: Swatinem/rust-cache@v2
with:
workspaces: _spore

- name: Build spore compiler
run: cargo build --release --manifest-path _spore/Cargo.toml --bin spore

- name: Format Spore files
run: |
paths=(examples platform)
if [ -d tests ]; then
paths+=(tests)
fi
find "${paths[@]}" -name '*.sp' -type f | sort | xargs _spore/target/release/spore format
git diff --exit-code -- "${paths[@]}"

- name: Check Spore files
run: |
paths=(examples platform)
if [ -d tests ]; then
paths+=(tests)
fi
find "${paths[@]}" -name '*.sp' -type f | sort | xargs -n 1 _spore/target/release/spore check

- name: Build Spore files
run: |
paths=(examples platform)
if [ -d tests ]; then
paths+=(tests)
fi
find "${paths[@]}" -name '*.sp' -type f | sort | xargs -n 1 _spore/target/release/spore build

- name: Run hello example
run: _spore/target/release/spore run examples/hello.sp

- name: Run Spore spec tests
run: |
if [ ! -d tests ]; then
echo "No Spore spec tests yet; skipping spore test."
exit 0
fi
find tests -name '*.sp' -type f | sort | xargs -r -n 1 _spore/target/release/spore test
26 changes: 0 additions & 26 deletions .github/workflows/ci.yml

This file was deleted.

43 changes: 29 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,58 +23,73 @@ Operating System
| `Stdout` | `uses [Console]` | Standard output |
| `Stdin` | `uses [Console]` | Standard input |
| `File` | `uses [FileRead]` or `uses [FileWrite]` | File system operations |
| `Dir` | `uses [FileRead]` | Directory listing |
| `Dir` | `uses [FileRead]` or `uses [FileWrite]` | Directory listing and creation |
| `Env` | `uses [Env]` | Environment variables |
| `Cmd` | `uses [Spawn]` | Process execution |

## Quick Start

```spore
// hello.sp
uses [Console]

fn main() -> Unit uses [Console] {
println("Hello from basic-cli!")
/// A simple "Hello World" using the basic-cli platform.
fn main() -> () uses [Console] {
println("Hello from Spore basic-cli!")
}
```

```bash
spore run hello.sp --platform basic-cli
spore check examples/hello.sp
spore build examples/hello.sp
spore run examples/hello.sp
```

This repository currently keeps `examples/` limited to files that PR CI validates today.
It also intentionally keeps the current loose `.sp` file layout for now; the manifest/package-layout migration is deferred to a follow-up change.

## Project Structure

```
basic-cli/
├── platform/ # Spore API modules (.sp files)
├── platform/ # Spore API modules (.sp files), checked and built in CI
│ ├── Stdout.sp # Standard output operations
│ ├── Stdin.sp # Standard input operations
│ ├── File.sp # File read/write operations
│ ├── Dir.sp # Directory operations
│ ├── Env.sp # Environment variable access
│ ├── Cmd.sp # Process execution
│ └── main.sp # Platform entry point
├── host/ # Rust host implementation
│ ├── Cargo.toml
│ └── src/
│ └── lib.rs # Foreign function implementations
├── examples/ # Example Spore programs using this platform
└── tests/ # Integration tests
└── examples/ # Canonical examples that format/check/build in CI
```

## Tutorial Contract

- `examples/` is for truthful, CI-validated examples only.
- `platform/` is the API surface for the platform modules themselves.
- Only add `tests/` when the repo has real Spore-side regression coverage worth running with `spore test`.

If you want to add a new tutorial/example, treat this as the bar:

1. keep it self-contained;
2. make sure it passes `spore format --check`, `spore check`, and `spore build`;
3. only then promote it into `examples/` and mention it in this README.

## Design Philosophy

Following Spore's [SEP-0005 (Effect System)](https://github.com/spore-lang/spore-evolution):
Following Spore's [SEP-0003 (Effect Capability System)](https://github.com/spore-lang/spore-evolution/blob/main/seps/SEP-0003-effect-capability-system.md):

- **Capability-gated**: Every I/O function declares its required capabilities via `uses [Cap]`
- **Cost-annotated**: Platform functions carry `cost ≤ N` budgets where meaningful
- **Error-typed**: Functions declare error sets via `! [ErrorType]`
- **Cost-annotated**: Platform functions can carry `cost [c, a, i, p]` budgets where meaningful
- **Error-typed**: Functions declare error sets via `! ErrorType`
- **Pure by default**: The platform boundary is the only place side effects occur

## Status

🚧 **Early development** — API is unstable and subject to change.

At the moment, the validated example is `examples/hello.sp`. More ambitious host-backed demos such as environment/file workflows should stay out of `examples/` until the current platform import/runtime architecture supports them honestly.

## License

MIT — see [LICENSE](LICENSE).
11 changes: 0 additions & 11 deletions examples/env-reader.sp

This file was deleted.

10 changes: 0 additions & 10 deletions examples/file-copy.sp

This file was deleted.

8 changes: 2 additions & 6 deletions examples/hello.sp
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
/// A simple "Hello World" using the basic-cli platform.
///
/// Run with: spore run examples/hello.sp --platform basic-cli
uses [Console]

fn main() -> Unit uses [Console] {
println("Hello from Spore basic-cli!")
}
/// Run with: spore run examples/hello.sp
fn main() -> () uses [Console] { println("Hello from Spore basic-cli!") }
8 changes: 2 additions & 6 deletions platform/Cmd.sp
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@

/// Run a command and capture its stdout as a string.
/// Returns the full stdout output on success.
foreign fn process_run(cmd: String, args: List[String]) -> String
uses [Spawn]
! [ExecError]
foreign fn process_run(cmd: String, args: List[String]) -> String ! ExecError uses [Spawn]

/// Run a command and return its exit code.
foreign fn process_run_status(cmd: String, args: List[String]) -> Int
uses [Spawn]
! [ExecError]
foreign fn process_run_status(cmd: String, args: List[String]) -> Int ! ExecError uses [Spawn]
8 changes: 2 additions & 6 deletions platform/Dir.sp
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
/// basic-cli platform — Directory operations

/// List entries in a directory, returning their names.
foreign fn dir_list(path: String) -> List[String]
uses [FileRead]
! [IoError]
foreign fn dir_list(path: String) -> List[String] ! IoError uses [FileRead]

/// Create a directory (and any missing parents).
foreign fn dir_mkdir(path: String) -> Unit
uses [FileWrite]
! [IoError]
foreign fn dir_mkdir(path: String) -> Unit ! IoError uses [FileWrite]
6 changes: 2 additions & 4 deletions platform/Env.sp
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
/// basic-cli platform — Environment variable access

/// Get the value of an environment variable, or None if not set.
foreign fn env_get(key: String) -> Option[String]
uses [Env]
foreign fn env_get(key: String) -> Option[String] uses [Env]

/// Set an environment variable.
foreign fn env_set(key: String, value: String) -> Unit
uses [Env]
foreign fn env_set(key: String, value: String) -> Unit uses [Env]
Loading
Loading