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
2 changes: 1 addition & 1 deletion docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ All four of TypeScript's `?` uses (`?.`, `??`, `?:`, `? :`) are removed. `?` now
- Dot shorthand `.field` for implicit field-access closures
- JSX / TSX (full support)
- Generics (types and functions), template literals
- Async via `Promise.await` stdlib function (no `async`/`await` keywords)
- Async via `|> await` (or `|> Promise.await`) — no `async` keyword; return type must be `Promise<T>`
- Destructuring, spread, rest params
- `||` (boolean OR), `&&`, `!` (boolean operators)
- `==` (but only between same types — structural equality on objects)
Expand Down
17 changes: 9 additions & 8 deletions docs/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -399,11 +399,12 @@ items |> Array.dropWhile(.done) // drop while predicate holds
["a", "b", "c"] |> Array.intersperse(", ") // ["a", ", ", "b", ", ", "c"]

// Promise helpers
// Promise.await: Promise<T> -> T (compiles to await, infers async on enclosing fn)
const users = Promise.all([fetchUser(1), fetchUser(2)]) |> Promise.await
const fastest = Promise.race([fetchFromCDN(url), fetchFromOrigin(url)]) |> Promise.await
const results = Promise.allSettled([fetchA(), fetchB()]) |> Promise.await // Array<Result<T, Error>>
Promise.delay(1000) |> Promise.await // wait 1 second
// Promise.await (or bare `await`): Promise<T> -> T (compiles to await, infers async on enclosing fn)
// Functions using await must return Promise<T> (like ? requires Result<T, E>)
const users = Promise.all([fetchUser(1), fetchUser(2)]) |> await
const fastest = Promise.race([fetchFromCDN(url), fetchFromOrigin(url)]) |> await
const results = Promise.allSettled([fetchA(), fetchB()]) |> await // Array<Result<T, Error>>
Promise.delay(1000) |> await // wait 1 second

