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
50 changes: 25 additions & 25 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -1521,53 +1521,53 @@ Automatic conversions at import boundary:
- `T | undefined` → `Option<T>`
- `T | null | undefined` → `Option<T>`
- External `any` → `unknown` (forces narrowing)
- npm imports are callable directly by default (like `useState`, `clsx`)
- `throws` modifier marks imports whose functions may throw — calls are auto-wrapped in `Result<T, Error>`
- npm imports are **untrusted by default** — calls are auto-wrapped in `Result<T, Error>`
- `trusted` modifier marks imports that are safe to call directly (no Result wrapping)
- Nullable/optional types at the boundary are converted to `Option<T>`

#### Default npm imports
#### Default npm imports (untrusted)

npm imports work directly — no special syntax needed for safe functions:
npm imports are untrusted by default — calls are auto-wrapped in `Result<T, Error>`:

```floe
import { useState } from "react"
import { clsx } from "clsx"
import { parseYaml } from "yaml-lib"
import { fetchUser } from "api-client"

const [count, setCount] = useState(0) // direct call
const classes = clsx("btn", active) // direct call
const data = parseYaml(input) // Result<unknown, Error> — auto-wrapped
const user = fetchUser(id) // Result<User, Error> — auto-wrapped
```

#### throws imports
#### trusted imports

For npm functions that may throw, mark them with `throws`. The compiler auto-wraps calls in `Result<T, Error>`:
For npm functions known to be safe, mark them with `trusted`. The compiler allows direct calls without Result wrapping:

```floe
// Whole module:
import trusted { useState, useEffect } from "react"
useState(0) // direct call, no wrapping

// Per-function:
import { capitalize, throws fetchUser } from "some-ts-lib"
import { trusted capitalize, fetchUser } from "some-ts-lib"
capitalize("hello") // string, direct call
fetchUser(id) // Result<User, Error> — auto-wrapped

// Whole module:
import throws { parseYaml, dumpYaml } from "yaml-lib"
parseYaml(input) // Result<unknown, Error> — auto-wrapped
fetchUser(id) // Result<User, Error> — auto-wrapped (untrusted)
```

#### throws with Result and ?
#### Untrusted imports with Result and ?

`throws` calls return `Result<T, Error>` automatically. Use `?` to unwrap:
Untrusted calls return `Result<T, Error>` automatically. Use `?` to unwrap:

