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
4 changes: 2 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,13 @@ The language surface is organized around **structural types**, **explicit annota

| Feature | Status | Notes |
| --- | --- | --- |
| `ensure` statements (basic type assertions) | ✅ done | Validates assertions and optional blocks; does **not** narrow the subject’s type for statements **after** the `ensure` (see control-flow narrowing). |
| `ensure` statements (basic type assertions) | ✅ done | Validates assertions and optional blocks. **Successor narrowing** after a successful `ensure` works for **simple identifiers** (`ensure x is …` then use `x` on the next lines); **compound paths** (e.g. `ensure req.state is …`) remain deferred (see control-flow narrowing). |
| Shape guards (struct refinement) | ✅ done | Refinement on shapes. |
| `is` operator for `ensure` conditions | ✅ done | Parser requires `ensure … is …` (see `forst/internal/parser/ensure.go`; `ensure !ident` uses implicit `Nil()`). The typechecker enforces presence (`Present`) and type-guard subject compatibility (`forst/internal/typechecker/unify_typeguard.go`, `infer_ensure.go`); it does **not** fully validate arbitrary built-in constraint semantics beyond that. Emission: `forst/internal/transformer/go/ensure.go`, `ensure_constraint.go`. Examples: `examples/in/ensure.ft` and `task example:ensure`. |
| Type guards (beyond shape guards) | 🔬 experimental | Top-level `is (subject T) Name { … }` declarations are **implemented** end-to-end: parse (`forst/internal/parser/typeguard.go`), typecheck (`unify_typeguard.go`, registration in `Defs`), Go emit (`forst/internal/transformer/go/typeguard.go`, `ensure_typeguard.go`). Examples: `examples/in/rfc/guard/` (`task example:shape-guard`, `task example:basic-guard`). **Not** at full RFC “done” yet: narrowing across all control-flow positions and other polish overlap **Control-flow type narrowing**; language design: [type guards RFC](./examples/in/rfc/guard/guard.md); Go / `.d.ts` interop: [interop](./examples/in/rfc/guard/interop.md). |
| Immutability guarantees (`ensure`-scoped; unsafe mode for Go interop) | 📋 planned | Not implemented. |
| Binary type expressions (`A \| B`, `A & B`) | 🔬 experimental | Parser, AST, and typechecker support **`&`** / **`|`** on typedef bodies: unions flatten to **`TypeUnion`**, intersections use meet (`forst/internal/typechecker/typedef_binary.go`); Go emit lowers via canonical **`TypeNode`** (`forst/internal/transformer/go/typedef_expr.go`). **Closed unions of nominal errors** emit sealed Go interfaces (`error_union_sealed.go`); examples `union_error_types.ft`, `union_error_narrowing.ft`. **Gaps vs “done”:** general non-error unions, full intersection algebra, union narrowing in all control-flow positions, and TS/Go emit for arbitrary **`A \| B`** combinations—do not rely on binary typedef semantics beyond tested nominal-error and alias paths. **Narrowing** and future optionals should share one internal type algebra (assertions / `TypeNode`), not a parallel representation—see control-flow narrowing. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Escape the pipe in this table cell.

Line 78 is currently parsed as a 4-column row, so the binary-types entry will not render reliably. Use a representation that avoids a raw | inside the table cell.

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 78-78: Spaces inside code span elements

(MD038, no-space-in-code)


[warning] 78-78: Spaces inside code span elements

(MD038, no-space-in-code)


[warning] 78-78: Spaces inside code span elements

(MD038, no-space-in-code)


[warning] 78-78: Table column count
Expected: 3; Actual: 4; Too many cells, extra data will be missing

