• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

forst-lang / forst / 30218381422

26 Jul 2026 08:09PM UTC coverage: 75.716% (-0.4%) from 76.147%
30218381422

push

github

haveyaseen
feat(typechecker)!: map Go multi-return to Tuple, keep Result exclusive

Go FFI calls bound in single-value context (x := pkg.F()) now always
type as Tuple(T₁…Tₙ), with no trailing-error fold to Result. Result
remains a Forst-native exclusive sum built via return and ensure;
bridge FFI with n, err := …; ensure !err or err; return n.

Add tuple indexed access (t.0, t.1), multi-value return for
Tuple-declared functions, and Unwrap() emission on nominal errors
with a cause field. Extract Go FFI call checking into
typechecker/gointerop with struct-based call descriptors. Add
errors-compat test corpus (stdlib + third-party module) and
task test:errors-compat.

BREAKING CHANGE: strconv.Atoi(s) and similar single-value Go
multi-return bindings now infer Tuple(Int, Error) instead of
Result(Int, Error). Use n-value assignment or the ensure bridge
to obtain Result ergonomics.

245 of 652 new or added lines in 20 files covered. (37.58%)

9 existing lines in 1 file now uncovered.

36169 of 47769 relevant lines covered (75.72%)

1160.36 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.76
/forst/internal/parser/function.go
1
package parser
2

3
import (
4
        "forst/internal/ast"
5
)
6

7
func (p *Parser) parseParameterType() ast.TypeNode {
7✔
8
        if p.current().Type == ast.TokenIdentifier && p.peek().Type == ast.TokenDot {
8✔
9
                if p.looksLikeAssertionTypeAfterIdent() {
2✔
10
                        assertion := p.parseAssertionChain(true)
1✔
11
                        return ast.TypeNode{
1✔
12
                                Ident:     ast.TypeAssertion,
1✔
13
                                Assertion: &assertion,
1✔
14
                        }
1✔
15
                }
1✔
16
                return p.parseType(TypeIdentOpts{AllowLowercaseTypes: false})
×
17
        }
18
        if p.peek().Type == ast.TokenLParen {
6✔
19
                assertion := p.parseAssertionChain(true)
×
20
                return ast.TypeNode{
×
21
                        Ident:     ast.TypeAssertion,
×
22
                        Assertion: &assertion,
×
23
                }
×
24
        }
×
25
        // Disallow Shape({...}) wrapper for shape types
26
        if p.current().Type == ast.TokenIdentifier && p.current().Value == "Shape" {
7✔
27
                ident := p.expect(ast.TokenIdentifier)
1✔
28
                if p.current().Type == ast.TokenLParen {
1✔
29
                        p.FailWithParseError(p.current(), "Shape({...}) wrapper is not allowed. Use {...} directly for shape types.")
×
30
                }
×
31
                return ast.TypeNode{
1✔
32
                        Ident: ast.TypeIdent(ident.Value),
1✔
33
                }
1✔
34
        }
35
        // Allow direct {...} for shape types
36
        if p.current().Type == ast.TokenLBrace {
9✔
37
                shape := p.parseShapeType()
4✔
38
                baseType := ast.TypeIdent(ast.TypeShape)
4✔
39
                return ast.TypeNode{
4✔
40
                        Ident: ast.TypeShape,
4✔
41
                        Assertion: &ast.AssertionNode{
4✔
42
                                BaseType: &baseType,
4✔
43
                                Constraints: []ast.ConstraintNode{{
4✔
44
                                        Name: "Match",
4✔
45
                                        Args: []ast.ConstraintArgumentNode{{
4✔
46
                                                Shape: &shape,
4✔
47
                                        }},
4✔
48
                                }},
4✔
49
                        },
4✔
50
                }
4✔
51
        }
4✔
52
        return p.parseType(TypeIdentOpts{AllowLowercaseTypes: false})
1✔
53
}
54

55
func (p *Parser) parseDestructuredParameter() ast.ParamNode {
3✔
56
        p.expect(ast.TokenLBrace)
3✔
57
        fields := []string{}
3✔
58

3✔
59
        // Parse fields until we hit closing brace
3✔
60
        for p.current().Type != ast.TokenRBrace {
9✔
61
                name := p.expect(ast.TokenIdentifier)
6✔
62
                fields = append(fields, name.Value)
6✔
63

6✔
64
                // Handle comma between fields
6✔
65
                if p.current().Type == ast.TokenComma {
9✔
66
                        p.advance()
3✔
67
                }
3✔
68
        }
69

70
        p.expect(ast.TokenRBrace)
3✔
71
        if p.current().Type == ast.TokenColon {
6✔
72
                p.advance()
3✔
73
        }
3✔
74

75
        paramType := p.parseParameterType()
3✔
76

3✔
77
        return ast.DestructuredParamNode{
3✔
78
                Fields: fields,
3✔
79
                Type:   paramType,
3✔
80
        }
3✔
81
}
82

