1use super::{scope::TypeScope, TypeChecker};
16use crate::ast::*;
17use crate::builtin_signatures::TyExt;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum PredicateQuestionKind {
22 Boolean,
23 Choice,
24 Score,
25}
26
27impl PredicateQuestionKind {
28 pub fn as_str(self) -> &'static str {
29 match self {
30 Self::Boolean => "boolean",
31 Self::Choice => "choice",
32 Self::Score => "score",
33 }
34 }
35
36 fn from_literal(kind: &str) -> Option<Self> {
37 match kind {
38 "boolean" => Some(Self::Boolean),
39 "choice" => Some(Self::Choice),
40 "score" => Some(Self::Score),
41 _ => None,
42 }
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
49pub struct PredicateQuestionSpec {
50 pub id: String,
51 pub kind: PredicateQuestionKind,
52 pub instructions: String,
53 pub labels: Vec<String>,
54}
55
56impl PredicateQuestionSpec {
57 pub fn answer_type(&self) -> TypeExpr {
60 let (contract, label_field) = match self.kind {
61 PredicateQuestionKind::Boolean => {
62 return harn_builtin_meta::predicate::BOOLEAN_ANSWER.to_type_expr()
63 }
64 PredicateQuestionKind::Choice => {
65 (harn_builtin_meta::predicate::CHOICE_ANSWER, "choice")
66 }
67 PredicateQuestionKind::Score => (harn_builtin_meta::predicate::SCORE_ANSWER, "level"),
68 };
69 let mut answer = contract.to_type_expr();
70 if self.labels.is_empty() {
71 return answer;
72 }
73 let labels: Vec<TypeExpr> = self
74 .labels
75 .iter()
76 .map(|label| TypeExpr::LitString(label.clone()))
77 .collect();
78 let narrowed = if labels.len() == 1 {
79 labels.into_iter().next().expect("one label")
80 } else {
81 TypeExpr::Union(labels)
82 };
83 if let TypeExpr::Shape(fields) = &mut answer {
84 if let Some(field) = fields.iter_mut().find(|field| field.name == label_field) {
85 field.type_expr = narrowed;
86 }
87 }
88 answer
89 }
90}
91
92pub(super) enum QuestionSetError {
95 NotLiteral,
96 NoQuestions,
97 DuplicateId(String),
98 EntryNotAQuestion(String),
99 LabelsNotLiteral(String),
100 NoLabels(String),
101 DuplicateLabel(String, String),
102 InstructionsNotLiteral(String),
103}
104
105impl QuestionSetError {
106 pub(super) fn message(&self) -> String {
107 match self {
108 Self::NotLiteral => {
109 "evaluation questions must be a dict literal of question builders".into()
110 }
111 Self::NoQuestions => "evaluation declares no questions".into(),
112 Self::DuplicateId(id) => format!("question id `{id}` is declared more than once"),
113 Self::EntryNotAQuestion(id) => format!(
114 "question `{id}` is not a boolean, choice, or score question from std/predicate"
115 ),
116 Self::LabelsNotLiteral(id) => format!(
117 "question `{id}` must declare its criteria or levels as a literal so answers can be typed"
118 ),
119 Self::NoLabels(id) => format!("question `{id}` declares no criteria or levels"),
120 Self::DuplicateLabel(id, label) => {
121 format!("question `{id}` declares label `{label}` more than once")
122 }
123 Self::InstructionsNotLiteral(id) => {
124 format!("question `{id}` must declare its instructions as a nonempty string literal")
125 }
126 }
127 }
128
129 pub(super) fn help(&self) -> String {
130 match self {
131 Self::EntryNotAQuestion(_) | Self::NotLiteral => {
132 "build each question with boolean(...), choice(...), or score(...) from std/predicate, inline at the call".into()
133 }
134 _ => "the question set is part of the site's cache identity and types every answer, so it is read at check time".into(),
135 }
136 }
137}
138
139fn literal_text(node: &SNode) -> Option<String> {
140 match &node.node {
141 Node::StringLiteral(text) | Node::RawStringLiteral(text) if !text.is_empty() => {
142 Some(text.clone())
143 }
144 _ => None,
145 }
146}
147
148fn entry_key(entry: &DictEntry) -> Option<String> {
149 match &entry.key.node {
150 Node::StringLiteral(key) | Node::RawStringLiteral(key) | Node::Identifier(key)
151 if !key.is_empty() =>
152 {
153 Some(key.clone())
154 }
155 _ => None,
156 }
157}
158
159impl TypeChecker {
160 fn question_kind(&self, node: &SNode, scope: &TypeScope) -> Option<PredicateQuestionKind> {
162 let ty = self.resolve_alias(&self.infer_type(node, scope)?, scope);
163 let TypeExpr::Shape(fields) = super::union::without_nil(&ty)? else {
164 return None;
165 };
166 let kind = fields.iter().find(|field| field.name == "kind")?;
167 let TypeExpr::LitString(kind) = &kind.type_expr else {
168 return None;
169 };
170 PredicateQuestionKind::from_literal(kind)
171 }
172
173 fn question_spec(
176 &self,
177 id: &str,
178 node: &SNode,
179 scope: &TypeScope,
180 ) -> Result<PredicateQuestionSpec, QuestionSetError> {
181 let kind = self
182 .question_kind(node, scope)
183 .ok_or_else(|| QuestionSetError::EntryNotAQuestion(id.to_string()))?;
184 let Node::FunctionCall { args, .. } = &node.node else {
185 return Err(QuestionSetError::LabelsNotLiteral(id.to_string()));
186 };
187 let instructions = args
188 .first()
189 .and_then(literal_text)
190 .ok_or_else(|| QuestionSetError::InstructionsNotLiteral(id.to_string()))?;
191 let labels = match kind {
192 PredicateQuestionKind::Boolean => Vec::new(),
193 PredicateQuestionKind::Choice => {
194 let Some(SNode {
195 node: Node::DictLiteral(entries),
196 ..
197 }) = args.get(1)
198 else {
199 return Err(QuestionSetError::LabelsNotLiteral(id.to_string()));
200 };
201 entries
202 .iter()
203 .map(|entry| {
204 entry_key(entry)
205 .ok_or_else(|| QuestionSetError::LabelsNotLiteral(id.to_string()))
206 })
207 .collect::<Result<Vec<_>, _>>()?
208 }
209 PredicateQuestionKind::Score => {
210 let Some(SNode {
211 node: Node::ListLiteral(items),
212 ..
213 }) = args.get(1)
214 else {
215 return Err(QuestionSetError::LabelsNotLiteral(id.to_string()));
216 };
217 items
218 .iter()
219 .map(|item| {
220 literal_text(item)
221 .ok_or_else(|| QuestionSetError::LabelsNotLiteral(id.to_string()))
222 })
223 .collect::<Result<Vec<_>, _>>()?
224 }
225 };
226 if kind != PredicateQuestionKind::Boolean && labels.is_empty() {
227 return Err(QuestionSetError::NoLabels(id.to_string()));
228 }
229 for (index, label) in labels.iter().enumerate() {
230 if labels[..index].contains(label) {
231 return Err(QuestionSetError::DuplicateLabel(
232 id.to_string(),
233 label.clone(),
234 ));
235 }
236 }
237 Ok(PredicateQuestionSpec {
238 id: id.to_string(),
239 kind,
240 instructions,
241 labels,
242 })
243 }
244
245 pub(super) fn question_set(
247 &self,
248 node: &SNode,
249 scope: &TypeScope,
250 ) -> Result<Vec<PredicateQuestionSpec>, QuestionSetError> {
251 let Node::DictLiteral(entries) = &node.node else {
252 return Err(QuestionSetError::NotLiteral);
253 };
254 if entries.is_empty() {
255 return Err(QuestionSetError::NoQuestions);
256 }
257 let mut specs: Vec<PredicateQuestionSpec> = Vec::new();
258 for entry in entries {
259 let id = entry_key(entry).ok_or(QuestionSetError::NotLiteral)?;
260 if specs.iter().any(|spec| spec.id == id) {
261 return Err(QuestionSetError::DuplicateId(id));
262 }
263 specs.push(self.question_spec(&id, &entry.value, scope)?);
264 }
265 Ok(specs)
266 }
267
268 pub(in crate::typechecker) fn evaluation_answer_record(
273 &self,
274 args: &[SNode],
275 scope: &TypeScope,
276 ) -> Option<TypeExpr> {
277 let specs = self.question_set(args.get(2)?, scope).ok()?;
278 Some(TypeExpr::Shape(
279 specs
280 .iter()
281 .map(|spec| ShapeField::synthetic(spec.id.clone(), spec.answer_type(), false))
282 .collect(),
283 ))
284 }
285
286 pub(in crate::typechecker) fn narrow_evaluation_answers(
289 mut outcome: TypeExpr,
290 answers: TypeExpr,
291 ) -> TypeExpr {
292 let TypeExpr::Union(members) = &mut outcome else {
293 return outcome;
294 };
295 for member in members.iter_mut() {
296 let TypeExpr::Shape(fields) = member else {
297 continue;
298 };
299 let arm = fields
300 .iter()
301 .find(|field| field.name == "kind")
302 .map(|field| {
303 matches!(&field.type_expr, TypeExpr::LitString(kind)
304 if kind == "answered" || kind == "low_confidence")
305 });
306 if arm != Some(true) {
307 continue;
308 }
309 if let Some(field) = fields
310 .iter_mut()
311 .find(|field| matches!(field.name.as_str(), "value" | "candidates"))
312 {
313 field.type_expr = answers.clone();
314 }
315 }
316 outcome
317 }
318}