(MD056, table-column-count)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ROADMAP.md` at line 78, The binary types entry in ROADMAP.md contains raw
pipe characters inside a table cell, which breaks the row structure. Update the
table cell that describes binary type expressions so it no longer uses unescaped
`|` characters; use an escaped or otherwise safe representation while keeping
the references to the parser, AST, typechecker, and emit symbols intact.

Source: Linters/SAST tools

| Control-flow type narrowing | 🔬 experimental | **If-branch narrowing** for `if x is …` (assertion or shape RHS): refined type for the subject is recorded in the branch scope; variable types are keyed by identifier **and** source span in the typechecker so hover can differ per occurrence. Join/merge across branches and full `ensure`-successor narrowing are not done yet. Implementation: `forst/internal/typechecker/infer_if.go`, `narrow_if.go`; LSP uses `InferredTypesForVariableNode` when the variable AST node is found at the cursor. Type guards vs optionals / `Result`: [type guards & optionals](./examples/in/rfc/optionals/10-type-guards-shape-guards-and-optionals.md). |
| Control-flow type narrowing | 🔬 experimental | **If-branch narrowing** for `if x is …` (assertion or shape RHS): refined type for the subject is recorded in the branch scope; variable types are keyed by identifier **and** source span in the typechecker so hover can differ per occurrence. **Ensure-successor narrowing** works for simple identifiers; compound paths and join/merge across branches are not done yet. Implementation: `forst/internal/typechecker/infer_if.go`, `narrow_if.go`; LSP uses `InferredTypesForVariableNode` when the variable AST node is found at the cursor. Type guards vs optionals / `Result`: [type guards & optionals](./examples/in/rfc/optionals/10-type-guards-shape-guards-and-optionals.md). |

### Generics, aliases, and nominal features

Expand Down
12 changes: 12 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,18 @@ tasks:
cmds:
- go run ./{{.BUILD_DIR}} run -loglevel trace -report-phases -- {{.EXAMPLES_DIR}}/union_error_types.ft

example:union-error-narrowing:
desc: Compile union error narrowing example (golden examples/out/union_error_narrowing.go)
dir: forst
cmds:
- go run ./{{.BUILD_DIR}} run -loglevel trace -report-phases -- {{.EXAMPLES_DIR}}/union_error_narrowing.ft

example:map-catalog:
desc: Compile map catalog example (golden examples/out/map_catalog.go)
dir: forst
cmds:
- go run ./{{.BUILD_DIR}} run -loglevel trace -report-phases -- {{.EXAMPLES_DIR}}/map_catalog.ft

example:result-ensure:
desc: Compile Result + ensure Ok() example (golden examples/out/result_ensure/)
dir: forst
Expand Down
6 changes: 3 additions & 3 deletions docs/language/shapes-and-constraints.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import ShapesConstraintChaining from "/snippets/shapes-constraint-chaining.mdx";
import ShapesPlaceOrderTags from "/snippets/shapes-place-order-tags.mdx";
import ShapesAddressCustomer from "/snippets/shapes-address-customer.mdx";

In Go you declare a struct and write manual checks. In Forst, **constraints live on the type**. The compiler emits runtime validation at the boundary.
In Go you declare a struct and write manual checks. In Forst, **constraints live on the type**. Where values are known at compile time, the typechecker proves them statically; for dynamic input, use **`ensure … is …`** in the function body today. Automatic validation prologues on parameters are planned—not emitted yet.

## Manual validation in Go

Expand All @@ -35,7 +35,7 @@ The struct says nothing about valid ranges. Every handler repeats the same `if`

<CatalogOrder />

When `placeOrder` runs, the compiler has already proven the shape at compile time where values are known, and emits checks for dynamic input. Invalid SKUs and quantities never reach your catalog logic.
When `placeOrder` runs, the compiler has already proven the shape at compile time where values are known; use `ensure` (or explicit checks) for dynamic input. Invalid SKUs and quantities never reach your catalog logic.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

This still overstates the runtime guarantee.

The surrounding edits correctly say dynamic validation happens via explicit ensure checks in the function body, but “Invalid SKUs and quantities never reach your catalog logic” still reads like automatic pre-body validation exists. Until prologue emission lands, invalid dynamic input reaches the function and is only blocked once those explicit checks run.

🧰 Tools
🪛 LanguageTool

[style] ~38-~38: It might be better to use ‘time’ with the time-relative pronoun ‘when’. (Alternatively, use ‘in/on which’.)
Context: ...has already proven the shape at compile time where values are known; use ensure (or expl...

(WHEN_WHERE)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/language/shapes-and-constraints.mdx` at line 38, The current wording in
the `placeOrder` example still implies invalid dynamic input is blocked before
the function body runs. Update the sentence near `placeOrder` to make it clear
that runtime safety comes from explicit `ensure` checks or equivalent validation
inside the function body, and that invalid SKUs or quantities can reach the
function before those checks execute.


## Built-in constraints

Expand Down Expand Up @@ -95,7 +95,7 @@ Invalid nested data fails at the same boundary. You do not re-walk the tree in a

## What gets emitted