83
func (p *Parser) parseSimpleParameter() ast.ParamNode {
109✔
84
        name := p.expect(ast.TokenIdentifier).Value
109✔
85
        // Go-style parameter declarations (name Type). Optional ':' before the type is accepted for legacy sources.
109✔
86
        if p.current().Type == ast.TokenColon {
109✔
87
                p.advance()
×
88
        }
×
89

90
        tok := p.current()
109✔
91
        if tok.Type == ast.TokenIdentifier && p.peek().Type == ast.TokenDot {
113✔
92
                if p.looksLikeAssertionTypeAfterIdent() {
7✔
93
                        assertion := p.parseAssertionChain(true)
3✔
94
                        return ast.SimpleParamNode{
3✔
95
                                Ident: ast.Ident{ID: ast.Identifier(name)},
3✔
96
                                Type: ast.TypeNode{
3✔
97
                                        Ident:     ast.TypeAssertion,
3✔
98
                                        Assertion: &assertion,
3✔
99
                                },
3✔
100
                        }
3✔
101
                }
3✔
102
                typ := p.parseType(TypeIdentOpts{AllowLowercaseTypes: false})
1✔
103
                return ast.SimpleParamNode{
1✔
104
                        Ident: ast.Ident{ID: ast.Identifier(name)},
1✔
105
                        Type:  typ,
1✔
106
                }
1✔
107
        }
108
        if tok.Type == ast.TokenIdentifier && p.peek().Type == ast.TokenLParen {
105✔
109
                assertion := p.parseAssertionChain(true)
×
110
                return ast.SimpleParamNode{
×
111
                        Ident: ast.Ident{ID: ast.Identifier(name)},
×
112
                        Type: ast.TypeNode{
×
113
                                Ident:     ast.TypeAssertion,
×
114
                                Assertion: &assertion,
×
115
                        },
×
116
                }
×
117
        }
×
118

119
        if tok.Type == ast.TokenIdentifier && tok.Value == "Shape" {
107✔
120
                // Check if this is Shape({...})
2✔
121
                if p.peek().Type == ast.TokenLParen {
2✔
122
                        p.FailWithParseError(tok, "Direct usage of Shape({...}) is not allowed. Use a shape type directly, e.g. { field: Type }.")
×
123
                }
×
124
                // Allow direct usage of Shape as a type name
125
                p.advance()
2✔
126
                typeIdent := ast.TypeIdent("Shape")
2✔
127
                return ast.SimpleParamNode{
2✔
128
                        Ident: ast.Ident{ID: ast.Identifier(name)},
2✔
129
                        Type:  ast.TypeNode{Ident: typeIdent},
2✔
130
                }
2✔
131
        }
132
        if tok.Type == ast.TokenLBrace {
105✔
133
                shape := p.parseShapeType()
2✔
134
                baseType := ast.TypeIdent(ast.TypeShape)
2✔
135
                return ast.SimpleParamNode{
2✔
136
                        Ident: ast.Ident{ID: ast.Identifier(name)},
2✔
137
                        Type: ast.TypeNode{
2✔
138
                                Ident: ast.TypeShape,
2✔
139
                                Assertion: &ast.AssertionNode{
2✔
140
                                        BaseType: &baseType,
2✔
141
                                        Constraints: []ast.ConstraintNode{{
2✔
142
                                                Name: "Match",
2✔
143
                                                Args: []ast.ConstraintArgumentNode{{
2✔
144
                                                        Shape: &shape,
2✔
145
                                                }},
2✔
146
                                        }},
2✔
147
                                },
2✔
148
                        },
2✔
149
                }
2✔
150
        }
2✔
151
        // Parse the type, which may include dots (e.g. AppMutation.Input)
152
        typ := p.parseType(TypeIdentOpts{AllowLowercaseTypes: false})
101✔
153
        p.logParsedNodeWithMessage(typ, "Parsed parameter type, next token: "+p.current().Type.String()+" ("+p.current().Value+")")
101✔
154
        return ast.SimpleParamNode{
101✔
155
                Ident: ast.Ident{ID: ast.Identifier(name)},
101✔
156
                Type:  typ,
101✔
157
        }
101✔
158
}
159

