Skip to content

Commit a30ca1a

Browse files
authored
feat: add semantic recorder for source-level metadata capture during compilation (#993)
* feat: add semantic recorder for source-level metadata capture during compilation * feat: add ReferenceAt method and related test cases to enhance semantic analysis
1 parent 34d35d9 commit a30ca1a

29 files changed

Lines changed: 2801 additions & 135 deletions

pkg/compiler/analysis.go

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
package compiler
2+
3+
import (
4+
"sort"
5+
6+
"github.com/MontFerret/ferret/v2/pkg/diagnostics"
7+
"github.com/MontFerret/ferret/v2/pkg/source"
8+
)
9+
10+
// Analysis is an immutable semantic snapshot produced by Compiler.Analyze.
11+
// Its accessors return defensive copies.
12+
type Analysis struct {
13+
data analysisData
14+
}
15+
16+
func newAnalysis(data analysisData) *Analysis {
17+
return &Analysis{data: data}
18+
}
19+
20+
// Symbols returns all source-visible symbols in deterministic analysis-local ID order.
21+
func (a *Analysis) Symbols() []Symbol {
22+
if a == nil {
23+
return nil
24+
}
25+
26+
return append([]Symbol(nil), a.data.symbols...)
27+
}
28+
29+
// Symbol returns the symbol with the analysis-local ID.
30+
func (a *Analysis) Symbol(id SymbolID) (Symbol, bool) {
31+
if a == nil || id == 0 || int(id) > len(a.data.symbols) {
32+
return Symbol{}, false
33+
}
34+
35+
return a.data.symbols[int(id)-1], true
36+
}
37+
38+
// References returns all resolved references in source order.
39+
func (a *Analysis) References() []Reference {
40+
if a == nil {
41+
return nil
42+
}
43+
44+
return append([]Reference(nil), a.data.references...)
45+
}
46+
47+
// ReferencesTo returns references resolved to symbol in source order.
48+
func (a *Analysis) ReferencesTo(symbol SymbolID) []Reference {
49+
if a == nil || symbol == 0 {
50+
return nil
51+
}
52+
53+
out := make([]Reference, 0)
54+
for _, reference := range a.data.references {
55+
if reference.Symbol == symbol {
56+
out = append(out, reference)
57+
}
58+
}
59+
60+
return out
61+
}
62+
63+
// Calls returns all resolved calls in source order.
64+
func (a *Analysis) Calls() []Call {
65+
if a == nil {
66+
return nil
67+
}
68+
69+
out := make([]Call, len(a.data.calls))
70+
for i := range a.data.calls {
71+
out[i] = cloneCall(a.data.calls[i])
72+
}
73+
74+
return out
75+
}
76+
77+
// Diagnostics returns deep copies of all source diagnostics in compiler order.
78+
func (a *Analysis) Diagnostics() []*diagnostics.Diagnostic {
79+
if a == nil {
80+
return nil
81+
}
82+
83+
return cloneDiagnostics(a.data.diagnostics)
84+
}
85+
86+
// TypeFacts returns all compiler-established expression facts in source order.
87+
func (a *Analysis) TypeFacts() []TypeFact {
88+
if a == nil {
89+
return nil
90+
}
91+
92+
return append([]TypeFact(nil), a.data.typeFacts...)
93+
}
94+
95+
// FunctionParameters returns the declared parameters for a UDF symbol.
96+
func (a *Analysis) FunctionParameters(function SymbolID) []Symbol {
97+
if a == nil || function == 0 {
98+
return nil
99+
}
100+
101+
out := make([]Symbol, 0)
102+
for i, symbol := range a.data.symbols {
103+
if symbol.Kind == SymbolKindFunctionParameter && a.data.symbolMetadata[i].Function == function {
104+
out = append(out, symbol)
105+
}
106+
}
107+
108+
return out
109+
}
110+
111+
// SymbolAt returns the symbol whose declaration selection or reference contains offset.
112+
// The offset is a zero-based UTF-8 byte offset into the analyzed source.
113+
func (a *Analysis) SymbolAt(offset int) (Symbol, bool) {
114+
if !a.validOffset(offset) {
115+
return Symbol{}, false
116+
}
117+
118+
best := source.Span{}
119+
var found SymbolID
120+
121+
for _, symbol := range a.data.symbols {
122+
if spanContains(symbol.SelectionSpan, offset) && narrowerSpan(symbol.SelectionSpan, best) {
123+
found = symbol.ID
124+
best = symbol.SelectionSpan
125+
}
126+
}
127+
128+
for _, reference := range a.data.references {
129+
if spanContains(reference.Span, offset) && narrowerSpan(reference.Span, best) {
130+
found = reference.Symbol
131+
best = reference.Span
132+
}
133+
}
134+
135+
return a.Symbol(found)
136+
}
137+
138+
// ReferenceAt returns the narrowest semantic reference containing offset.
139+
// Declaration selection spans are not references. The offset is a zero-based
140+
// UTF-8 byte offset into the analyzed source.
141+
func (a *Analysis) ReferenceAt(offset int) (Reference, bool) {
142+
if !a.validOffset(offset) {
143+
return Reference{}, false
144+
}
145+
146+
best := source.Span{}
147+
var found Reference
148+
ok := false
149+
150+
for _, reference := range a.data.references {
151+
if spanContains(reference.Span, offset) && narrowerSpan(reference.Span, best) {
152+
found = reference
153+
best = reference.Span
154+
ok = true
155+
}
156+
}
157+
158+
return found, ok
159+
}
160+
161+
// CallAt returns the narrowest call whose complete Call.Span contains offset.
162+
// The offset may be inside the callee or any argument. Consumers that need to
163+
// detect the callee specifically should test the offset against Call.CalleeSpan.
164+
// The offset is a zero-based UTF-8 byte offset into the analyzed source.
165+
func (a *Analysis) CallAt(offset int) (Call, bool) {
166+
if !a.validOffset(offset) {
167+
return Call{}, false
168+
}
169+
170+
best := source.Span{}
171+
var found *Call
172+
173+
for i := range a.data.calls {
174+
call := &a.data.calls[i]
175+
if spanContains(call.Span, offset) && narrowerSpan(call.Span, best) {
176+
found = call
177+
best = call.Span
178+
}
179+
}
180+
181+
if found == nil {
182+
return Call{}, false
183+
}
184+
185+
return cloneCall(*found), true
186+
}
187+
188+
// TypeAt returns the narrowest expression type fact containing offset.
189+
// The offset is a zero-based UTF-8 byte offset into the analyzed source.
190+
func (a *Analysis) TypeAt(offset int) (TypeFact, bool) {
191+
if !a.validOffset(offset) {
192+
return TypeFact{}, false
193+
}
194+
195+
best := source.Span{}
196+
var found TypeFact
197+
ok := false
198+
199+
for _, fact := range a.data.typeFacts {
200+
if spanContains(fact.Span, offset) && narrowerSpan(fact.Span, best) {
201+
found = fact
202+
best = fact.Span
203+
ok = true
204+
}
205+
}
206+
207+
return found, ok
208+
}
209+
210+
// VisibleSymbols returns source-visible symbols active at offset after lexical shadowing.
211+
// The offset is a zero-based UTF-8 byte offset into the analyzed source.
212+
func (a *Analysis) VisibleSymbols(offset int) []Symbol {
213+
if !a.validOffset(offset) {
214+
return nil
215+
}
216+
217+
type candidate struct {
218+
symbol Symbol
219+
depth int
220+
}
221+
222+
candidates := make([]candidate, 0)
223+
for i, symbol := range a.data.symbols {
224+
metadata := a.data.symbolMetadata[i]
225+
if offset < metadata.Activation || !a.scopeContains(metadata.Scope, offset) {
226+
continue
227+
}
228+
229+
candidates = append(candidates, candidate{symbol: symbol, depth: a.scope(metadata.Scope).Depth})
230+
}
231+
232+
sort.SliceStable(candidates, func(i, j int) bool {
233+
if candidates[i].depth != candidates[j].depth {
234+
return candidates[i].depth > candidates[j].depth
235+
}
236+
237+
return candidates[i].symbol.ID > candidates[j].symbol.ID
238+
})
239+
240+
seen := make(map[string]struct{}, len(candidates))
241+
out := make([]Symbol, 0, len(candidates))
242+
for _, candidate := range candidates {
243+
key := semanticNamespace(candidate.symbol.Kind) + "\x00" + candidate.symbol.Name
244+
if _, ok := seen[key]; ok {
245+
continue
246+
}
247+
248+
seen[key] = struct{}{}
249+
out = append(out, candidate.symbol)
250+
}
251+
252+
sort.Slice(out, func(i, j int) bool {
253+
return out[i].ID < out[j].ID
254+
})
255+
256+
return out
257+
}
258+
259+
func (a *Analysis) validOffset(offset int) bool {
260+
return a != nil && offset >= 0 && offset <= a.data.sourceLength
261+
}
262+
263+
func (a *Analysis) scope(id analysisScopeID) analysisScope {
264+
if a == nil || id == 0 || int(id) > len(a.data.scopes) {
265+
return analysisScope{}
266+
}
267+
268+
return a.data.scopes[int(id)-1]
269+
}
270+
271+
func (a *Analysis) scopeContains(id analysisScopeID, offset int) bool {
272+
scope := a.scope(id)
273+
if scope.ID == 0 {
274+
return false
275+
}
276+
277+
if scope.Parent == 0 && offset == a.data.sourceLength {
278+
return true
279+
}
280+
281+
return spanContains(scope.Span, offset)
282+
}

0 commit comments

Comments
 (0)