```floe
// JSON.parse is stdlib — already returns Result, no throws needed
// JSON.parse is stdlib — already returns Result
const result = JSON.parse(input)
// result: Result<T, ParseError>

// throws is for external npm imports that might throw:
import throws { parseYaml } from "yaml-lib"
// npm imports are untrusted by default:
import { parseYaml } from "yaml-lib"
const data = parseYaml(input)
// data: Result<unknown, Error>

// Async throws: auto-awaits Promises and catches rejections
import throws { fetchUser } from "api-client"
// Async untrusted: auto-awaits Promises and catches rejections
import { fetchUser } from "api-client"
const user = fetchUser(id)
// user: Result<User, Error> — auto-awaited + wrapped

Expand Down Expand Up @@ -1595,7 +1595,7 @@ match findElement("app") {
#### Codegen

```typescript
// import throws { parseYaml } from "yaml-lib"; parseYaml(input) →
// import { parseYaml } from "yaml-lib"; parseYaml(input) → (untrusted, auto-wrapped)
(() => { try { return { ok: true as const, value: parseYaml(input) }; } catch (_e) { return { ok: false as const, error: _e instanceof Error ? _e : new Error(String(_e)) }; } })()
```

Expand All @@ -1614,7 +1614,7 @@ Emits clean, readable `.tsx`. Zero runtime imports.
| `Type.Variant` (qualified) | `{ tag: "Variant" }` (same as bare) |
| `fn f(x: T) -> U { ... }` | `function f(x: T): U { ... }` |
| `fn f<T>(x: T) -> T { ... }` | `function f<T>(x: T): T { ... }` |
| `throws` call (e.g. `parseYaml(input)`) | `(() => { try { return { ok: true, value: parseYaml(input) }; } catch (_e) { return { ok: false, error: _e instanceof Error ? _e : new Error(String(_e)) }; } })()` |
| untrusted npm call (e.g. `parseYaml(input)`) | `(() => { try { return { ok: true, value: parseYaml(input) }; } catch (_e) { return { ok: false, error: _e instanceof Error ? _e : new Error(String(_e)) }; } })()` |
| `match x { A -> ..., B -> ... }` | `x.tag === "A" ? ... : x.tag === "B" ? ... : absurd(x)` |
| `match x { A(v) when v > 0 -> ... }` | `x.tag === "A" ? (() => { const v = x.value; if (v > 0) { return ...; } ... })()` |
| `match url { "/users/{id}" -> f(id) }` | `url.match(/^\/users\/([^/]+)$/) ? (() => { const _m = url.match(...); const id = _m![1]; return f(id); })() : ...` |
Expand Down
36 changes: 18 additions & 18 deletions docs/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -405,8 +405,8 @@ const fastest = Promise.race([fetchFromCDN(url), fetchFromOrigin(url)]) |> Promi
const results = Promise.allSettled([fetchA(), fetchB()]) |> Promise.await // Array<Result<T, Error>>
Promise.delay(1000) |> Promise.await // wait 1 second

// throws imports: auto-wrapped in try/catch, return Result<T, Error>
import throws { npmAsyncFn } from "some-lib"
// npm imports are untrusted by default: auto-wrapped in try/catch, return Result<T, Error>
import { npmAsyncFn } from "some-lib"
const result = npmAsyncFn() // Result<T, Error> — auto-wrapped, awaits + catches rejections

// parse<T> — compiler built-in for runtime type validation
Expand Down Expand Up @@ -438,7 +438,7 @@ const vals = ICONS |> Record.values // Array<string> → Object.values(I
const has = Record.has(ICONS, key) // boolean → (key in ICONS)
const n = ICONS |> Record.size // number → Object.keys(ICONS).length

// Http — pipe-friendly fetch with Result (no throws needed)
// Http — pipe-friendly fetch with Result (already returns Result natively)
const data = Http.get("https://api.example.com/users") |> Promise.await? |> Http.json |> Promise.await?
const result = Http.post(url, { name: "Alice" }) |> Promise.await?
const body = Http.get(url) |> Promise.await? |> Http.text |> Promise.await?
Expand Down Expand Up @@ -468,22 +468,22 @@ const iso = d |> Date.toIso // d.toISOString()
// Standard import (Floe files)
import { Todo, Filter } from "./types"

// npm imports — safe by default, call directly
import { useState, useEffect } from "react"
import { clsx } from "clsx"
const [count, setCount] = useState(0) // no special syntax needed
// npm imports — untrusted by default, auto-wrapped in Result<T, Error>
import { fetchUser } from "some-lib"
const user = fetchUser("id") // Result<User, Error> — auto-wrapped

// throws imports — for npm functions that may throw
import throws { parseYaml } from "yaml-lib"
const result = parseYaml(input) // auto-wrapped in Result<T, Error>
// trusted imports — safe to call directly, no wrapping
import trusted { useState, useEffect } from "react"
import { clsx } from "clsx" // untrusted by default
const [count, setCount] = useState(0) // direct call (trusted)

// Per-function throws
import { capitalize, throws fetchData } from "some-lib"
capitalize("hello") // direct call, no wrapping
const data = fetchData() // Result<T, Error> — auto-wrapped
// Per-function trusted
import { trusted capitalize, fetchData } from "some-lib"
capitalize("hello") // direct call, no wrapping (trusted)
const data = fetchData() // Result<T, Error> — auto-wrapped (untrusted)

// Use ? to unwrap throws results
const parsed = parseYaml(input)? // unwraps or returns Err early
// Use ? to unwrap untrusted results
const parsed = fetchData()? // unwraps or returns Err early

// Importing for-block extensions
import { for User } from "./user-helpers"
Expand Down Expand Up @@ -674,7 +674,7 @@ test "addition" {
```floe
// todo — placeholder, type never, warns at compile time
fn processPayment(order: Order) -> Result<Receipt, Error> {
todo // throws "not implemented" at runtime
todo // panics with "not implemented" at runtime
}

