Summary
Two engines' EvaluatorResponse.Type() implementations return wrong data.Types values for common results. Callers switching on Type() — the cross-engine contract — misbehave depending on which engine produced the result.
1. Extism: integer results report data.ERROR
engines/extism/evaluator/response.go:53:
case int32, int64, uint32, uint64:
return data.INT
The Extism decode path (evaluator.go:127-136 → internal.FixJSONNumberTypes → convertJSONNumber in engines/extism/internal/jsonHelpers.go:13-24) converts JSON integers to plain int, which is absent from the switch. A guest returning a bare JSON number falls to default and reports data.ERROR (and logs Unknown type ... type=int).
Repro (confirmed): guest output 42 → decoded Go int → Type() == data.ERROR. Existing tests cover only int32/int64/uint32/uint64, which the decode path never produces.
Fix: add case int: (and arguably int8, int16, uint, uint8, uint16) returning data.INT.
2. Risor: nil result reports "null", never data.NONE
engines/risor/evaluator/response.go:46-48:
func (r *execResult) Type() data.Types {
return data.Types(r.Object.Type()) // Risor's raw type string, passed through
}
Risor v2's nil type string is "null" (pkg/object.NIL), which is outside the data.Types enum (data.NONE == "none"). Starlark and Extism normalize nil to data.NONE; Risor does not.
Repro (confirmed): script nil → resp.Type() == data.NONE is false. Other Risor type strings ("byte", "time", "module") similarly leak through as non-enum values.
Fix: map "null" → data.NONE and normalize (or deliberately handle) the other non-enum Risor type strings instead of a blind cast.
Impact
Medium — data.Types is part of the public EvaluatorResponse contract and the mechanism for engine-agnostic result handling. Both bugs break it for ordinary values (integers, nil).
Fix
Add the missing int cases to the Extism switch; replace the Risor blind cast with an explicit mapping (at minimum "null" → data.NONE). Add per-engine tests for integer and nil results, and a cross-engine test asserting the same conceptual result yields identical Type().
Summary
Two engines'
EvaluatorResponse.Type()implementations return wrongdata.Typesvalues for common results. Callers switching onType()— the cross-engine contract — misbehave depending on which engine produced the result.1. Extism: integer results report
data.ERRORengines/extism/evaluator/response.go:53:The Extism decode path (
evaluator.go:127-136→internal.FixJSONNumberTypes→convertJSONNumberinengines/extism/internal/jsonHelpers.go:13-24) converts JSON integers to plainint, which is absent from the switch. A guest returning a bare JSON number falls todefaultand reportsdata.ERROR(and logsUnknown type ... type=int).Repro (confirmed): guest output
42→ decoded Goint→Type() == data.ERROR. Existing tests cover onlyint32/int64/uint32/uint64, which the decode path never produces.Fix: add
case int:(and arguablyint8, int16, uint, uint8, uint16) returningdata.INT.2. Risor: nil result reports
"null", neverdata.NONEengines/risor/evaluator/response.go:46-48:Risor v2's nil type string is
"null"(pkg/object.NIL), which is outside thedata.Typesenum (data.NONE == "none"). Starlark and Extism normalize nil todata.NONE; Risor does not.Repro (confirmed): script
nil→resp.Type() == data.NONEis false. Other Risor type strings ("byte","time","module") similarly leak through as non-enum values.Fix: map
"null"→data.NONEand normalize (or deliberately handle) the other non-enum Risor type strings instead of a blind cast.Impact
Medium —
data.Typesis part of the publicEvaluatorResponsecontract and the mechanism for engine-agnostic result handling. Both bugs break it for ordinary values (integers, nil).Fix
Add the missing
intcases to the Extism switch; replace the Risor blind cast with an explicit mapping (at minimum"null"→data.NONE). Add per-engine tests for integer and nil results, and a cross-engine test asserting the same conceptual result yields identicalType().