160
// looksLikeAssertionTypeAfterIdent reports Ident . Ident ( constraint calls, not pkg.Type refs.
161
func (p *Parser) looksLikeAssertionTypeAfterIdent() bool {
5✔
162
        if p.current().Type != ast.TokenIdentifier || p.peek().Type != ast.TokenDot {
5✔
163
                return false
×
164
        }
×
165
        return p.peek(2).Type == ast.TokenIdentifier && p.peek(3).Type == ast.TokenLParen
5✔
166
}
167

168
func (p *Parser) parseParameter() ast.ParamNode {
112✔
169
        switch p.current().Type {
112✔
170
        case ast.TokenIdentifier:
108✔
171
                return p.parseSimpleParameter()
108✔
172
        case ast.TokenLBrace:
3✔
173
                return p.parseDestructuredParameter()
3✔
174
        default:
1✔
175
                p.FailWithParseError(p.current(), "Expected parameter")
1✔
176
                panic("Reached unreachable path")
1✔
177
        }
178
}
179

180
// Parse function parameters
181
func (p *Parser) parseFunctionSignature() []ast.ParamNode {
217✔
182
        p.expect(ast.TokenLParen)
217✔
183
        params := []ast.ParamNode{}
217✔
184

217✔
185
        // Handle empty parameter list
217✔
186
        if p.current().Type == ast.TokenRParen {
363✔
187
                p.advance()
146✔
188
                return params
146✔
189
        }
146✔
190

191
        // Parse parameters
192
        for {
161✔
193
                param := p.parseParameter()
90✔
194
                switch param.(type) {
90✔
195
                case ast.DestructuredParamNode:
2✔
196
                        p.logParsedNodeWithMessage(param, "Parsed destructured function param")
2✔
197
                default:
88✔
198
                        p.logParsedNodeWithMessage(param, "Parsed function param")
88✔
199
                }
200
                params = append(params, param)
90✔
201

90✔
202
                // Check if there are more parameters
90✔
203
                if p.current().Type == ast.TokenComma {
109✔
204
                        p.advance()
19✔
205
                } else {
90✔
206
                        break
71✔
207
                }
208
        }
209

210
        p.expect(ast.TokenRParen)
71✔
211
        return params
71✔
212
}
213

214
func (p *Parser) parseReturnType() []ast.TypeNode {
221✔
215
        returnType := []ast.TypeNode{}
221✔
216
        if p.current().Type == ast.TokenColon {
281✔
217
                p.advance() // Consume the colon
60✔
218
                // Single return type only (use Result(T, Error) or Tuple(...) instead of Go-style (T, U))
60✔
219
                if p.current().Type == ast.TokenLParen {
61✔
220
                        p.FailWithParseError(p.current(), "multi-value return types are not supported; use Result(Success, Error) or Tuple(T1, ...)")
1✔
221
                }
1✔
222
                returnType = append(returnType, p.parseReturnTypeSingle())
59✔
223
        }
224
        return returnType
220✔
225
}
226

227
func (p *Parser) parseReturnTypeSingle() ast.TypeNode {
59✔
228
        // Special handling for shape types in return positions
59✔
229
        if p.current().Type == ast.TokenLBrace {
61✔
230
                shape := p.parseShapeType()
2✔
231
                baseType := ast.TypeIdent(ast.TypeShape)
2✔
232
                return ast.TypeNode{
2✔
233
                        Ident: ast.TypeShape,
2✔
234
                        Assertion: &ast.AssertionNode{
2✔
235
                                BaseType: &baseType,
2✔
236
                                Constraints: []ast.ConstraintNode{{
2✔
237
                                        Name: "Match",
2✔
238
                                        Args: []ast.ConstraintArgumentNode{{
2✔
239
                                                Shape: &shape,
2✔
240
                                        }},
2✔
241
                                }},
2✔
242
                        },
2✔
243
                }
2✔
244
        }
2✔
245
        return p.parseType(TypeIdentOpts{AllowLowercaseTypes: false})
57✔
246
}
247

