-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathSTDLIB.bs
More file actions
167 lines (132 loc) · 8.41 KB
/
Copy pathSTDLIB.bs
File metadata and controls
167 lines (132 loc) · 8.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
?bs 0.8
?primer
// STDLIB.bs — every botscript feature, exactly once.
//
// Read this file end-to-end before writing botscript code in this repo. If a
// feature you want is not in this file, it does not exist yet — propose it
// before using it. Keep this file *small*: every form here is load-bearing.
import { ok, err, some, none, http, time, type Option, type Result } from "@mbfarias/botscript-runtime";
// ─── 1. Tagged union (0.2+) ────────────────────────────────────────────────
// `type Name = Tag { fields } | Tag2 { fields } | …` desugars to a TS
// discriminated union with a `kind` literal that matches what `match` already
// destructures on. Bare-tag alternatives (no `{ }`) are supported too.
export type Shape = Circle { r: number } | Square { side: number };
// ─── 2. fn with capabilities ────────────────────────────────────────────────
// `uses { ... }` declares the side-effect categories this function may touch.
// Capabilities: net, fs, time, random, process, stdout, stderr.
fn now() uses { time } -> number = time.now()
// ─── 2b. fn with reads / writes (0.8+) ──────────────────────────────────────
// `reads { ... }` and `writes { ... }` declare which user-defined resource
// categories a function reads from or writes to. Labels are identifiers
// (e.g. cache, db, metrics) — not stdlib names. Metadata-only in 0.8;
// transitivity is enforced at ?bs 0.9 (DEP001 / DEP002).
fn lookupCache(id: string) reads { cache } -> Option<string> = none
fn writeCache(id: string, value: string) writes { cache } -> void { }
// ─── 3. fn pure shorthand ───────────────────────────────────────────────────
// `= pure { expr }` is equivalent to `uses { } { return expr; }`.
fn slug(s: string) -> string = pure {
s.trim().toLowerCase().replaceAll(" ", "-")
}
// ─── 4. fn single-expression body ───────────────────────────────────────────
// Any single expression after `=` becomes the function body. Common with match.
fn area(s: Shape) -> number = match s {
Circle { r } -> Math.PI * r * r
Square { side } -> side * side
}
// ─── 4b. fn with type parameters (0.4+) ─────────────────────────────────────
// Generic fns work like TypeScript: `<T>` between the name and the args.
// `extends` constraints and defaults are supported. `match` and capabilities
// compose unchanged.
fn first<T>(xs: Array<T>) -> Option<T> = pure {
xs.length > 0 ? some(xs[0]!) : none
}
// ─── 5. ? unwrap ────────────────────────────────────────────────────────────
// On a Result-returning expression at end of a `let`/`const`/`return` line,
// `?` unwraps the Ok or short-circuits the enclosing fn with the Err.
async fn loadFirstLine(url: string) uses { net } -> Promise<Result<string, string>> {
let res = (await doFetch(url))?
return ok(res.split("\n")[0] ?? "");
}
// ─── 5b. Result.try { } and Result.tryAsync { } (0.3+) ──────────────────────
// Lift a JS-boundary call that may throw into a Result<T, string>. Use the
// async form whenever the body awaits or returns a Promise. Composes with `?`.
fn parseConfig(input: string) -> Result<unknown, string> {
let parsed = Result.try { JSON.parse(input) }?
return ok(parsed);
}
// ─── 6. Result vs Option ────────────────────────────────────────────────────
// Result has an error payload; Option does not. Use Option for "maybe", Result
// for "could fail".
fn firstAdmin(users: { role: string; name: string }[]) -> Option<string> = pure {
(() => {
const u = users.find((u) => u.role === "admin");
return u ? some(u.name) : none;
})()
}
// ─── 7. match patterns (the four supported) ─────────────────────────────────
fn label(x: unknown) -> string = match x {
"ping" -> "pong" // string literal
42 -> "answer" // number literal
true -> "yes" // boolean literal
_ -> "unknown" // wildcard (must be last)
}
// ─── 8. test "name" { } and assert ──────────────────────────────────────────
// `test` is a top-level form; it forwards to vitest's `test` when run there.
test "slug normalizes whitespace" {
assert slug(" Hello World ") === "hello-world";
}
test "area handles both shapes" {
assert area({ kind: "Circle", r: 1 }) === Math.PI;
assert area({ kind: "Square", side: 4 }) === 16;
}
// ─── 8b. test with mocks (0.2+) ─────────────────────────────────────────────
// `with mocks { time, random }` injects deterministic stubs for the named
// capabilities. Inside the body, time.now() returns 0, 1, 2, … and
// random.next() returns a deterministic counter. Sources are restored when
// the body returns or throws.
test "deterministic clock" with mocks { time } {
const a = time.now();
const b = time.now();
assert b === a + 1;
}
// ─── 9. effect annotations on callback parameters (0.7+) ────────────────────
// A function-typed parameter can carry `uses { caps }` to declare the
// side-effect surface the callback may exercise. The outer fn must declare
// at least those capabilities in its own `uses {}` clause (EFF002).
// Botscript's `->` arrow is used inside parameter types and converts to
// TypeScript's `=>` in emitted code.
fn withRetry(action: () uses { net } -> string) uses { net } -> string = action()
// ─── 10. unsafe "<reason>" { } (0.3+) ────────────────────────────────────────
// The escape hatch for `as` casts and other places the type system can't
// follow. Every unsafe block must carry a non-empty justification string —
// the cast and its reason live together in the diff, so the next reviewer
// (human or model) sees the *why* not just the *what*.
//
// From ?bs 0.5 the rule is enforced at parse time: a bare `as` cast outside
// an unsafe "<reason>" { ... } block is UNS004. The form below is the only
// way to write a cast in 0.5 and later.
fn castShape(raw: unknown) -> Shape = unsafe "trust caller-validated payload" {
raw as Shape
}
// ─── 10b. unsafe "reason" fn (declaration-level escape hatch) ────────────────
// When a function is *the* trust boundary for type coercions in a module,
// repeating the justification at every call site is noise. Declaration-level
// `unsafe fn` moves the reason to the declaration; callers treat it as a normal
// fn. The reason is preserved as a `/* unsafe: "…" */` comment in the emitted
// TypeScript. Works with async: `unsafe "reason" async fn`.
unsafe "raw API response shape validated by schema" fn parseShape(raw: unknown) -> Shape {
return raw as Shape;
}
// ─── helpers used above ─────────────────────────────────────────────────────
// (These exist only to make the examples compile in isolation.)
async fn doFetch(url: string) uses { net } -> Promise<Result<string, string>> {
// http.get returns Promise<Result<Response, Error>>. Project the Error
// payload from `Error` to `string` so this example fits its declared
// `Result<string, string>` error channel, then let Result.tryAsync lift
// any decoding throw from `.text()` into Err. The function is `async`,
// so its return type is wrapped in Promise<…> at the type level —
// botscript doesn't auto-promote, the annotation must spell it out.
let getRes = await http.get(url)
if (getRes.kind === "err") return err(getRes.error.message);
return Result.tryAsync { await getRes.value.text() }
}