The compiler lowers constraints to plain Go checks before your function body runs: `String.Min(1)` becomes a `len(field)` comparison, `Int.Min(1)` a numeric comparison, and `HasPrefix` / `Contains` call `strings.*` with an auto-added import. Generated Go stays readable and uses standard error returns. There is no hidden magic at runtime.
The compiler lowers **`ensure`** checks in the function body to plain Go: `String.Min(1)` becomes a `len(field)` comparison, `Int.Min(1)` a numeric comparison, and `HasPrefix` / `Contains` call `strings.*` with an auto-added import. Parameter prologue validation (automatic checks before your function body) is not emitted yet—use `ensure` explicitly today.

## Try it

Expand Down
6 changes: 3 additions & 3 deletions docs/resources/roadmap.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ Forst is under active development. This page is a **simplified** view. The canon

| Status | Feature | Notes |
| --- | --- | --- |
| <Icon icon="circle-check" iconType="solid" size={16} color="#16A34A" /> | [Shapes and constraints](/language/shapes-and-constraints) | Structural types, built-in constraints, runtime validation at the boundary. |
| <Icon icon="circle-check" iconType="solid" size={16} color="#16A34A" /> | [`ensure`, `is`, and shape guards](/language/ensure-and-narrowing) | Runtime checks tied to types; shape refinement on records. |
| <Icon icon="flask" iconType="solid" size={16} color="#7C3AED" /> | [Shapes and constraints](/language/shapes-and-constraints) | Structural types and built-in constraints; compile-time checks where values are static, runtime validation via `ensure` in the body today. Parameter prologue emission is future work. |
| <Icon icon="flask" iconType="solid" size={16} color="#7C3AED" /> | [`ensure`, `is`, and shape guards](/language/ensure-and-narrowing) | Runtime checks tied to types; shape refinement on records. Successor narrowing after `ensure` works for simple identifiers; compound paths still open. |
| <Icon icon="circle-check" iconType="solid" size={16} color="#16A34A" /> | [Nil and presence checks](/language/ensure-and-narrowing) | `ensure x is Nil()`, `ensure !x`, and `*T` `.Present()` / `.Nil()` on pointers. Not full optional value types yet. |
| <Icon icon="flask" iconType="solid" size={16} color="#7C3AED" /> | [Type guards](/language/type-guards) | Domain predicates with `is (subject T) Name { … }`; narrowing polish still open. |
| <Icon icon="flask" iconType="solid" size={16} color="#7C3AED" /> | [Result and nominal errors](/language/errors-and-result) | `Result(S, F)`, `error X { … }`, and `ensure` / `if x is Ok()` narrowing. Closed nominal error unions (`ParseError \| IoError`) emit sealed Go interfaces. Failure-type and export rules still maturing. |
| <Icon icon="flask" iconType="solid" size={16} color="#7C3AED" /> | Control-flow narrowing | If-branch refinement works; join across branches and post-`ensure` narrowing still open. |
| <Icon icon="flask" iconType="solid" size={16} color="#7C3AED" /> | Control-flow narrowing | If-branch refinement works; ensure-successor narrowing for simple identifiers works; compound paths and join across branches still open. |
| <Icon icon="flask" iconType="solid" size={16} color="#7C3AED" /> | Type aliases | Simple `type Name = BaseType` works end-to-end; broader alias semantics still evolving. |
| <Icon icon="flask" iconType="solid" size={16} color="#7C3AED" /> | Binary types (`A \| B`, `A & B`) | Union and intersection typedefs type-check; closed nominal error unions emit to Go and TS. General unions, intersections, and narrowing still incomplete. |
| <Icon icon="calendar" iconType="solid" size={16} color="#64748B" /> | Optional types (`T \| Nil`) | Planned: Crystal-style `T \| Nil` / `T?` unions, narrowing, and Go lowering. See [optionals RFC](https://github.com/forst-lang/forst/tree/main/examples/in/rfc/optionals). |
Expand Down
7 changes: 4 additions & 3 deletions docs/snippets/ensure-if-ok.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
</Tab>
<Tab title="Generated Go">
```go
// forst build
if x, ok := x.(Ok); ok {
println(x.Value)
// forst build — Result lowers to (T, error)
x, xErr := one()
if xErr == nil {
println(x)
Comment on lines +11 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the Generated Go tab aligned with the Forst tab.

The Forst example starts from an existing x, but the Go example now introduces one() and xErr without showing the corresponding setup. Either include the x := one() source in the Forst tab or annotate that this Go snippet assumes x came from a Result-returning call.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/snippets/ensure-if-ok.mdx` around lines 11 - 14, The Forst and Go
snippets are out of sync because the Go tab introduces one() and xErr without
showing where x comes from, while the Forst tab still starts from an existing x.
Update the example so both tabs reflect the same setup: either add the missing x
:= one() source to the Forst side or clearly annotate in the Go snippet that x
is assumed to come from a Result-returning call. Use the existing snippet
structure in the ensure-if-ok example to keep the two tabs aligned.

}
```
</Tab>
Expand Down
2 changes: 1 addition & 1 deletion docs/snippets/type-guards-mutation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
</Tab>
<Tab title="Generated Go">
```go
// forst build
// forst build — illustrative; type-level guards do not emit Go helpers yet

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