// unreachable — assert impossible, type never
Expand Down Expand Up @@ -752,7 +752,7 @@ floe lsp # start language server (used by editors)
4. **`?` only works** inside functions returning `Result` or `Option`
5. **`==` is structural equality** — only between same types
6. **No bracket indexing** — use `Map.get(map, key)` for maps, `Record.get(obj, key)` for plain-object records, `Array.get(arr, i)` for arrays (all return `Option<T>`). `obj[key]` is not valid Floe syntax
7. **npm imports that may throw** — mark with `import throws` to auto-wrap in `Result<T, Error>`
7. **npm imports are untrusted by default** — calls auto-wrap in `Result<T, Error>`; use `import trusted` for safe imports
8. **`self` is explicit** in for blocks — no implicit `this`
9. **No mutation** of function parameters
10. **Unused variables/imports are compile errors** (prefix with `_` to suppress)
Expand Down
9 changes: 6 additions & 3 deletions docs/site/src/content/docs/docs/guide/error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,14 @@ The boundary wrapping also converts:
- `T | undefined` to `Option<T>`
- `any` to `unknown`

For npm functions that may throw, use `throws` imports. Calls are auto-wrapped in `Result<T, Error>`:
npm imports are untrusted by default -- calls are auto-wrapped in `Result<T, Error>`. Use `trusted` to mark safe imports that can be called directly:

```floe
import throws { parseYaml } from "yaml-lib"
const data = parseYaml(input)? // Result<T, Error>, ? unwraps
import { parseYaml } from "yaml-lib" // untrusted (default)
const data = parseYaml(input)? // Result<T, Error>, ? unwraps

import trusted { useState } from "react" // trusted = direct call
const [count, setCount] = useState(0)
```

