Skip to content

Commit 016b948

Browse files
committed
Reject ambiguous enum type matches
1 parent 13befcf commit 016b948

2 files changed

Lines changed: 108 additions & 18 deletions

File tree

vibes/enum_test.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,10 @@ func TestLookupEnumInEnvSkipsNonEnumShadowBindings(t *testing.T) {
215215
shadow := newEnv(root)
216216
shadow.Define("Status", NewString("shadow"))
217217

218-
got, ok := lookupEnumInEnv(shadow, "Status")
218+
got, ok, err := lookupEnumInEnv(shadow, "Status")
219+
if err != nil {
220+
t.Fatalf("lookup enum: %v", err)
221+
}
219222
if !ok {
220223
t.Fatalf("expected lookup to resolve parent enum")
221224
}
@@ -224,6 +227,36 @@ func TestLookupEnumInEnvSkipsNonEnumShadowBindings(t *testing.T) {
224227
}
225228
}
226229

230+
func TestLookupEnumInEnvRejectsAmbiguousCaseInsensitiveMatches(t *testing.T) {
231+
statusDef, err := compileEnumDef(&EnumStmt{
232+
Name: "Status",
233+
Members: []EnumMemberStmt{
234+
{Name: "Draft"},
235+
},
236+
})
237+
if err != nil {
238+
t.Fatalf("compile enum: %v", err)
239+
}
240+
statusUpperDef, err := compileEnumDef(&EnumStmt{
241+
Name: "STATUS",
242+
Members: []EnumMemberStmt{
243+
{Name: "Draft"},
244+
},
245+
})
246+
if err != nil {
247+
t.Fatalf("compile enum: %v", err)
248+
}
249+
250+
env := newEnv(nil)
251+
env.Define("Status", NewEnum(statusDef))
252+
env.Define("STATUS", NewEnum(statusUpperDef))
253+
254+
_, _, err = lookupEnumInEnv(env, "status")
255+
if err == nil || err.Error() != "ambiguous enum type status matches STATUS, Status" {
256+
t.Fatalf("expected deterministic ambiguity error, got %v", err)
257+
}
258+
}
259+
227260
func TestEnumTypeAnnotationsResolveCaseInsensitive(t *testing.T) {
228261
script := compileScript(t, `
229262
enum Status
@@ -254,6 +287,24 @@ end
254287
compareArrays(t, owners, []Value{NewEnum(script.enums["Status"])})
255288
}
256289

290+
func TestEnumTypeAnnotationsRejectAmbiguousCaseInsensitiveMatches(t *testing.T) {
291+
script := compileScript(t, `
292+
enum Status
293+
Draft
294+
end
295+
296+
enum STATUS
297+
Draft
298+
end
299+
300+
def echo(status: status) -> status
301+
status
302+
end
303+
`)
304+
305+
requireCallErrorContains(t, script, "echo", []Value{NewSymbol("draft")}, CallOptions{}, "ambiguous enum type status matches STATUS, Status")
306+
}
307+
257308
func TestEnumModuleExportsAndTypedCalls(t *testing.T) {
258309
engine := moduleTestEngine(t)
259310
script := compileScriptWithEngine(t, engine, `def run()

vibes/execution_types_normalize.go

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package vibes
33
import (
44
"fmt"
55
"reflect"
6+
"sort"
67
"strings"
78
)
89

@@ -257,55 +258,93 @@ func resolveEnumType(ty *TypeExpr, ctx typeContext) (*EnumDef, error) {
257258
if ty.Kind != TypeEnum {
258259
return nil, fmt.Errorf("unknown type %s", ty.Name)
259260
}
260-
if enumDef, ok := lookupEnumDef(ctx.owner, ty.Name); ok {
261+
if enumDef, ok, err := lookupEnumDef(ctx.owner, ty.Name); err != nil {
262+
return nil, err
263+
} else if ok {
261264
return enumDef, nil
262265
}
263-
if enumDef, ok := lookupEnumInEnv(ctx.env, ty.Name); ok {
266+
if enumDef, ok, err := lookupEnumInEnv(ctx.env, ty.Name); err != nil {
267+
return nil, err
268+
} else if ok {
264269
return enumDef, nil
265270
}
266271
if ctx.fallback != ctx.env {
267-
if enumDef, ok := lookupEnumInEnv(ctx.fallback, ty.Name); ok {
272+
if enumDef, ok, err := lookupEnumInEnv(ctx.fallback, ty.Name); err != nil {
273+
return nil, err
274+
} else if ok {
268275
return enumDef, nil
269276
}
270277
}
271278
return nil, fmt.Errorf("unknown type %s", ty.Name)
272279
}
273280

274-
func lookupEnumDef(owner *Script, name string) (*EnumDef, bool) {
281+
func lookupEnumDef(owner *Script, name string) (*EnumDef, bool, error) {
275282
if owner == nil || len(owner.enums) == 0 {
276-
return nil, false
283+
return nil, false, nil
277284
}
278285
if enumDef, ok := owner.enums[name]; ok {
279-
return enumDef, true
286+
return enumDef, true, nil
280287
}
288+
var match *EnumDef
289+
matches := make([]string, 0, 2)
281290
for enumName, enumDef := range owner.enums {
282-
if strings.EqualFold(enumName, name) {
283-
return enumDef, true
291+
if !strings.EqualFold(enumName, name) {
292+
continue
293+
}
294+
matches = append(matches, enumName)
295+
if match == nil {
296+
match = enumDef
297+
continue
284298
}
299+
if match != enumDef {
300+
return nil, false, ambiguousEnumTypeError(name, matches)
301+
}
302+
}
303+
if match != nil {
304+
return match, true, nil
285305
}
286-
return nil, false
306+
return nil, false, nil
287307
}
288308

289-
func lookupEnumInEnv(env *Env, name string) (*EnumDef, bool) {
309+
func lookupEnumInEnv(env *Env, name string) (*EnumDef, bool, error) {
290310
for scope := env; scope != nil; scope = scope.parent {
291-
if enumDef, ok := lookupEnumValue(scope.values, name); ok {
292-
return enumDef, true
311+
if enumDef, ok, err := lookupEnumValue(scope.values, name); err != nil {
312+
return nil, false, err
313+
} else if ok {
314+
return enumDef, true, nil
293315
}
294316
}
295-
return nil, false
317+
return nil, false, nil
296318
}
297319

298-
func lookupEnumValue(values map[string]Value, name string) (*EnumDef, bool) {
320+
func lookupEnumValue(values map[string]Value, name string) (*EnumDef, bool, error) {
299321
if val, ok := values[name]; ok && val.Kind() == KindEnum {
300-
return val.Enum(), true
322+
return val.Enum(), true, nil
301323
}
324+
var match *EnumDef
325+
matches := make([]string, 0, 2)
302326
for key, val := range values {
303327
if key == name || !strings.EqualFold(key, name) || val.Kind() != KindEnum {
304328
continue
305329
}
306-
return val.Enum(), true
330+
matches = append(matches, key)
331+
if match == nil {
332+
match = val.Enum()
333+
continue
334+
}
335+
if match != val.Enum() {
336+
return nil, false, ambiguousEnumTypeError(name, matches)
337+
}
338+
}
339+
if match != nil {
340+
return match, true, nil
307341
}
308-
return nil, false
342+
return nil, false, nil
343+
}
344+
345+
func ambiguousEnumTypeError(name string, matches []string) error {
346+
sort.Strings(matches)
347+
return fmt.Errorf("ambiguous enum type %s matches %s", name, strings.Join(matches, ", "))
309348
}
310349

311350
func errorAsTypeMismatch(err error, target **typeMismatchError) bool {

0 commit comments

Comments
 (0)