// npm imports are untrusted by default: auto-wrapped in try/catch, return Result<T, Error>
import { npmAsyncFn } from "some-lib"
Expand Down Expand Up @@ -704,7 +705,7 @@ match key {
| `void` | Unit type `()` |
| `as` (type assertion) | Type guards or `match` |
| `return` | Implicit returns — last expression is the return value |
| `async`/`await` | `Promise.await` stdlib function; return type `Promise<T>` |
| `async`/`await` | `\|> await` (or `\|> Promise.await`); return type `Promise<T>` |

## Compilation Examples

Expand All @@ -727,7 +728,7 @@ match key {
| `Console.log(x)` | `console.log(x)` |
| `(1, 2)` | `[1, 2] as const` |
| `()` (unit) | `undefined` |
| `expr \|> Promise.await` | `await expr` (enclosing fn inferred `async`) |
| `expr \|> await` | `await expr` (enclosing fn inferred `async`) |

## CLI Commands

Expand All @@ -749,7 +750,7 @@ floe lsp # start language server (used by editors)
1. **No semicolons** in Floe source
2. **Exported functions must have explicit return types** with `->`
3. **match is exhaustive** — all variants must be handled
4. **`?` only works** inside functions returning `Result` or `Option`
4. **`?` only works** inside functions returning `Result` or `Option`; **`|> await` requires** return type `Promise<T>`
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 are untrusted by default** — calls auto-wrap in `Result<T, Error>`; use `import trusted` for safe imports
Expand Down
4 changes: 2 additions & 2 deletions docs/site/src/content/docs/docs/guide/from-typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Floe is designed to be familiar to TypeScript developers.
- Import/export syntax
- Template literals
- JSX
- Async (via `Promise.await` stdlib function instead of keywords)
- Async (via `|> await` instead of keywords; return type must be `Promise<T>`)
- Type annotations
- Generics

Expand All @@ -27,7 +27,7 @@ Floe is designed to be familiar to TypeScript developers.
| `{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)` |
| `async`/`await` | `Promise.await` | `expr \|> Promise.await` (compiler infers `async`) |
| `async`/`await` | `\|> await` | `expr \|> await` (compiler infers `async`; return type must be `Promise<T>`) |

## What's Removed

Expand Down
8 changes: 5 additions & 3 deletions docs/site/src/content/docs/docs/guide/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,15 +128,17 @@ type Callback = () -> ()

### Async Functions

There is no `async` keyword. A function is async when its body uses `Promise.await`. The return type explicitly uses `Promise<T>`:
There is no `async` keyword. A function is async when its body uses `|> await` (or `|> Promise.await`). The return type must explicitly use `Promise<T>` -- the compiler enforces this, just like `?` requires `Result<T, E>`:

```floe
fn fetchUser(id: string) -> Promise<User> {
const response = fetch(`/api/users/${id}`) |> Promise.await
response.json() |> Promise.await
const response = fetch(`/api/users/${id}`) |> await
response.json() |> await
}
```

For functions without a return type annotation, the compiler infers `Promise<T>` automatically.

## Callback Flattening with `use`

The `use` keyword flattens nested callbacks. The rest of the block becomes the callback body:
Expand Down
32 changes: 28 additions & 4 deletions docs/site/src/content/docs/docs/reference/stdlib/promise.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,41 @@ Functions for working with `Promise<T>` values.

## `Promise.await`

`Promise.await` is a stdlib function with signature `Promise<T> -> T`. It compiles to JavaScript's `await` keyword. Using `Promise.await` anywhere in a function body causes the compiler to infer `async` on the emitted function -- no `async` keyword is needed in Floe.
`Promise.await` is a stdlib function with signature `Promise<T> -> T`. It compiles to JavaScript's `await` keyword. Using `Promise.await` anywhere in a function body causes the compiler to infer `async` on the emitted function -- no `async` keyword is needed in Floe. `await` is also available as a bare shorthand in pipes: `expr |> await`.

```floe
fn fetchUser(id: string) -> Promise<User> {
const response = fetch(`/api/users/${id}`) |> Promise.await
response.json() |> Promise.await
const response = fetch(`/api/users/${id}`) |> await
response.json() |> await
}
// Compiles to: async function fetchUser(id: string): Promise<User> { ... }
```

The return type must explicitly use `Promise<T>`, making async behavior visible to callers.
The return type must explicitly use `Promise<T>`, making async behavior visible to callers. The compiler enforces this -- using `await` in a function with a non-`Promise` return type is a compile error:

```floe
// Error: function `bad` uses `await` but return type is `string`, not `Promise<string>`
fn bad() -> string {
getData() |> await
}

// OK
fn good() -> Promise<string> {
getData() |> await
}
```

This parallels how `?` requires the function to return `Result<T, E>`. Both operators change the function contract, and both require explicit return types.

For functions without a return type annotation, the compiler infers `Promise<T>` automatically:

```floe
fn fetchName(id: string) {
const user = fetchUser(id) |> await
user.name
}
// Inferred return type: Promise<string>
```

## Untrusted async imports

Expand Down
25 changes: 20 additions & 5 deletions src/checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,22 @@ pub fn mark_async_functions(program: &mut Program) {
_ => {}
}
}
// Also mark arrows inside all expressions
crate::walk::walk_program_mut(program, &mut |expr| {
if let ExprKind::Arrow { async_fn, body, .. } = &mut expr.kind {
// Mark nested fn declarations and arrows inside all expressions.
// The walker visits Block/Collect items recursively, so this catches
// nested `fn` declarations (e.g. handleDragEnd inside a component body).
crate::walk::walk_program_mut(program, &mut |expr| match &mut expr.kind {
ExprKind::Arrow { async_fn, body, .. } => {
*async_fn = body_has_promise_await(body) || body_has_throwing_call(body, &throwing);
}
ExprKind::Block(items) | ExprKind::Collect(items) => {
for item in items {
if let ItemKind::Function(decl) = &mut item.kind {
decl.async_fn = body_has_promise_await(&decl.body)
|| body_has_throwing_call(&decl.body, &throwing);
}
}
}
_ => {}
});
}

Expand Down Expand Up @@ -116,16 +127,20 @@ fn body_has_throwing_call(expr: &Expr, throwing: &HashSet<String>) -> bool {
walk(expr, throwing)
}

/// Check if an expression body contains a `Promise.await` member access.
fn body_has_promise_await(expr: &Expr) -> bool {
/// Check if an expression body contains a `Promise.await` member access
/// or bare `await` identifier (shorthand for `Promise.await` in pipes).
pub(crate) fn body_has_promise_await(expr: &Expr) -> bool {
fn walk(expr: &Expr) -> bool {
match &expr.kind {
// Qualified: `Promise.await`
ExprKind::Member { object, field }
if field == "await"
&& matches!(&object.kind, ExprKind::Identifier(m) if m == "Promise") =>
{
true
}
// Bare shorthand: `|> await`
ExprKind::Identifier(name) if name == "await" => true,
ExprKind::Call { callee, args, .. } => {
walk(callee)
|| args.iter().any(|a| match a {
Expand Down
3 changes: 3 additions & 0 deletions src/checker/error_codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ pub enum ErrorCode {
UnknownBinding,
/// `try` used on a Floe function (which never throws).
TryOnFloeFunction,
/// Function uses `await` but return type is not `Promise<T>`.
MissingPromiseReturn,
}

impl ErrorCode {
Expand Down Expand Up @@ -161,6 +163,7 @@ impl ErrorCode {
Self::SuspiciousBinding => "W005",
Self::UnknownBinding => "W006",
Self::TryOnFloeFunction => "W007",
Self::MissingPromiseReturn => "E041",
}
}
}
Expand Down
25 changes: 24 additions & 1 deletion src/checker/items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ impl Checker {
fn is_promise_await_member(expr: &Expr) -> bool {
matches!(&expr.kind, ExprKind::Member { object, field }
if field == "await" && matches!(&object.kind, ExprKind::Identifier(m) if m == "Promise"))
|| matches!(&expr.kind, ExprKind::Identifier(name) if name == "await")
}

/// Infer the type of an element from an array/tuple destructuring at a given index.
Expand Down Expand Up @@ -442,12 +443,19 @@ impl Checker {

// Check body
let body_type = self.check_expr(&decl.body);
let uses_await = super::body_has_promise_await(&decl.body);

// When no return type annotation, infer from body and update the function type
if decl.return_type.is_none() && !matches!(body_type, Type::Var(_) | Type::Unknown) {
// If the body uses await, wrap the inferred return type in Promise<T>
let inferred_return = if uses_await {
Type::Promise(Box::new(body_type.clone()))
} else {
body_type.clone()
};
let fn_type = Type::Function {
params: param_types.clone(),
return_type: Box::new(body_type.clone()),
return_type: Box::new(inferred_return),
required_params,
};
// Update in the name_types map for hover display
Expand All @@ -460,6 +468,21 @@ impl Checker {
// Check return type compatibility
if let Some(ref declared_return) = decl.return_type {
let resolved = self.resolve_type(declared_return);

// Error if function uses await but return type is not Promise<T>
if uses_await && !matches!(resolved, Type::Promise(_)) {
self.emit_error_with_help(
format!(
"function `{}` uses `await` but return type is `{}`, not `Promise<{}>`",
decl.name, resolved, body_type
),
span,
ErrorCode::MissingPromiseReturn,
format!("expected `Promise<{}>`", body_type),
"change the return type to `Promise<T>`, or remove the `await`",
);
}

// For functions with Promise<T> return type, unwrap Promise since
// the body type is the inner value (async wrapping is automatic)
let effective_declared = match &resolved {
Expand Down
60 changes: 59 additions & 1 deletion src/checker/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2874,7 +2874,65 @@ fn stdlib_member_access_still_works() {

// ── Promise / Promise.await ─────────────────────────────────

// ── Promise / Promise.await ─────────────────────────────────
#[test]
fn await_without_promise_return_type_errors() {
let diags = check(
r#"
fn getData() -> Promise<string> { "hello" }
fn bad() -> string { getData() |> Promise.await }
"#,
);
assert!(
has_error_containing(&diags, "uses `await`"),
"should error when await used but return type is not Promise<T>, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}

#[test]
fn await_with_promise_return_type_ok() {
let diags = check(
r#"
fn getData() -> Promise<string> { "hello" }
fn good() -> Promise<string> { getData() |> Promise.await }
"#,
);
assert!(
!has_error_containing(&diags, "uses `await`"),
"should not error when return type is Promise<T>, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}

#[test]
fn bare_await_without_promise_return_type_errors() {
let diags = check(
r#"
fn getData() -> Promise<string> { "hello" }
fn bad() -> string { getData() |> await }
"#,
);
assert!(
has_error_containing(&diags, "uses `await`"),
"bare |> await should also trigger the error, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}

#[test]
fn await_inferred_return_type_is_promise() {
let diags = check(
r#"
fn getData() -> Promise<string> { "hello" }
fn inferred() { getData() |> Promise.await }
"#,
);
assert!(
!has_error_containing(&diags, "uses `await`"),
"unannotated async fn should not error, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}

#[test]
fn promise_return_type_unwrap() {
Expand Down
21 changes: 13 additions & 8 deletions src/codegen/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -808,7 +808,7 @@ impl Codegen {
}

/// Check if an expression tree contains a Promise.await stdlib call.
/// Detects `expr |> Promise.await` and `Promise.await(expr)` patterns.
/// Detects `expr |> Promise.await`, `Promise.await(expr)`, and bare `|> await` patterns.
pub(super) fn expr_contains_await(expr: &Expr) -> bool {
match &expr.kind {
// Direct member access: Promise.await (in pipe target position)
Expand All @@ -818,6 +818,8 @@ pub(super) fn expr_contains_await(expr: &Expr) -> bool {
{
true
}
// Bare shorthand: `|> await`
ExprKind::Identifier(name) if name == "await" => true,
ExprKind::Call { callee, args, .. } => {
expr_contains_await(callee)
|| args.iter().any(|a| match a {
Expand All @@ -833,13 +835,16 @@ pub(super) fn expr_contains_await(expr: &Expr) -> bool {
| ExprKind::Grouped(operand)
| ExprKind::Unwrap(operand)
| ExprKind::Spread(operand) => expr_contains_await(operand),
ExprKind::Collect(items) | ExprKind::Block(items) => items.iter().any(|item| {
if let ItemKind::Expr(e) = &item.kind {
expr_contains_await(e)
} else {
false
}
}),
ExprKind::Match { subject, arms } => {
expr_contains_await(subject) || arms.iter().any(|a| expr_contains_await(&a.body))
}
ExprKind::Collect(items) | ExprKind::Block(items) => {
items.iter().any(|item| match &item.kind {
ItemKind::Expr(e) => expr_contains_await(e),
ItemKind::Const(c) => expr_contains_await(&c.value),
_ => false,
})
}
_ => false,
}
}
Loading