This means npm libraries work transparently with Floe's type system.
Expand Down
2 changes: 1 addition & 1 deletion docs/site/src/content/docs/docs/guide/from-typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Floe is designed to be familiar to TypeScript developers.
| `let` / `const` | `const` only | No mutation |
| `===` | `==` | `==` compiles to `===` |
| `switch` | `match` | Exhaustive, no fall-through |
| `try/catch` | `throws` imports | `import throws { parseYaml } from "yaml-lib"` |
| `try/catch` | Untrusted imports (default) | `import { parseYaml } from "yaml-lib"` (auto-wrapped in Result) |
| `{x && <Comp />}` | `Option.map` | `{x \|> Option.map((v) => <Comp v={v} />)}` |
| `T \| null` | `Option<T>` | `Some(value)` / `None` |
| `throw` | `Result<T, E>` | `Ok(value)` / `Err(error)` |
Expand Down
2 changes: 1 addition & 1 deletion docs/site/src/content/docs/docs/guide/llm-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,5 @@ curl -o llms.txt https://floe-lang.dev/llms.txt
- Type system (records, unions, newtypes, opaque types, Result/Option)
- Compilation rules (what Floe compiles to in TypeScript)
- Standard library functions
- Import system (`throws` for error-wrapping, for-blocks)
- Import system (`trusted` for safe imports, untrusted by default, for-blocks)
- Common pitfalls and rules
4 changes: 2 additions & 2 deletions docs/site/src/content/docs/docs/guide/tour.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,9 @@ export fn Counter() -> JSX.Element {

```floe
import { Todo } from "./types"
import throws { parseYaml } from "yaml-lib"
import { parseYaml } from "yaml-lib" // untrusted by default
const data = parseYaml(input)? // auto-wrapped in Result, ? unwraps
import { useState } from "react" // npm imports work directly
import trusted { useState } from "react" // trusted = direct call, no wrapping
import { for Array } from "./helpers" // import for-block extensions

fn fetchUser(id: string) -> Promise<Result<User, Error>> {
Expand Down
26 changes: 18 additions & 8 deletions docs/site/src/content/docs/docs/guide/typescript-interop.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ import { z } from "zod"
import { clsx } from "clsx"
```

The compiler reads `.d.ts` type definitions to understand the types of imported values. These imports are callable directly -- no special syntax needed.
The compiler reads `.d.ts` type definitions to understand the types of imported values. npm imports are **untrusted by default** -- calls are auto-wrapped in `Result<T, Error>`.

## `throws` imports
## Untrusted imports (default)

Some npm functions can throw exceptions at runtime (JSON parsers, API clients, file I/O). Mark these imports with `throws` so the compiler auto-wraps calls in `Result<T, Error>`:
All npm imports are untrusted by default. The compiler auto-wraps calls in `Result<T, Error>`:

```floe
import throws { parseYaml } from "yaml-lib"
import { parseYaml } from "yaml-lib"

// parseYaml is auto-wrapped — returns Result<T, Error>
const result = parseYaml(input)
Expand All @@ -37,13 +37,23 @@ Use `?` to unwrap the result concisely:
const data = parseYaml(input)? // unwraps or returns Err early
```

You can mark individual functions as throwing from a module:
## `trusted` imports

For npm functions known to be safe (like React hooks, utility libraries), mark them with `trusted` so they can be called directly without Result wrapping:

```floe
import trusted { useState, useEffect } from "react"

const [count, setCount] = useState(0) // direct call, no wrapping
```

You can mark individual functions as trusted from a module:

```floe
import { capitalize, throws fetchData } from "some-lib"
import { trusted capitalize, fetchData } from "some-lib"

capitalize("hello") // direct call, no wrapping
const data = fetchData() // Result<T, Error> — auto-wrapped
capitalize("hello") // direct call, no wrapping (trusted)
const data = fetchData() // Result<T, Error> — auto-wrapped (untrusted)
```

## Bridge types (`=` syntax)
Expand Down
2 changes: 1 addition & 1 deletion docs/site/src/content/docs/docs/reference/stdlib/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ sidebar:
order: 10
---

Pipe-friendly HTTP functions that return `Promise<Result<...>>` natively. No `throws` import needed -- errors are captured automatically.
Pipe-friendly HTTP functions that return `Promise<Result<...>>` natively. As a stdlib module, errors are captured automatically.

## Functions

Expand Down
11 changes: 6 additions & 5 deletions docs/site/src/content/docs/docs/reference/stdlib/promise.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ fn fetchUser(id: string) -> Promise<User> {

The return type must explicitly use `Promise<T>`, making async behavior visible to callers.

## `throws` with async functions
## Untrusted async imports

When a `throws` import returns a `Promise<T>`, the auto-wrapping handles both sync throws and async rejections. The call is auto-awaited and wrapped in `Result<T, Error>`:
When an untrusted npm import returns a `Promise<T>`, the auto-wrapping handles both sync throws and async rejections. The call is auto-awaited and wrapped in `Result<T, Error>`:

```floe
// npm async function that might reject
import throws { transitionIssue } from "jira-api"
// npm async function that might reject (untrusted by default)
import { transitionIssue } from "jira-api"
const result = transitionIssue(id, tid)
// Result<(), Error> — auto-awaited, rejections caught

Expand All @@ -52,7 +52,8 @@ match result {
| Tool | For | Does |
|---|---|---|
| `Promise.await` | Floe async functions | Unwrap `Promise<Result<T, E>>`, use `?` for errors |
| `throws` imports | npm functions that may throw | Auto-wrap calls in `Result<T, Error>` (auto-awaits if Promise) |
| Untrusted imports (default) | npm functions | Auto-wrap calls in `Result<T, Error>` (auto-awaits if Promise) |
| `trusted` imports | npm functions known to be safe | Direct calls, no wrapping |

## Examples

Expand Down
16 changes: 8 additions & 8 deletions docs/site/src/content/docs/docs/reference/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,16 +302,16 @@ import { name } from "module"
import { name as alias } from "module"
import { a, b, c } from "module"

// npm imports work directly
import { useState } from "react"
const [count, setCount] = useState(0)
// npm imports are untrusted by default — auto-wrapped in Result<T, Error>
import { parseYaml } from "yaml-lib"
const result = parseYaml(input) // Result<T, Error> — auto-wrapped

// throws imports — for npm functions that may throw
import throws { parseYaml } from "yaml-lib"
const result = parseYaml(input) // auto-wrapped in Result<T, Error>
// trusted imports — safe to call directly, no wrapping
import trusted { useState } from "react"
const [count, setCount] = useState(0)

// Per-function throws
import { capitalize, throws fetchData } from "some-lib"
// Per-function trusted
import { trusted capitalize, fetchData } from "some-lib"

// Import for-block functions by type
import { for User } from "./helpers"
Expand Down
2 changes: 1 addition & 1 deletion docs/site/src/pages/playground.astro
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@

if (stream.match(/^[a-zA-Z_]\w*/)) {
const w = stream.current();
if (['fn','const','type','match','import','export','from','if','else','return','use','for','trait','opaque','throws','test','assert','pub','when'].includes(w)) return 'keyword';
if (['fn','const','type','match','import','export','from','if','else','return','use','for','trait','opaque','trusted','test','assert','pub','when'].includes(w)) return 'keyword';
if (['true','false'].includes(w)) return 'atom';
if (['Ok','Err','Some','None','Result','Option','Array','Map','Set','string','number','boolean','JSX'].includes(w)) return 'typeName';
if (w[0] >= 'A' && w[0] <= 'Z') return 'typeName';
Expand Down
2 changes: 1 addition & 1 deletion editors/neovim/queries/floe/highlights.scm
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"import" @keyword
"from" @keyword
"export" @keyword
"throws" @keyword
"trusted" @keyword
"for" @keyword
"trait" @keyword
"opaque" @keyword
Expand Down
4 changes: 2 additions & 2 deletions editors/tree-sitter-floe/grammar.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ module.exports = grammar({
import_declaration: ($) =>
seq(
"import",
optional("throws"),
optional("trusted"),
"{",
commaSep1(choice($.import_specifier, $.import_for_specifier)),
"}",
Expand All @@ -76,7 +76,7 @@ module.exports = grammar({
),

import_specifier: ($) =>
seq(optional("throws"), $.identifier, optional(seq("as", $.identifier))),
seq(optional("trusted"), $.identifier, optional(seq("as", $.identifier))),

import_for_specifier: ($) =>
seq("for", field("type", choice($.type_identifier, $.identifier))),
Expand Down
2 changes: 1 addition & 1 deletion editors/tree-sitter-floe/queries/highlights.scm
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"import" @keyword
"from" @keyword
"export" @keyword
"throws" @keyword
"trusted" @keyword
"for" @keyword
"trait" @keyword
"opaque" @keyword
Expand Down
4 changes: 2 additions & 2 deletions editors/tree-sitter-floe/src/grammar.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
"members": [
{
"type": "STRING",
"value": "throws"
"value": "trusted"
},
{
"type": "BLANK"
Expand Down Expand Up @@ -139,7 +139,7 @@
"members": [
{
"type": "STRING",
"value": "throws"
"value": "trusted"
},
{
"type": "BLANK"
Expand Down
Loading