This comment contradicts the feature added in this PR.

The stack context for this change says type-level guards now emit stub Go declarations, so saying they “do not emit Go helpers yet” is immediately stale. Please update the note and snippet to describe the stub output instead of framing it as purely illustrative.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/snippets/type-guards-mutation.mdx` at line 17, Update the note in the
type-guards mutation snippet to match the new behavior: type-level guards now
emit stub Go declarations, so remove the stale “do not emit Go helpers yet”
wording and rewrite the comment to describe the stub output. Keep the change
localized to the snippet in the docs and ensure any accompanying text around
type-level guards reflects the new emitted Go declaration behavior.

func G_Input(m MutationArg, input Shape) error {
// shape guard: m must include input field
return nil
Expand Down
2 changes: 1 addition & 1 deletion examples/in/union_error_narrowing.ft
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package main

// Narrowing: failure payloads from closed nominal-error unions (ErrKind = A | B) use Result(S, ErrKind)
// and `Err()` / `Err(...)` guards — not `is ParseError` on the union (nominal errors are not `is` guards).
// Covered by compiler and typechecker tests; no golden .go (TestExamples skips when out/ is missing).
// Golden: ../out/union_error_narrowing.go (`task example:union-error-narrowing`, cmd/forst TestExamples).

error ParseError {
code: Int,
Expand Down
52 changes: 52 additions & 0 deletions forst/internal/ast/compound_assign_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,58 @@ func TestIsAssignmentOperatorToken(t *testing.T) {
}
}

func TestCompoundAssignBinaryOp_allOperators(t *testing.T) {
t.Parallel()
cases := []struct {
compound TokenIdent
binary TokenIdent
}{
{TokenMinusEq, TokenMinus},
{TokenStarEq, TokenStar},
{TokenDivideEq, TokenDivide},
{TokenModuloEq, TokenModulo},
{TokenBitwiseAndEq, TokenBitwiseAnd},
{TokenBitwiseOrEq, TokenBitwiseOr},
}
for _, tc := range cases {
op, ok := CompoundAssignBinaryOp(tc.compound)
if !ok || op != tc.binary {
t.Fatalf("%s -> %v ok=%v", tc.compound, op, ok)
}
}
if _, ok := CompoundAssignBinaryOp(TokenColonEquals); ok {
t.Fatal(":= is not compound assign")
}
}

func TestCompoundAssignOperatorString_allSpellings(t *testing.T) {
t.Parallel()
cases := map[TokenIdent]string{
TokenPlusEq: "+=",
TokenMinusEq: "-=",
TokenStarEq: "*=",
TokenDivideEq: "/=",
TokenModuloEq: "%=",
TokenBitwiseAndEq: "&=",
TokenBitwiseOrEq: "|=",
}
for tok, want := range cases {
if got := CompoundAssignOperatorString(tok); got != want {
t.Fatalf("%s: got %q want %q", tok, got, want)
}
}
if got := CompoundAssignOperatorString(TokenPlus); got != string(TokenPlus) {
t.Fatalf("unknown token: %q", got)
}
}

func TestIsAssignmentOperatorToken_colonEquals(t *testing.T) {
t.Parallel()
if !IsAssignmentOperatorToken(Token{Type: TokenColonEquals, Value: ":="}) {
t.Fatal("expected := to be assignment operator")
}
}

func TestAssignmentNode_String_compound(t *testing.T) {
t.Parallel()
a := AssignmentNode{
Expand Down
2 changes: 1 addition & 1 deletion forst/internal/ast/expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ type IndexExpressionNode struct {
Index ExpressionNode
}

func (IndexExpressionNode) isExpression() {}
func (i IndexExpressionNode) isExpression() { _ = i }

// Kind returns the node kind for index expressions.
func (i IndexExpressionNode) Kind() NodeKind {
Expand Down
2 changes: 2 additions & 0 deletions forst/internal/ast/for_stmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import "fmt"
// Range: IsRange true, RangeX required; RangeKey/RangeValue optional (nil means omitted, as in `for range ch`).
// Use Ident ID "_" for blank identifiers.
type ForNode struct {
Label *Ident

Init Node
Cond ExpressionNode
Post Node
Expand Down
5 changes: 5 additions & 0 deletions forst/internal/ast/interface_markers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,9 @@ func TestInterfaceMarkerMethodsExecute(t *testing.T) {
VariableNode{}.isValue()
DereferenceNode{}.isValue()
ReferenceNode{}.isValue()

DereferenceNode{}.isExpression()
IndexExpressionNode{}.isExpression()
OkExprNode{}.isExpression()
ErrExprNode{}.isExpression()
}
10 changes: 9 additions & 1 deletion forst/internal/ast/param.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package ast

import "fmt"
import (
"fmt"
"strings"
)

// ParamNode is the interface for parameter nodes
type ParamNode interface {
Expand Down Expand Up @@ -55,3 +58,8 @@ func (p SimpleParamNode) GetType() TypeNode {
func (p DestructuredParamNode) GetType() TypeNode {
return p.Type
}

// GetIdent returns a comma-separated list of destructured field names.
func (p DestructuredParamNode) GetIdent() string {
return strings.Join(p.Fields, ", ")
}
3 changes: 3 additions & 0 deletions forst/internal/ast/param_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ func TestDestructuredParamNode_String_Kind_GetType(t *testing.T) {
if d.GetType().Ident != TypeInt {
t.Fatal(d.GetType())
}
if d.GetIdent() != "a, b" {
t.Fatalf("GetIdent() = %q", d.GetIdent())
}
}
17 changes: 17 additions & 0 deletions forst/internal/ast/providers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,23 @@ func TestIsPublicExportIdent(t *testing.T) {
if IsPublicExportIdent("handle") {
t.Fatal("handle is not public")
}
if IsPublicExportIdent("") {
t.Fatal("empty ident is not public")
}
}

func TestParamTypesFromFunction_simpleParamsOnly(t *testing.T) {
fn := FunctionNode{
Params: []ParamNode{
SimpleParamNode{Ident: Ident{ID: "a"}, Type: TypeNode{Ident: TypeInt}},
DestructuredParamNode{Fields: []string{"x"}, Type: TypeNode{Ident: TypeString}},
SimpleParamNode{Ident: Ident{ID: "b"}, Type: TypeNode{Ident: TypeString}},
},
}
types := ParamTypesFromFunction(fn)
if len(types) != 2 || types[0].Ident != TypeInt || types[1].Ident != TypeString {
t.Fatalf("got %+v", types)
}
}

func TestIsTestingTParamType(t *testing.T) {
Expand Down
4 changes: 2 additions & 2 deletions forst/internal/ast/result_expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ type OkExprNode struct {
Value ExpressionNode
}

func (OkExprNode) isExpression() {}
func (o OkExprNode) isExpression() { _ = o }

func (OkExprNode) Kind() NodeKind { return NodeKindOkExpr }

Expand All @@ -25,7 +25,7 @@ type ErrExprNode struct {
Value ExpressionNode
}

func (ErrExprNode) isExpression() {}
func (e ErrExprNode) isExpression() { _ = e }

func (ErrExprNode) Kind() NodeKind { return NodeKindErrExpr }

Expand Down
49 changes: 49 additions & 0 deletions forst/internal/ast/shape_format_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,55 @@ func TestShapeFieldNamesForCompositeEmit_preservesShapeFieldOrder(t *testing.T)
}
}

func TestShapeFieldNode_MethodSignatureString(t *testing.T) {
field := ShapeFieldNode{
IsMethod: true,
MethodParams: []ParamNode{
SimpleParamNode{Ident: Ident{ID: "n"}, Type: TypeNode{Ident: TypeInt}},
},
}
if got := field.MethodSignatureString(); got != "(n Int)" {
t.Fatalf("got %q", got)
}
}

func TestShapeFieldNamesInOrder_usesFieldOrderWhenSet(t *testing.T) {
fields := map[string]ShapeFieldNode{"b": {}, "a": {}}
got := ShapeFieldNamesInOrder(fields, []string{"b", "a"})
if len(got) != 2 || got[0] != "b" || got[1] != "a" {
t.Fatalf("got %v", got)
}
}

func TestShapeFieldNamesForCompositeEmit_skipsOrderEntriesMissingFromPayload(t *testing.T) {
payload := map[string]ShapeFieldNode{"only": {Type: &TypeNode{Ident: TypeString}}}
shape := &ShapeNode{FieldOrder: []string{"ghost", "only"}}
got := ShapeFieldNamesForCompositeEmit(payload, shape)
if len(got) != 1 || got[0] != "only" {
t.Fatalf("got %v", got)
}
}

func TestShapeFieldNamesForCompositeEmit_appendsExtraPayloadFieldsSorted(t *testing.T) {
payload := map[string]ShapeFieldNode{
"id": {Type: &TypeNode{Ident: TypeString}},
"extra": {Type: &TypeNode{Ident: TypeInt}},
}
shape := &ShapeNode{FieldOrder: []string{"id"}}
got := ShapeFieldNamesForCompositeEmit(payload, shape)
if len(got) != 2 || got[0] != "id" || got[1] != "extra" {
t.Fatalf("got %v", got)
}
}

func TestShapeFieldNamesForCompositeEmit_nilShapeUsesSortedOrder(t *testing.T) {
payload := map[string]ShapeFieldNode{"z": {}, "a": {}}
got := ShapeFieldNamesForCompositeEmit(payload, nil)
if len(got) != 2 || got[0] != "a" || got[1] != "z" {
t.Fatalf("got %v", got)
}
}

func TestShapeFieldNamesInOrder_sortsWhenUnspecified(t *testing.T) {
fields := map[string]ShapeFieldNode{
"z": {},
Expand Down
41 changes: 41 additions & 0 deletions forst/internal/ast/shape_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,47 @@ func TestShapeNode_String_multiple_top_level_fields(t *testing.T) {
}
}

func TestShapeFieldNode_IsMethodField_and_methodString(t *testing.T) {
field := ShapeFieldNode{
IsMethod: true,
MethodParams: []ParamNode{
SimpleParamNode{Ident: Ident{ID: "msg"}, Type: TypeNode{Ident: TypeString}},
},
MethodReturnTypes: []TypeNode{{Ident: TypeError}},
}
if !field.IsMethodField() {
t.Fatal("expected method field")
}
if got := field.String(); got != "(msg String): Error" {
t.Fatalf("got %q", got)
}
}

func TestShapeNode_IsMethodOnlyContract_emptyFields(t *testing.T) {
if (ShapeNode{Fields: map[string]ShapeFieldNode{}}).IsMethodOnlyContract() {
t.Fatal("empty shape is not method-only")
}
}

func TestShapeFieldNode_ValueExpression_fromNode(t *testing.T) {
lit := StringLiteralNode{Value: "wired"}
field := ShapeFieldNode{Node: lit}
expr, ok := field.ValueExpression()
if !ok {
t.Fatal("expected expression from Node")
}
if got, ok := expr.(StringLiteralNode); !ok || got.Value != "wired" {
t.Fatalf("got %#v", expr)
}
}

func TestShapeFieldNode_ValueExpression_nonExpressionNode(t *testing.T) {
field := ShapeFieldNode{Node: CommentNode{Text: "note"}}
if _, ok := field.ValueExpression(); ok {
t.Fatal("comment node is not an expression")
}
}

func TestShapeNode_String_field_assertion_branch(t *testing.T) {
bt := TypeIdent("Str")
s := ShapeNode{Fields: map[string]ShapeFieldNode{
Expand Down
14 changes: 14 additions & 0 deletions forst/internal/ast/type_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,20 @@ func TestTypeNode_String_each_simple_builtin_switch_case(t *testing.T) {
}
}

func TestNewChannelType_andString(t *testing.T) {
t.Parallel()
ch := NewChannelType(NewBuiltinType(TypeInt))
if ch.Ident != TypeChannel || len(ch.TypeParams) != 1 || ch.TypeParams[0].Ident != TypeInt {
t.Fatalf("got %+v", ch)
}
if got := ch.String(); got != "chan Int" {
t.Fatalf("got %q", got)
}
if got := (TypeNode{Ident: TypeChannel}).String(); got != "chan" {
t.Fatalf("got %q", got)
}
}

func TestTypeNode_String_default_branch_multiple_type_params(t *testing.T) {
tn := TypeNode{
Ident: "Pair",
Expand Down
Loading
Loading