248
func (p *Parser) parseReturnStatement() ast.ReturnNode {
90✔
249
        p.advance() // Move past `return`
90✔
250

90✔
251
        var values []ast.ExpressionNode
90✔
252
        if p.current().Type != ast.TokenSemicolon && p.current().Type != ast.TokenRBrace {
179✔
253
                for {
178✔
254
                        values = append(values, p.parseExpression())
89✔
255
                        if p.current().Type != ast.TokenComma {
177✔
256
                                break
88✔
257
                        }
NEW
258
                        p.advance()
×
259
                }
260
        }
261

262
        return ast.ReturnNode{
89✔
263
                Values: values,
89✔
264
                Type:   ast.TypeNode{Ident: ast.TypeImplicit},
89✔
265
        }
89✔
266
}
267

268
func (p *Parser) parseFunctionBody() []ast.Node {
216✔
269
        return p.parseBlock()
216✔
270
}
216✔
271

272
// parseFunctionName reads a function or method name. `error` is allowed when it starts a signature.
273
// `use` is allowed as a function name when followed by `(` (the `use` statement keyword uses a different lookahead).
274
func (p *Parser) parseFunctionName() ast.Token {
217✔
275
        tok := p.current()
217✔
276
        switch tok.Type {
217✔
277
        case ast.TokenIdentifier:
214✔
278
                p.advance()
214✔
279
                return tok
214✔
280
        case ast.TokenError:
2✔
281
                if p.peek().Type == ast.TokenLParen {
4✔
282
                        p.advance()
2✔
283
                        return tok
2✔
284
                }
2✔
285
                p.FailWithParseError(tok, "expected function name")
×
286
        case ast.TokenUse:
1✔
287
                if p.peek().Type == ast.TokenLParen {
2✔
288
                        p.advance()
1✔
289
                        return tok
1✔
290
                }
1✔
291
                p.FailWithParseError(tok, "expected function name")
×
292
        default:
×
293
                p.FailWithParseError(tok, "expected function name")
×
294
        }
295
        panic("unreachable")
×
296
}
297

298
// Parse a function definition
299
func (p *Parser) parseFunctionDefinition() ast.FunctionNode {
217✔
300
        p.expect(ast.TokenFunc) // Expect `func`
217✔
301

217✔
302
        var receiver *ast.SimpleParamNode
217✔
303
        if p.current().Type == ast.TokenLParen {
227✔
304
                receiver = p.parseReceiver()
10✔
305
        }
10✔
306

307
        name := p.parseFunctionName() // Function name
217✔
308

217✔
309
        p.context.ScopeStack.CurrentScope().FunctionName = name.Value
217✔
310

217✔
311
        params := p.parseFunctionSignature() // Parse function parameters
217✔
312

217✔
313
        returnType := p.parseReturnType()
217✔
314

217✔
315
        body := p.parseFunctionBody()
217✔
316

217✔
317
        node := ast.FunctionNode{
217✔
318
                Receiver:    receiver,
217✔
319
                Ident:       ast.Ident{ID: ast.Identifier(name.Value), Span: ast.SpanFromToken(name)},
217✔
320
                ReturnTypes: returnType,
217✔
321
                Params:      params,
217✔
322
                Body:        body,
217✔
323
        }
217✔
324

217✔
325
        return node
217✔
326
}
327

328
func (p *Parser) parseReceiver() *ast.SimpleParamNode {
10✔
329
        p.expect(ast.TokenLParen)
10✔
330

10✔
331
        firstTok := p.expect(ast.TokenIdentifier)
10✔
332
        var recvName ast.Identifier
10✔
333
        var recvType ast.TypeNode
10✔
334

10✔
335
        if p.current().Type == ast.TokenIdentifier || p.current().Type == ast.TokenStar ||
10✔
336
                p.current().Type == ast.TokenMap || p.current().Type == ast.TokenLBracket {
16✔
337
                // func (l StdLogger) or func (l *T)
6✔
338
                recvName = ast.Identifier(firstTok.Value)
6✔
339
                recvType = p.parseType(TypeIdentOpts{AllowLowercaseTypes: false})
6✔
340
        } else {
10✔
341
                // func (StdLogger) or func (NopLogger) — type only, no receiver name
4✔
342
                p.currentIndex--
4✔
343
                recvType = p.parseType(TypeIdentOpts{AllowLowercaseTypes: false})
4✔
344
        }
4✔
345

346
        p.expect(ast.TokenRParen)
10✔
347

10✔
348
        var ident ast.Ident
10✔
349
        if recvName != "" {
16✔
350
                ident = ast.Ident{ID: recvName, Span: ast.SpanFromToken(firstTok)}
6✔
351
        }
6✔
352

353
        return &ast.SimpleParamNode{
10✔
354
                Ident: ident,
10✔
355
                Type:  recvType,
10✔
356
        }
10✔
357
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc