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

forst-lang / forst / 28306106493

28 Jun 2026 12:15AM UTC coverage: 74.702% (-1.6%) from 76.292%
28306106493

push

github

web-flow
feat!: Providers (`use` / `with`) (#97)

2576 of 4131 new or added lines in 87 files covered. (62.36%)

28 existing lines in 6 files now uncovered.

23821 of 31888 relevant lines covered (74.7%)

88.82 hits per line

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

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

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

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

47
func (p *Parser) parseDestructuredParameter() ast.ParamNode {
1✔
48
        p.expect(ast.TokenLBrace)
1✔
49
        fields := []string{}
1✔
50

1✔
51
        // Parse fields until we hit closing brace
1✔
52
        for p.current().Type != ast.TokenRBrace {
3✔
53
                name := p.expect(ast.TokenIdentifier)
2✔
54
                fields = append(fields, name.Value)
2✔
55

2✔
56
                // Handle comma between fields
2✔
57
                if p.current().Type == ast.TokenComma {
3✔
58
                        p.advance()
1✔
59
                }
1✔
60
        }
61

62
        p.expect(ast.TokenRBrace)
1✔
63
        if p.current().Type == ast.TokenColon {
2✔
64
                p.advance()
1✔
65
        }
1✔
66

67
        paramType := p.parseParameterType()
1✔
68

1✔
69
        return ast.DestructuredParamNode{
1✔
70
                Fields: fields,
1✔
71
                Type:   paramType,
1✔
72
        }
1✔
73
}
74

75
func (p *Parser) parseSimpleParameter() ast.ParamNode {
40✔
76
        name := p.expect(ast.TokenIdentifier).Value
40✔
77
        // Go-style parameter declarations (name Type). Optional ':' before the type is accepted for legacy sources.
40✔
78
        if p.current().Type == ast.TokenColon {
40✔
79
                p.advance()
×
80
        }
×
81

82
        tok := p.current()
40✔
83
        if tok.Type == ast.TokenIdentifier && (p.peek().Type == ast.TokenDot || p.peek().Type == ast.TokenLParen) {
42✔
84
                assertion := p.parseAssertionChain(true)
2✔
85
                return ast.SimpleParamNode{
2✔
86
                        Ident: ast.Ident{ID: ast.Identifier(name)},
2✔
87
                        Type: ast.TypeNode{
2✔
88
                                Ident:     ast.TypeAssertion,
2✔
89
                                Assertion: &assertion,
2✔
90
                        },
2✔
91
                }
2✔
92
        }
2✔
93

94
        if tok.Type == ast.TokenIdentifier && tok.Value == "Shape" {
40✔
95
                // Check if this is Shape({...})
2✔
96
                if p.peek().Type == ast.TokenLParen {
2✔
97
                        p.FailWithParseError(tok, "Direct usage of Shape({...}) is not allowed. Use a shape type directly, e.g. { field: Type }.")
×
98
                }
×
99
                // Allow direct usage of Shape as a type name
100
                p.advance()
2✔
101
                typeIdent := ast.TypeIdent("Shape")
2✔
102
                return ast.SimpleParamNode{
2✔
103
                        Ident: ast.Ident{ID: ast.Identifier(name)},
2✔
104
                        Type:  ast.TypeNode{Ident: typeIdent},
2✔
105
                }
2✔
106
        }
107
        if tok.Type == ast.TokenLBrace {
37✔
108
                shape := p.parseShapeType()
1✔
109
                baseType := ast.TypeIdent(ast.TypeShape)
1✔
110
                return ast.SimpleParamNode{
1✔
111
                        Ident: ast.Ident{ID: ast.Identifier(name)},
1✔
112
                        Type: ast.TypeNode{
1✔
113
                                Ident: ast.TypeShape,
1✔
114
                                Assertion: &ast.AssertionNode{
1✔
115
                                        BaseType: &baseType,
1✔
116
                                        Constraints: []ast.ConstraintNode{{
1✔
117
                                                Name: "Match",
1✔
118
                                                Args: []ast.ConstraintArgumentNode{{
1✔
119
                                                        Shape: &shape,
1✔
120
                                                }},
1✔
121
                                        }},
1✔
122
                                },
1✔
123
                        },
1✔
124
                }
1✔
125
        }
1✔
126
        // Parse the type, which may include dots (e.g. AppMutation.Input)
127
        typ := p.parseType(TypeIdentOpts{AllowLowercaseTypes: false})
35✔
128
        p.logParsedNodeWithMessage(typ, "Parsed parameter type, next token: "+p.current().Type.String()+" ("+p.current().Value+")")
35✔
129
        return ast.SimpleParamNode{
35✔
130
                Ident: ast.Ident{ID: ast.Identifier(name)},
35✔
131
                Type:  typ,
35✔
132
        }
35✔
133
}
134

135
func (p *Parser) parseParameter() ast.ParamNode {
41✔
136
        switch p.current().Type {
41✔
137
        case ast.TokenIdentifier:
40✔
138
                return p.parseSimpleParameter()
40✔
139
        case ast.TokenLBrace:
1✔
140
                return p.parseDestructuredParameter()
1✔
141
        default:
×
142
                p.FailWithParseError(p.current(), "Expected parameter")
×
143
                panic("Reached unreachable path")
×
144
        }
145
}
146

147
// Parse function parameters
148
func (p *Parser) parseFunctionSignature() []ast.ParamNode {
110✔
149
        p.expect(ast.TokenLParen)
110✔
150
        params := []ast.ParamNode{}
110✔
151

110✔
152
        // Handle empty parameter list
110✔
153
        if p.current().Type == ast.TokenRParen {
192✔
154
                p.advance()
82✔
155
                return params
82✔
156
        }
82✔
157

158
        // Parse parameters
159
        for {
58✔
160
                param := p.parseParameter()
30✔
161
                switch param.(type) {
30✔
162
                case ast.DestructuredParamNode:
1✔
163
                        p.logParsedNodeWithMessage(param, "Parsed destructured function param")
1✔
164
                default:
29✔
165
                        p.logParsedNodeWithMessage(param, "Parsed function param")
29✔
166
                }
167
                params = append(params, param)
30✔
168

30✔
169
                // Check if there are more parameters
30✔
170
                if p.current().Type == ast.TokenComma {
32✔
171
                        p.advance()
2✔
172
                } else {
30✔
173
                        break
28✔
174
                }
175
        }
176

177
        p.expect(ast.TokenRParen)
28✔
178
        return params
28✔
179
}
180

181
func (p *Parser) parseReturnType() []ast.TypeNode {
111✔
182
        returnType := []ast.TypeNode{}
111✔
183
        if p.current().Type == ast.TokenColon {
131✔
184
                p.advance() // Consume the colon
20✔
185
                // Single return type only (use Result(T, Error) or Tuple(...) instead of Go-style (T, U))
20✔
186
                if p.current().Type == ast.TokenLParen {
20✔
187
                        p.FailWithParseError(p.current(), "multi-value return types are not supported; use Result(Success, Error) or Tuple(T1, ...)")
×
188
                }
×
189
                returnType = append(returnType, p.parseReturnTypeSingle())
20✔
190
        }
191
        return returnType
111✔
192
}
193

194
func (p *Parser) parseReturnTypeSingle() ast.TypeNode {
20✔
195
        // Special handling for shape types in return positions
20✔
196
        if p.current().Type == ast.TokenLBrace {
21✔
197
                p.parseShapeType() // Parse the shape but don't use it for return type
1✔
198
                // For return types, create a simple shape type instead of an assertion
1✔
199
                return ast.TypeNode{
1✔
200
                        Ident: ast.TypeShape,
1✔
201
                }
1✔
202
        }
1✔
203
        return p.parseType(TypeIdentOpts{AllowLowercaseTypes: false})
19✔
204
}
205

206
func (p *Parser) parseReturnStatement() ast.ReturnNode {
36✔
207
        p.advance() // Move past `return`
36✔
208

36✔
209
        var values []ast.ExpressionNode
36✔
210
        if p.current().Type != ast.TokenSemicolon && p.current().Type != ast.TokenRBrace {
72✔
211
                values = append(values, p.parseExpression())
36✔
212
                if p.current().Type == ast.TokenComma {
36✔
213
                        p.FailWithParseError(p.current(), "multiple return values are not supported; use a single Result(S, F) success value or delegate to a Result-returning call")
×
214
                }
×
215
        }
216

217
        return ast.ReturnNode{
36✔
218
                Values: values,
36✔
219
                Type:   ast.TypeNode{Ident: ast.TypeImplicit},
36✔
220
        }
36✔
221
}
222

223
func (p *Parser) parseFunctionBody() []ast.Node {
110✔
224
        return p.parseBlock()
110✔
225
}
110✔
226

227
// parseFunctionName reads a function or method name. `error` is allowed when it starts a signature.
228
// `use` is allowed as a function name when followed by `(` (the `use` statement keyword uses a different lookahead).
229
func (p *Parser) parseFunctionName() ast.Token {
110✔
230
        tok := p.current()
110✔
231
        switch tok.Type {
110✔
232
        case ast.TokenIdentifier:
109✔
233
                p.advance()
109✔
234
                return tok
109✔
235
        case ast.TokenError:
1✔
236
                if p.peek().Type == ast.TokenLParen {
2✔
237
                        p.advance()
1✔
238
                        return tok
1✔
239
                }
1✔
NEW
240
                p.FailWithParseError(tok, "expected function name")
×
NEW
241
        case ast.TokenUse:
×
NEW
242
                if p.peek().Type == ast.TokenLParen {
×
NEW
243
                        p.advance()
×
NEW
244
                        return tok
×
NEW
245
                }
×
NEW
246
                p.FailWithParseError(tok, "expected function name")
×
NEW
247
        default:
×
NEW
248
                p.FailWithParseError(tok, "expected function name")
×
249
        }
NEW
250
        panic("unreachable")
×
251
}
252

253
// Parse a function definition
254
func (p *Parser) parseFunctionDefinition() ast.FunctionNode {
110✔
255
        p.expect(ast.TokenFunc) // Expect `func`
110✔
256

110✔
257
        var receiver *ast.SimpleParamNode
110✔
258
        if p.current().Type == ast.TokenLParen {
115✔
259
                receiver = p.parseReceiver()
5✔
260
        }
5✔
261

262
        name := p.parseFunctionName() // Function name
110✔
263

110✔
264
        p.context.ScopeStack.CurrentScope().FunctionName = name.Value
110✔
265

110✔
266
        params := p.parseFunctionSignature() // Parse function parameters
110✔
267

110✔
268
        returnType := p.parseReturnType()
110✔
269

110✔
270
        body := p.parseFunctionBody()
110✔
271

110✔
272
        node := ast.FunctionNode{
110✔
273
                Receiver:    receiver,
110✔
274
                Ident:       ast.Ident{ID: ast.Identifier(name.Value), Span: ast.SpanFromToken(name)},
110✔
275
                ReturnTypes: returnType,
110✔
276
                Params:      params,
110✔
277
                Body:        body,
110✔
278
        }
110✔
279

110✔
280
        return node
110✔
281
}
282

283
func (p *Parser) parseReceiver() *ast.SimpleParamNode {
5✔
284
        p.expect(ast.TokenLParen)
5✔
285

5✔
286
        firstTok := p.expect(ast.TokenIdentifier)
5✔
287
        var recvName ast.Identifier
5✔
288
        var recvType ast.TypeNode
5✔
289

5✔
290
        if p.current().Type == ast.TokenIdentifier || p.current().Type == ast.TokenStar ||
5✔
291
                p.current().Type == ast.TokenMap || p.current().Type == ast.TokenLBracket {
8✔
292
                // func (l StdLogger) or func (l *T)
3✔
293
                recvName = ast.Identifier(firstTok.Value)
3✔
294
                recvType = p.parseType(TypeIdentOpts{AllowLowercaseTypes: false})
3✔
295
        } else {
5✔
296
                // func (StdLogger) or func (NopLogger) — type only, no receiver name
2✔
297
                p.currentIndex--
2✔
298
                recvType = p.parseType(TypeIdentOpts{AllowLowercaseTypes: false})
2✔
299
        }
2✔
300

301
        p.expect(ast.TokenRParen)
5✔
302

5✔
303
        var ident ast.Ident
5✔
304
        if recvName != "" {
8✔
305
                ident = ast.Ident{ID: recvName, Span: ast.SpanFromToken(firstTok)}
3✔
306
        }
3✔
307

308
        return &ast.SimpleParamNode{
5✔
309
                Ident: ident,
5✔
310
                Type:  recvType,
5✔
311
        }
5✔
312
}
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