Skip to main content

zincite_query/program/
source_copy.rs

1//! Original-source copies and final literal obligations for explicit append edits.
2use super::*;
3use crate::data::charge;
4use crate::{LiteralKind as L, LiteralValue, reduction, structured};
5use std::collections::{BTreeSet, HashSet};
6use zincite_lint::{
7    DeclarationId, DeclarationRole, Domain, EditPart, Instantiation, TextEdit, TypeKind,
8};
9use zincite_syntax::{NodeKind as N, SyntaxNode, TokenKind as T};
10
11#[derive(Debug)]
12pub(super) struct ScalarArgument {
13    source: String,
14    range: Range<usize>,
15    names: Vec<Located<String>>,
16}
17
18#[derive(Debug)]
19pub(super) enum CopyStage {
20    Members(ScalarArgument, ScalarArgument),
21    Elements(Located<String>),
22    Repeat(ScalarArgument),
23}
24
25/// Opaque source fragments tied to their original input and model-fact owner.
26/// Values are created by clone/repeat stages and consumed by append statements.
27#[derive(Debug)]
28pub struct SourceCopies<'a> {
29    input: &'a Input,
30    owner: Rc<ModelFacts<'a>>,
31    parts: Vec<EditPart>,
32    kind: CopyKind,
33}
34#[derive(Debug)]
35enum CopyKind {
36    Members {
37        declaration: DeclarationId,
38        maps: Vec<BTreeMap<String, String>>,
39    },
40    Elements {
41        array: bool,
42    },
43}
44enum CopyRequest<'a, 'v> {
45    Members { count: usize, prefix: String },
46    Elements(&'v SourceCopies<'a>),
47    Repeat(usize),
48}
49impl Parser<'_> {
50    pub(super) fn parse_copy_stage(
51        &mut self,
52        reader: &mut zirql::ArgumentReader<'_>,
53        budget: &mut Budget,
54    ) -> Result<Stage, zirql::QueryError> {
55        let name = reader.word()?;
56        reader.expect('(')?;
57        let stage = if name.value == "clone_elements" {
58            let binding = reader.word()?;
59            if self.names.get(binding.value) != Some(&Kind::MemberCopies) {
60                return Err(zirql::QueryError::query(
61                    binding.range,
62                    "clone_elements requires a member-copy binding",
63                ));
64            }
65            CopyStage::Elements(Located::new(binding.value.to_owned(), binding.range))
66        } else {
67            let count = self.copy_argument(reader, budget)?;
68            if name.value == "clone_members" {
69                reader.expect(',')?;
70                CopyStage::Members(count, self.copy_argument(reader, budget)?)
71            } else {
72                CopyStage::Repeat(count)
73            }
74        };
75        reader.expect(')')?;
76        self.cursor.position = reader.position();
77        self.report_model_demand = true;
78        self.source_copy_range = Some(name.range.start..reader.position());
79        Ok(Stage::Copy(stage))
80    }
81    fn copy_argument(
82        &mut self,
83        reader: &mut zirql::ArgumentReader<'_>,
84        budget: &mut Budget,
85    ) -> Result<ScalarArgument, zirql::QueryError> {
86        self.cursor.position = reader.position();
87        let range = self
88            .cursor
89            .scalar_range()
90            .map_err(composition::query_error)?;
91        reader.advance(self.cursor.position - reader.position())?;
92        std::mem::swap(&mut self.preflight, budget);
93        let result = self.pure_value::<()>(range.clone());
94        std::mem::swap(&mut self.preflight, budget);
95        let (_, names) = result.map_err(composition::query_error)?;
96        budget.charge(range.len() + 1, zirql::Location::Query(range.clone()))?;
97        budget.charge_results(1, zirql::Location::Query(range.clone()))?;
98        Ok(ScalarArgument {
99            source: self.cursor.source[range.clone()].to_owned(),
100            range,
101            names,
102        })
103    }
104}
105impl NativeHost<'_> {
106    pub(super) fn source_copy<'a>(
107        &self,
108        stage: &CopyStage,
109        value: ReportValue<'a>,
110        context: &mut Context<'_, 'a>,
111        bindings: &Bindings<ReportValue<'a>>,
112        budget: &mut Budget,
113        range: &Range<usize>,
114    ) -> Result<ReportValue<'a>, zirql::EvaluationError<QueryError>> {
115        if !self.editing {
116            return Err(zirql::QueryError::query(
117                range.clone(),
118                "source-copy stages require an editing program ending with emit",
119            )
120            .into());
121        }
122        let facts = context
123            .report_facts(budget, range)
124            .map_err(zirql::EvaluationError::Host)?
125            .ok_or_else(|| {
126                zirql::QueryError::query(
127                    range.clone(),
128                    "source copying requires read-only model context (--model)",
129                )
130            })?;
131        let mut scalar = |arg: &ScalarArgument, budget: &mut Budget| {
132            let expression =
133                comprehension::read_data(&arg.source, arg.range.clone(), budget, self.nesting)
134                    .map_err(zirql::EvaluationError::Host)?;
135            self.constructed(
136                &expression,
137                &arg.names,
138                context,
139                bindings,
140                budget,
141                &arg.range,
142            )
143        };
144        let request = match stage {
145            CopyStage::Members(count, prefix) => {
146                let n = scalar(count, budget)?;
147                let p = scalar(prefix, budget)?;
148                let zirql::ConstructedValue::String(prefix) = p.data() else {
149                    return Err(zirql::QueryError::query(
150                        range.clone(),
151                        "clone_members prefix must be a string",
152                    )
153                    .into());
154                };
155                charge(budget, prefix.len(), range).map_err(zirql::EvaluationError::Host)?;
156                CopyRequest::Members {
157                    count: copy_count(&n, range)?,
158                    prefix: prefix.clone(),
159                }
160            }
161            CopyStage::Repeat(count) => {
162                CopyRequest::Repeat(copy_count(&scalar(count, budget)?, range)?)
163            }
164            CopyStage::Elements(name) => {
165                let ReportValue::SourceCopies(mapping) =
166                    functions::unretained(bindings.get(&name.value, name.range.clone())?)
167                else {
168                    return Err(zirql::QueryError::query(
169                        name.range.clone(),
170                        "clone_elements requires member copies",
171                    )
172                    .into());
173                };
174                let CopyKind::Members { .. } = &mapping.kind else {
175                    return Err(zirql::QueryError::query(
176                        name.range.clone(),
177                        "clone_elements requires member copies",
178                    )
179                    .into());
180                };
181                admit_owner(mapping, context.input, &facts, range)
182                    .map_err(zirql::EvaluationError::Host)?;
183                CopyRequest::Elements(mapping.as_ref())
184            }
185        };
186        let Some(SemanticStream::Inspection(selection)) = value.stream() else {
187            return Err(zirql::QueryError::query(
188                range.clone(),
189                "source copying requires one literal assignment",
190            )
191            .into());
192        };
193        let QueryResult::Literals(selection) = selection.as_ref() else {
194            return Err(zirql::QueryError::query(
195                range.clone(),
196                "source copying requires one literal assignment",
197            )
198            .into());
199        };
200        if selection.values.len() != 1 || !std::ptr::eq(selection.input, context.input) {
201            return Err(zirql::QueryError::query(
202                range.clone(),
203                "source copying requires one original literal assignment",
204            )
205            .into());
206        }
207        let copies = make_copies(
208            context.input,
209            facts,
210            &selection.values[0],
211            request,
212            budget,
213            self.limits,
214            range,
215        )
216        .map_err(zirql::EvaluationError::Host)?;
217        Ok(ReportValue::SourceCopies(Rc::new(copies)))
218    }
219}
220fn copy_count(
221    value: &zirql::ConstructedValue,
222    range: &Range<usize>,
223) -> Result<usize, zirql::QueryError> {
224    match value.data() {
225        zirql::ConstructedValue::Integer(n) => usize::try_from(*n).map_err(|_| {
226            zirql::QueryError::query(
227                range.clone(),
228                "additional copies must be a nonnegative integer",
229            )
230        }),
231        _ => Err(zirql::QueryError::query(
232            range.clone(),
233            "additional copies must be a nonnegative integer",
234        )),
235    }
236}
237fn admit_owner<'a>(
238    copies: &SourceCopies<'a>,
239    input: &Input,
240    facts: &ModelFacts<'a>,
241    range: &Range<usize>,
242) -> Result<(), QueryError> {
243    if !std::ptr::eq(copies.input, input) || !std::ptr::eq(copies.owner.as_ref(), facts) {
244        return Err(QueryError::query(
245            range.clone(),
246            "source copies belong to a different input or model owner",
247        ));
248    }
249    Ok(())
250}
251fn assignment<'a>(
252    input: &'a Input,
253    name: &str,
254    budget: &mut Budget,
255    range: &Range<usize>,
256) -> Result<&'a SyntaxNode, QueryError> {
257    let mut result = None;
258    for node in input.syntax().tree().child_nodes() {
259        charge(budget, 1, range)?;
260        if node.kind() == N::Assignment
261            && structured::direct_token(input, node).is_some_and(|token| {
262                crate::identifier_identity(std::str::from_utf8(input.token_bytes(token)).unwrap())
263                    == name
264            })
265            && result.replace(node).is_some()
266        {
267            return Err(QueryError::input(
268                input.original_range(node.range()),
269                "source edits require exactly one original assignment",
270            ));
271        }
272    }
273    result.ok_or_else(|| {
274        QueryError::query(range.clone(), format!("missing data assignment for {name}"))
275    })
276}
277fn source_assignment<'a>(
278    input: &'a Input,
279    value: &LiteralValue<'_>,
280    budget: &mut Budget,
281    range: &Range<usize>,
282) -> Result<(&'a str, &'a SyntaxNode), QueryError> {
283    for node in input.syntax().tree().child_nodes() {
284        charge(budget, 1, range)?;
285        if let Some(rhs) = crate::assignment_rhs(node)
286            && (input.original_range(rhs.range()) == value.range
287                || input.original_range(structured::unwrapped(rhs).range()) == value.range)
288        {
289            let token = structured::direct_token(input, node).unwrap();
290            return Ok((
291                crate::identifier_identity(std::str::from_utf8(input.token_bytes(token)).unwrap()),
292                structured::unwrapped(rhs),
293            ));
294        }
295    }
296    Err(QueryError::input(
297        value.range.clone(),
298        "source copying requires a complete assignment RHS",
299    ))
300}
301fn uninitialized(
302    facts: &ModelFacts<'_>,
303    name: &str,
304    role: DeclarationRole,
305    budget: &mut Budget,
306    range: &Range<usize>,
307) -> Result<DeclarationId, QueryError> {
308    let declaration = comprehension::unique_declaration(facts, name, role, budget, range)?;
309    let node = comprehension::declaration_node(facts, declaration.id, budget, range)?.unwrap();
310    let children = node
311        .child_nodes()
312        .filter(|n| n.kind() != N::Annotation)
313        .count();
314    if children != usize::from(role == DeclarationRole::Value) {
315        return Err(QueryError::query(
316            range.clone(),
317            "source edits require an uninitialized model declaration",
318        ));
319    }
320    Ok(declaration.id)
321}
322fn interior(input: &Input, node: &SyntaxNode) -> Result<(Range<usize>, bool), QueryError> {
323    let array = node.kind() == N::ArrayLiteral;
324    if !array && node.kind() != N::SetLiteral {
325        return Err(QueryError::input(
326            input.original_range(node.range()),
327            "source copying supports direct sets and rank-one arrays",
328        ));
329    }
330    let r = input.original_range(node.range());
331    Ok((r.start + 1..r.end - 1, array))
332}
333fn trailing_comma(input: &Input, interior: &Range<usize>) -> bool {
334    input
335        .syntax()
336        .tokens()
337        .iter()
338        .enumerate()
339        .rev()
340        .find(|(i, t)| {
341            let r = input.token_range(*i);
342            interior.start <= r.start
343                && r.end <= interior.end
344                && !matches!(t.kind, T::Whitespace | T::LineComment | T::BlockComment)
345        })
346        .is_some_and(|(_, t)| t.kind == T::Comma)
347}
348fn make_copies<'a>(
349    input: &'a Input,
350    owner: Rc<ModelFacts<'a>>,
351    value: &LiteralValue<'a>,
352    request: CopyRequest<'a, '_>,
353    budget: &mut Budget,
354    limits: Limits,
355    range: &Range<usize>,
356) -> Result<SourceCopies<'a>, QueryError> {
357    let (count, prefix, mapping) = match &request {
358        CopyRequest::Members { count, prefix } => (*count, Some(prefix.as_str()), None),
359        CopyRequest::Repeat(count) => (*count, None, None),
360        CopyRequest::Elements(mapping) => {
361            let CopyKind::Members { maps, .. } = &mapping.kind else {
362                unreachable!()
363            };
364            (maps.len(), None, Some(*mapping))
365        }
366    };
367    let (name, node) = source_assignment(input, value, budget, range)?;
368    let (inside, array) = interior(input, node)?;
369    let units = count
370        .checked_mul(inside.len().max(1))
371        .ok_or_else(|| QueryError::query(range.clone(), "source copy size overflow"))?;
372    charge(budget, units, range)?;
373    budget
374        .charge_results(count, zirql::Location::Query(range.clone()))
375        .map_err(pipeline::core_error)?;
376    if count > limits.collection {
377        return Err(QueryError::query(
378            range.clone(),
379            "query collection limit exceeded",
380        ));
381    }
382    for (i, token) in input.syntax().tokens().iter().enumerate() {
383        charge(budget, 1, range)?;
384        let r = input.token_range(i);
385        if inside.start <= r.start
386            && r.end <= inside.end
387            && matches!(token.kind, T::LineComment | T::BlockComment)
388            && crate::directive(input.token_bytes(i)).is_some()
389        {
390            return Err(QueryError::input(r, "cannot copy source directives"));
391        }
392    }
393    let mut checker = Validator::new(input, &owner, budget, limits, range);
394    let mut replacements = Vec::new();
395    let kind = if let Some(prefix) = prefix {
396        if prefix.is_empty()
397            || !prefix.as_bytes()[0].is_ascii_alphabetic()
398            || !prefix
399                .bytes()
400                .all(|b| b.is_ascii_alphanumeric() || b == b'_')
401        {
402            return Err(QueryError::query(
403                range.clone(),
404                "member prefix must be a plain identifier",
405            ));
406        }
407        let id = uninitialized(&owner, name, DeclarationRole::Enum, checker.budget, range)?;
408        let members = checker.members(id)?;
409        if members.is_empty() {
410            return Err(QueryError::input(
411                value.range.clone(),
412                "clone_members requires a nonempty enum",
413            ));
414        }
415        collect_member_tokens(
416            input,
417            value,
418            &members,
419            &mut replacements,
420            checker.budget,
421            range,
422        )?;
423        let mut maps = Vec::with_capacity(count);
424        let mut names = existing_names(input, &owner, checker.budget, range)?;
425        for copy in 1..=count {
426            let mut map = BTreeMap::new();
427            for (position, member) in members.iter().enumerate() {
428                charge(checker.budget, prefix.len() + member.len() + 48, range)?;
429                checker
430                    .budget
431                    .charge_results(1, zirql::Location::Query(range.clone()))
432                    .map_err(pipeline::core_error)?;
433                let generated = format!("{prefix}_{copy}_{}", position + 1);
434                charge(checker.budget, generated.len() + 1, range)?;
435                checker
436                    .budget
437                    .charge_results(1, zirql::Location::Query(range.clone()))
438                    .map_err(pipeline::core_error)?;
439                if !names.insert(generated.clone()) {
440                    return Err(QueryError::query(
441                        range.clone(),
442                        format!("generated member collision: {generated}"),
443                    ));
444                }
445                map.insert(member.clone(), generated);
446            }
447            maps.push(map);
448        }
449        CopyKind::Members {
450            declaration: id,
451            maps,
452        }
453    } else {
454        let id = uninitialized(&owner, name, DeclarationRole::Value, checker.budget, range)?;
455        let shape = reduction::declaration_shape(
456            &owner,
457            id,
458            &mut Work {
459                budget: checker.budget,
460            },
461            range,
462            limits,
463        )?;
464        if let Some(mapping) = mapping {
465            let CopyKind::Members { declaration, .. } = &mapping.kind else {
466                unreachable!()
467            };
468            checker.remap = Some(*declaration);
469        }
470        let metadata = comprehension::declared_value(&owner, name, checker.budget, range)?.unwrap();
471        checker.validate(value, &shape, metadata, 0)?;
472        replacements = std::mem::take(&mut checker.replacements);
473        CopyKind::Elements { array }
474    };
475    let maps = match &kind {
476        CopyKind::Members { maps, .. } => Some(maps),
477        _ => mapping.and_then(|m| match &m.kind {
478            CopyKind::Members { maps, .. } => Some(maps),
479            _ => None,
480        }),
481    };
482    let entries = match &value.kind {
483        L::Set(v) => v.len(),
484        L::Array { indexing, entries } if *indexing != structured::ArrayIndexing::StartingKey => {
485            entries.len()
486        }
487        _ => {
488            return Err(QueryError::input(
489                value.range.clone(),
490                "source copying requires a direct rank-one collection",
491            ));
492        }
493    };
494    let literal_count = literal_copy_units(value, checker.budget, range)?;
495    let copied_literals = count
496        .checked_mul(literal_count)
497        .ok_or_else(|| QueryError::query(range.clone(), "source copy size overflow"))?;
498    checker
499        .budget
500        .charge_results(copied_literals, zirql::Location::Query(range.clone()))
501        .map_err(pipeline::core_error)?;
502    let part_count = count
503        .checked_mul(
504            replacements
505                .len()
506                .checked_mul(2)
507                .and_then(|n| n.checked_add(2))
508                .ok_or_else(|| QueryError::query(range.clone(), "source copy size overflow"))?,
509        )
510        .ok_or_else(|| QueryError::query(range.clone(), "source copy size overflow"))?;
511    charge(checker.budget, part_count, range)?;
512    checker
513        .budget
514        .charge_results(part_count, zirql::Location::Query(range.clone()))
515        .map_err(pipeline::core_error)?;
516    let mut parts = Vec::with_capacity(if entries > 0 { part_count } else { 0 });
517    if entries > 0 {
518        for copy in 0..count {
519            if copy > 0 && !trailing_comma(input, &inside) {
520                parts.push(EditPart::Text(",".into()));
521            }
522            let mut start = inside.start;
523            for (token, member) in &replacements {
524                let Some(map) = maps.map(|maps| &maps[copy]) else {
525                    break;
526                };
527                parts.push(EditPart::Original(start..token.start));
528                charge(checker.budget, map[member].len(), range)?;
529                parts.push(EditPart::Text(map[member].clone()));
530                start = token.end;
531            }
532            parts.push(EditPart::Original(start..inside.end));
533        }
534    }
535    Ok(SourceCopies {
536        input,
537        owner,
538        parts,
539        kind,
540    })
541}
542fn literal_copy_units(
543    value: &LiteralValue<'_>,
544    budget: &mut Budget,
545    range: &Range<usize>,
546) -> Result<usize, QueryError> {
547    charge(budget, 1, range)?;
548    let mut units = 1;
549    match &value.kind {
550        L::Set(values) | L::Tuple(values) => {
551            for value in values {
552                units += literal_copy_units(value, budget, range)?;
553            }
554        }
555        L::Record(fields) => {
556            for field in fields {
557                units += literal_copy_units(&field.value, budget, range)?;
558            }
559        }
560        L::Array { entries, .. } => {
561            for entry in entries {
562                if let Some(key) = &entry.key {
563                    units += literal_copy_units(key, budget, range)?;
564                }
565                units += literal_copy_units(&entry.value, budget, range)?;
566            }
567        }
568        _ => {}
569    }
570    Ok(units)
571}
572
573fn collect_member_tokens(
574    input: &Input,
575    value: &LiteralValue<'_>,
576    members: &[String],
577    output: &mut Vec<(Range<usize>, String)>,
578    budget: &mut Budget,
579    range: &Range<usize>,
580) -> Result<(), QueryError> {
581    charge(budget, 1 + members.len(), range)?;
582    match &value.kind {
583        L::Member(name) if members.iter().any(|m| m == name) => {
584            charge(budget, name.len() + 1, range)?;
585            budget
586                .charge_results(1, zirql::Location::Query(range.clone()))
587                .map_err(pipeline::core_error)?;
588            output.push((
589                member_token_range(input, value, budget, range)?,
590                (*name).to_owned(),
591            ));
592        }
593        L::Set(v) | L::Tuple(v) => {
594            for v in v {
595                collect_member_tokens(input, v, members, output, budget, range)?;
596            }
597        }
598        L::Record(fields) => {
599            for f in fields {
600                collect_member_tokens(input, &f.value, members, output, budget, range)?;
601            }
602        }
603        L::Array { entries, .. } => {
604            for e in entries {
605                if let Some(key) = &e.key {
606                    collect_member_tokens(input, key, members, output, budget, range)?;
607                }
608                collect_member_tokens(input, &e.value, members, output, budget, range)?;
609            }
610        }
611        _ => {}
612    }
613    Ok(())
614}
615fn member_token_range(
616    input: &Input,
617    value: &LiteralValue<'_>,
618    budget: &mut Budget,
619    range: &Range<usize>,
620) -> Result<Range<usize>, QueryError> {
621    let tokens = input.syntax().tokens();
622    let start =
623        tokens.partition_point(|t| t.range.end + input.syntax_offset() <= value.range.start);
624    for (i, token) in tokens.iter().enumerate().skip(start) {
625        charge(budget, 1, range)?;
626        if token.range.start + input.syntax_offset() >= value.range.end {
627            break;
628        }
629        if matches!(token.kind, T::Identifier | T::QuotedIdentifier) {
630            return Ok(input.token_range(i));
631        }
632    }
633    Err(QueryError::input(
634        value.range.clone(),
635        "member has no identifier token",
636    ))
637}
638
639fn existing_names(
640    input: &Input,
641    facts: &ModelFacts<'_>,
642    budget: &mut Budget,
643    range: &Range<usize>,
644) -> Result<BTreeSet<String>, QueryError> {
645    let mut names = BTreeSet::new();
646    for (i, token) in input.syntax().tokens().iter().enumerate() {
647        charge(budget, 1 + token.range.len(), range)?;
648        if matches!(token.kind, T::Identifier | T::QuotedIdentifier) {
649            budget
650                .charge_results(1, zirql::Location::Query(range.clone()))
651                .map_err(pipeline::core_error)?;
652            names.insert(
653                crate::identifier_identity(std::str::from_utf8(input.token_bytes(i)).unwrap())
654                    .to_owned(),
655            );
656        }
657    }
658    for declaration in &facts.bindings().declarations {
659        charge(budget, declaration.name.len() + 1, range)?;
660        budget
661            .charge_results(1, zirql::Location::Query(range.clone()))
662            .map_err(pipeline::core_error)?;
663        names.insert(declaration.name.clone());
664    }
665    Ok(names)
666}
667
668pub(crate) struct EnumScale {
669    declaration: DeclarationId,
670    pub(crate) edit: Option<TextEdit>,
671}
672
673/// A constructor domain supplies a count, never synthesized member identities.
674struct ConstructorDomain<'a> {
675    name: &'a str,
676    name_range: Range<usize>,
677    bound_range: Range<usize>,
678    count: i64,
679}
680
681fn constructor_domain<'a>(
682    input: &'a Input,
683    rhs: &SyntaxNode,
684    budget: &mut Budget,
685    range: &Range<usize>,
686) -> Result<ConstructorDomain<'a>, QueryError> {
687    let rhs = structured::unwrapped(rhs);
688    let invalid = || {
689        QueryError::input(
690            input.original_range(rhs.range()),
691            "scale_enum requires a named constructor C(1..N) with a positive literal integer N",
692        )
693    };
694    let mut significant = Vec::new();
695    let tokens = input.syntax().tokens();
696    let start = tokens.partition_point(|token| token.range.start < rhs.range().start);
697    for (offset, token) in tokens[start..]
698        .iter()
699        .enumerate()
700        .take_while(|(_, t)| t.range.end <= rhs.range().end)
701    {
702        charge(budget, 1, range)?;
703        if !matches!(token.kind, T::Whitespace | T::LineComment | T::BlockComment) {
704            if significant.len() == 6 {
705                return Err(invalid());
706            }
707            significant.push(start + offset);
708        }
709    }
710    let [name, left, low, dots, high, right] = significant.as_slice() else {
711        return Err(invalid());
712    };
713    if !matches!(tokens[*name].kind, T::Identifier | T::QuotedIdentifier)
714        || tokens[*low].kind != T::IntegerLiteral
715        || tokens[*high].kind != T::IntegerLiteral
716        || input.token_bytes(*left) != b"("
717        || input.token_bytes(*dots) != b".."
718        || input.token_bytes(*right) != b")"
719    {
720        return Err(invalid());
721    }
722    let integer = |index, budget: &mut Budget| {
723        let literal = LiteralValue {
724            file: "",
725            range: input.token_range(index),
726            spelling: input.token_bytes(index),
727            kind: L::Integer,
728        };
729        comprehension::written_scalar(&literal, input, budget)
730    };
731    let zirql::ConstructedValue::Integer(count) = integer(*high, budget)? else {
732        return Err(invalid());
733    };
734    if integer(*low, budget)? != zirql::ConstructedValue::Integer(1) || count <= 0 {
735        return Err(invalid());
736    }
737    let name_text =
738        crate::identifier_identity(std::str::from_utf8(input.token_bytes(*name)).unwrap());
739    if name_text == "anon_enum" {
740        return Err(invalid());
741    }
742    Ok(ConstructorDomain {
743        name: name_text,
744        name_range: input.token_range(*name),
745        bound_range: input.token_range(*high),
746        count,
747    })
748}
749
750pub(crate) fn prepare_scale(
751    input: &Input,
752    facts: &ModelFacts<'_>,
753    edit: &data::Edit,
754    factor: &zirql::ConstructedValue,
755    budget: &mut Budget,
756) -> Result<EnumScale, QueryError> {
757    let zirql::ConstructedValue::Integer(factor) = factor.data() else {
758        return Err(QueryError::query(
759            edit.range.clone(),
760            "scale_enum factor must be a positive integer",
761        ));
762    };
763    if *factor <= 0 {
764        return Err(QueryError::query(
765            edit.range.clone(),
766            "scale_enum factor must be a positive integer",
767        ));
768    }
769    let declaration = uninitialized(
770        facts,
771        &edit.name.value,
772        DeclarationRole::Enum,
773        budget,
774        &edit.name.range,
775    )?;
776    let rhs = crate::assignment_rhs(assignment(
777        input,
778        &edit.name.value,
779        budget,
780        &edit.name.range,
781    )?)
782    .unwrap();
783    let domain = constructor_domain(input, rhs, budget, &edit.range)?;
784    let count = domain.count.checked_mul(*factor).ok_or_else(|| {
785        QueryError::query(
786            edit.range.clone(),
787            "scale_enum bound multiplication overflow",
788        )
789    })?;
790    for (i, token) in input.syntax().tokens().iter().enumerate() {
791        charge(budget, 1, &edit.range)?;
792        if matches!(token.kind, T::Identifier | T::QuotedIdentifier)
793            && input.token_range(i) != domain.name_range
794            && crate::identifier_identity(std::str::from_utf8(input.token_bytes(i)).unwrap())
795                == domain.name
796        {
797            return Err(QueryError::input(
798                input.token_range(i),
799                "scale_enum cannot remap constructor references outside its defining assignment",
800            ));
801        }
802    }
803    charge(budget, 22, &edit.range)?;
804    Ok(EnumScale {
805        declaration,
806        edit: (count != domain.count).then(|| TextEdit {
807            range: domain.bound_range,
808            replacement: vec![EditPart::Text(count.to_string())],
809        }),
810    })
811}
812
813// Keep the three edit inputs explicit alongside their shared validation context.
814#[allow(clippy::too_many_arguments)]
815pub(crate) fn prepare_appends<'a>(
816    input: &Input,
817    facts: &ModelFacts<'a>,
818    copies: &[(&Located<String>, Rc<SourceCopies<'a>>)],
819    edits: &[&data::Edit],
820    scales: &[EnumScale],
821    budget: &mut Budget,
822    limits: Limits,
823    range: &Range<usize>,
824) -> Result<Vec<TextEdit>, QueryError> {
825    if copies.is_empty() && scales.is_empty() {
826        return Ok(Vec::new());
827    }
828    let mut names = existing_names(input, facts, budget, range)?;
829    let mut changed: Vec<_> = scales.iter().map(|scale| scale.declaration).collect();
830    for (target, copy) in copies {
831        admit_owner(copy, input, facts, range)?;
832        if let CopyKind::Members { declaration, maps } = &copy.kind {
833            if facts.bindings().declarations[declaration.0].name != target.value {
834                return Err(QueryError::query(
835                    target.range.clone(),
836                    "member copies must append to their own enum",
837                ));
838            }
839            if !maps.is_empty() {
840                changed.push(*declaration);
841            }
842            for map in maps {
843                for name in map.values() {
844                    charge(budget, name.len() + 1, range)?;
845                    budget
846                        .charge_results(1, zirql::Location::Query(range.clone()))
847                        .map_err(pipeline::core_error)?;
848                    if !names.insert(name.clone()) {
849                        return Err(QueryError::query(
850                            target.range.clone(),
851                            format!("generated member collision: {name}"),
852                        ));
853                    }
854                }
855            }
856        }
857    }
858    for edit in edits {
859        let id = comprehension::unique_declaration(
860            facts,
861            &edit.name.value,
862            DeclarationRole::Value,
863            budget,
864            &edit.name.range,
865        )?
866        .id;
867        let shape = reduction::declaration_shape(facts, id, &mut Work { budget }, range, limits)?;
868        if changed.iter().any(|id| shape.contains_enum(Some(*id))) {
869            return Err(QueryError::query(
870                edit.name.range.clone(),
871                "constructed edits depending on a changed enum cannot share a source-copy batch",
872            ));
873        }
874    }
875    let mut result = Vec::new();
876    for (target, copy) in copies {
877        let rhs = structured::unwrapped(
878            crate::assignment_rhs(assignment(input, &target.value, budget, &target.range)?)
879                .unwrap(),
880        );
881        let (inside, array) = interior(input, rhs)?;
882        match &copy.kind {
883            CopyKind::Members { .. } if array => {
884                return Err(QueryError::query(
885                    target.range.clone(),
886                    "enum append requires a member set",
887                ));
888            }
889            CopyKind::Elements { array: expected } => {
890                uninitialized(
891                    facts,
892                    &target.value,
893                    DeclarationRole::Value,
894                    budget,
895                    &target.range,
896                )?;
897                if array != *expected {
898                    return Err(QueryError::query(
899                        target.range.clone(),
900                        "append collection kinds differ",
901                    ));
902                }
903            }
904            _ => {}
905        }
906        if copy.parts.is_empty() {
907            continue;
908        }
909        let mut replacement = Vec::new();
910        charge(budget, inside.len() + copy.parts.len(), range)?;
911        let nonempty = rhs.child_nodes().next().is_some();
912        if nonempty && !trailing_comma(input, &inside) {
913            replacement.push(EditPart::Text(",".into()));
914        }
915        budget
916            .charge_results(copy.parts.len() + 2, zirql::Location::Query(range.clone()))
917            .map_err(pipeline::core_error)?;
918        for part in &copy.parts {
919            charge(
920                budget,
921                match part {
922                    EditPart::Text(s) => s.len(),
923                    EditPart::Original(r) => r.len(),
924                },
925                range,
926            )?;
927            replacement.push(part.clone());
928        }
929        result.push(TextEdit {
930            range: inside.end..inside.end,
931            replacement,
932        });
933    }
934    Ok(result)
935}
936
937pub(crate) fn validate_candidate(
938    candidate: &Input,
939    facts: &ModelFacts<'_>,
940    copies: &[(&Located<String>, Rc<SourceCopies<'_>>)],
941    scales: &[EnumScale],
942    budget: &mut Budget,
943    limits: Limits,
944    range: &Range<usize>,
945) -> Result<(), QueryError> {
946    if copies.is_empty() && scales.is_empty() {
947        return Ok(());
948    }
949    charge(budget, copies.len() + scales.len(), range)?;
950    budget
951        .charge_results(copies.len(), zirql::Location::Query(range.clone()))
952        .map_err(pipeline::core_error)?;
953    let changed: Vec<_> = copies
954        .iter()
955        .filter_map(|(_, c)| match &c.kind {
956            CopyKind::Members { declaration, maps } if !maps.is_empty() => Some(*declaration),
957            _ => None,
958        })
959        .chain(scales.iter().map(|scale| scale.declaration))
960        .collect();
961    let mut checker = Validator::new(candidate, facts, budget, limits, range);
962    for node in candidate.syntax().tree().child_nodes() {
963        charge(checker.budget, 1, range)?;
964        let Some(rhs) = crate::assignment_rhs(node) else {
965            continue;
966        };
967        let token = structured::direct_token(candidate, node).unwrap();
968        let name =
969            crate::identifier_identity(std::str::from_utf8(candidate.token_bytes(token)).unwrap());
970        let targeted = copies.iter().any(|(target, _)| target.value == name);
971        charge(checker.budget, facts.bindings().declarations.len(), range)?;
972        let mut declarations = facts
973            .bindings()
974            .declarations
975            .iter()
976            .filter(|d| d.top_level && d.name == name);
977        let declaration = declarations.next();
978        if declaration.is_none() || declarations.next().is_some() {
979            return Err(QueryError::input(
980                candidate.original_range(rhs.range()),
981                "data declaration identity is unavailable or ambiguous",
982            ));
983        }
984        let declaration = declaration.unwrap();
985        if declaration.role == DeclarationRole::Enum {
986            if targeted {
987                checker.members(declaration.id)?;
988            }
989            continue;
990        }
991        if declaration.role != DeclarationRole::Value {
992            continue;
993        }
994        let shape = reduction::declaration_shape(
995            facts,
996            declaration.id,
997            &mut Work {
998                budget: checker.budget,
999            },
1000            range,
1001            limits,
1002        )?;
1003        if targeted || changed.iter().any(|id| shape.contains_enum(Some(*id))) {
1004            assignment(candidate, name, checker.budget, range)?;
1005            let value = structured::read_literal(
1006                candidate,
1007                rhs,
1008                &mut Work {
1009                    budget: checker.budget,
1010                },
1011                range,
1012                limits,
1013            )?;
1014            let metadata =
1015                comprehension::declared_value(facts, name, checker.budget, range)?.unwrap();
1016            checker.validate(&value, &shape, metadata, 0)?;
1017        }
1018    }
1019    Ok(())
1020}
1021enum Axis {
1022    Keys(Vec<String>),
1023    Cardinality(usize),
1024}
1025impl Axis {
1026    fn len(&self) -> usize {
1027        match self {
1028            Self::Keys(keys) => keys.len(),
1029            Self::Cardinality(n) => *n,
1030        }
1031    }
1032}
1033struct Validator<'a, 'm, 'model, 'b> {
1034    input: &'a Input,
1035    facts: &'m ModelFacts<'model>,
1036    budget: &'b mut Budget,
1037    limits: Limits,
1038    range: &'b Range<usize>,
1039    enums: BTreeMap<usize, Vec<String>>,
1040    remap: Option<DeclarationId>,
1041    replacements: Vec<(Range<usize>, String)>,
1042}
1043impl<'a, 'm, 'model, 'b> Validator<'a, 'm, 'model, 'b> {
1044    fn new(
1045        input: &'a Input,
1046        facts: &'m ModelFacts<'model>,
1047        budget: &'b mut Budget,
1048        limits: Limits,
1049        range: &'b Range<usize>,
1050    ) -> Self {
1051        Self {
1052            input,
1053            facts,
1054            budget,
1055            limits,
1056            range,
1057            enums: BTreeMap::new(),
1058            remap: None,
1059            replacements: Vec::new(),
1060        }
1061    }
1062    fn members(&mut self, id: DeclarationId) -> Result<Vec<String>, QueryError> {
1063        if let Some(members) = self.enums.get(&id.0) {
1064            charge(
1065                self.budget,
1066                members.iter().map(|m| m.len() + 1).sum(),
1067                self.range,
1068            )?;
1069            self.budget
1070                .charge_results(members.len(), zirql::Location::Query(self.range.clone()))
1071                .map_err(pipeline::core_error)?;
1072            return Ok(members.clone());
1073        }
1074        let declaration = &self.facts.bindings().declarations[id.0];
1075        comprehension::unique_declaration(
1076            self.facts,
1077            &declaration.name,
1078            DeclarationRole::Enum,
1079            self.budget,
1080            self.range,
1081        )?;
1082        let node =
1083            comprehension::declaration_node(self.facts, id, self.budget, self.range)?.unwrap();
1084        let members = if node.child_nodes().any(|n| n.kind() != N::Annotation) {
1085            comprehension::enum_cases(self.facts, declaration, self.budget, self.range)?
1086                .iter()
1087                .map(|case| case.name.clone())
1088                .collect::<Vec<_>>()
1089        } else {
1090            let rhs = crate::assignment_rhs(assignment(
1091                self.input,
1092                &declaration.name,
1093                self.budget,
1094                self.range,
1095            )?)
1096            .unwrap();
1097            let value = structured::read_literal(
1098                self.input,
1099                rhs,
1100                &mut Work {
1101                    budget: self.budget,
1102                },
1103                self.range,
1104                self.limits,
1105            )?;
1106            let L::Set(values) = value.kind else {
1107                return Err(QueryError::input(
1108                    value.range,
1109                    "enum membership requires a literal member set",
1110                ));
1111            };
1112            let mut members = Vec::new();
1113            let mut seen = HashSet::new();
1114            for value in values {
1115                charge(self.budget, value.spelling.len() + 1, self.range)?;
1116                let L::Member(name) = value.kind else {
1117                    return Err(QueryError::input(
1118                        value.range,
1119                        "enum set requires member identifiers",
1120                    ));
1121                };
1122                if !seen.insert(name) {
1123                    return Err(QueryError::input(value.range, "duplicate enum member"));
1124                }
1125                members.push(name.to_owned());
1126            }
1127            members
1128        };
1129        charge(
1130            self.budget,
1131            members.iter().map(|m| m.len() + 1).sum(),
1132            self.range,
1133        )?;
1134        self.budget
1135            .charge_results(members.len(), zirql::Location::Query(self.range.clone()))
1136            .map_err(pipeline::core_error)?;
1137        self.enums.insert(id.0, members.clone());
1138        Ok(members)
1139    }
1140    fn member_token(
1141        &mut self,
1142        value: &LiteralValue<'_>,
1143        id: DeclarationId,
1144        name: &str,
1145    ) -> Result<(), QueryError> {
1146        if self.remap == Some(id) {
1147            charge(self.budget, name.len() + 1, self.range)?;
1148            self.budget
1149                .charge_results(1, zirql::Location::Query(self.range.clone()))
1150                .map_err(pipeline::core_error)?;
1151            self.replacements.push((
1152                member_token_range(self.input, value, self.budget, self.range)?,
1153                name.to_owned(),
1154            ));
1155        }
1156        Ok(())
1157    }
1158    fn enum_axis(&mut self, id: DeclarationId) -> Result<Axis, QueryError> {
1159        let declaration = &self.facts.bindings().declarations[id.0];
1160        let node =
1161            comprehension::declaration_node(self.facts, id, self.budget, self.range)?.unwrap();
1162        if !node.child_nodes().any(|n| n.kind() != N::Annotation) {
1163            uninitialized(
1164                self.facts,
1165                &declaration.name,
1166                DeclarationRole::Enum,
1167                self.budget,
1168                self.range,
1169            )?;
1170            let rhs = crate::assignment_rhs(assignment(
1171                self.input,
1172                &declaration.name,
1173                self.budget,
1174                self.range,
1175            )?)
1176            .unwrap();
1177            if structured::unwrapped(rhs).kind() != N::SetLiteral {
1178                let domain = constructor_domain(self.input, rhs, self.budget, self.range)?;
1179                let count = usize::try_from(domain.count).map_err(|_| {
1180                    QueryError::input(domain.bound_range, "constructor cardinality overflow")
1181                })?;
1182                return Ok(Axis::Cardinality(count));
1183            }
1184        }
1185        self.members(id).map(Axis::Keys)
1186    }
1187    fn axis(&mut self, domain: &Domain) -> Result<Option<Axis>, QueryError> {
1188        charge(self.budget, 1, self.range)?;
1189        match domain {
1190            Domain::Enum(id) => self.enum_axis(*id).map(Some),
1191            Domain::Named {
1192                declaration,
1193                domain,
1194            } => {
1195                if self.facts.bindings().declarations[declaration.0].role == DeclarationRole::Enum {
1196                    self.enum_axis(*declaration).map(Some)
1197                } else {
1198                    self.axis(domain)
1199                }
1200            }
1201            Domain::UnconstrainedInt => Ok(None),
1202            Domain::Range { lower, upper } => {
1203                let low = comprehension::integer_bound(
1204                    lower,
1205                    self.input,
1206                    &BTreeMap::new(),
1207                    self.facts,
1208                    self.budget,
1209                    self.range,
1210                )?;
1211                let high = comprehension::integer_bound(
1212                    upper,
1213                    self.input,
1214                    &BTreeMap::new(),
1215                    self.facts,
1216                    self.budget,
1217                    self.range,
1218                )?;
1219                let n = high
1220                    .checked_sub(low)
1221                    .and_then(|n| n.checked_add(1))
1222                    .unwrap_or(i64::MAX)
1223                    .max(0);
1224                let n = usize::try_from(n)
1225                    .map_err(|_| QueryError::query(self.range.clone(), "axis size overflow"))?;
1226                charge(self.budget, n.saturating_mul(21), self.range)?;
1227                self.budget
1228                    .charge_results(n, zirql::Location::Query(self.range.clone()))
1229                    .map_err(pipeline::core_error)?;
1230                if n > self.limits.collection {
1231                    return Err(QueryError::query(
1232                        self.range.clone(),
1233                        "query collection limit exceeded",
1234                    ));
1235                }
1236                Ok(Some(Axis::Keys(
1237                    (low..=high).map(|n| n.to_string()).collect(),
1238                )))
1239            }
1240            _ => Err(QueryError::query(
1241                self.range.clone(),
1242                "source-copy validation requires established literal array domains",
1243            )),
1244        }
1245    }
1246    fn validate(
1247        &mut self,
1248        value: &LiteralValue<'_>,
1249        shape: &reduction::Shape,
1250        metadata: comprehension::LiteralMetadata<'_>,
1251        depth: usize,
1252    ) -> Result<(), QueryError> {
1253        charge(self.budget, 1, self.range)?;
1254        let metadata = comprehension::resolve_metadata(
1255            metadata,
1256            self.facts,
1257            self.budget,
1258            self.range,
1259            self.limits.nesting,
1260        )?;
1261        let invalid = || {
1262            QueryError::input(
1263                value.range.clone(),
1264                "literal does not satisfy its retained model type or final domain",
1265            )
1266        };
1267        if depth >= self.limits.nesting.min(64)
1268            || shape.ty.optional
1269            || shape.ty.instantiation != Instantiation::Parameter
1270        {
1271            return Err(invalid());
1272        }
1273        match (&value.kind, &shape.ty.kind) {
1274            (L::Integer, TypeKind::Int | TypeKind::Float) | (L::Float, TypeKind::Float) => {
1275                let number = comprehension::written_scalar(value, self.input, self.budget)?;
1276                data::validate_numeric_domain(
1277                    &number,
1278                    metadata,
1279                    self.input,
1280                    &BTreeMap::new(),
1281                    self.facts,
1282                    self.budget,
1283                    self.range,
1284                )
1285                .map_err(|mut error| {
1286                    if matches!(error.location, crate::ErrorLocation::Query) {
1287                        error.location = crate::ErrorLocation::Input;
1288                        error.range = value.range.clone();
1289                    }
1290                    error
1291                })
1292            }
1293            (L::Boolean(_), TypeKind::Bool) | (L::String, TypeKind::String) => Ok(()),
1294            (L::Member(name), TypeKind::Enum(id)) => {
1295                // Expanded enum types also describe subset domains. Require the
1296                // retained domain itself to name this enum after alias resolution.
1297                let mut domain = metadata
1298                    .node
1299                    .filter(|node| node.kind() == N::DomainType)
1300                    .and_then(|node| node.child_nodes().next());
1301                while let Some(node) = domain
1302                    && node.kind() == N::ParenthesizedExpression
1303                {
1304                    charge(self.budget, 1 + node.children().len(), self.range)?;
1305                    domain = node.child_nodes().next();
1306                }
1307                let location = domain
1308                    .filter(|node| node.kind() == N::Expression)
1309                    .map(|node| self.facts.model().files[metadata.file].location(node.range()));
1310                let mut full_enum = false;
1311                for reference in &self.facts.bindings().references {
1312                    charge(self.budget, 1, self.range)?;
1313                    if reference.file == metadata.file
1314                        && location.as_ref().is_some_and(|location| {
1315                            location.range.start <= reference.location.range.start
1316                                && reference.location.range.end <= location.range.end
1317                        })
1318                        && reference.resolution == zincite_lint::BindingResolution::Resolved(*id)
1319                    {
1320                        full_enum = true;
1321                        break;
1322                    }
1323                }
1324                if !full_enum {
1325                    return Err(QueryError::input(
1326                        value.range.clone(),
1327                        "source-copy validation requires a full enum domain; enum subsets are unsupported",
1328                    ));
1329                }
1330                if !self.members(*id)?.iter().any(|m| m == name) {
1331                    return Err(invalid());
1332                }
1333                self.member_token(value, *id, name)?;
1334                Ok(())
1335            }
1336            (L::Set(values), TypeKind::Set(_)) => {
1337                for v in values {
1338                    self.validate(
1339                        v,
1340                        &shape.components[0].1,
1341                        comprehension::child_metadata(
1342                            Some(metadata),
1343                            &shape.components[0].1.ty,
1344                            None,
1345                        ),
1346                        depth + 1,
1347                    )?;
1348                }
1349                Ok(())
1350            }
1351            (L::Tuple(values), TypeKind::Tuple(_)) if values.len() == shape.components.len() => {
1352                for (i, (v, (_, s))) in values.iter().zip(&shape.components).enumerate() {
1353                    self.validate(
1354                        v,
1355                        s,
1356                        comprehension::child_metadata(Some(metadata), &s.ty, Some(i)),
1357                        depth + 1,
1358                    )?;
1359                }
1360                Ok(())
1361            }
1362            (L::Record(fields), TypeKind::Record(_)) if fields.len() == shape.components.len() => {
1363                let mut seen = HashSet::new();
1364                for f in fields {
1365                    charge(
1366                        self.budget,
1367                        shape.components.len() + f.label.name.len(),
1368                        self.range,
1369                    )?;
1370                    self.budget
1371                        .charge_results(1, zirql::Location::Query(self.range.clone()))
1372                        .map_err(pipeline::core_error)?;
1373                    let Some((_, s)) = shape
1374                        .components
1375                        .iter()
1376                        .find(|(name, _)| name.as_deref() == Some(f.label.name))
1377                    else {
1378                        return Err(invalid());
1379                    };
1380                    if !seen.insert(f.label.name) {
1381                        return Err(invalid());
1382                    }
1383                    let child = data::record_metadata(
1384                        metadata,
1385                        f.label.name,
1386                        &s.ty,
1387                        self.facts,
1388                        self.budget,
1389                        self.range,
1390                    )?;
1391                    self.validate(&f.value, s, child, depth + 1)?;
1392                }
1393                Ok(())
1394            }
1395            (L::Array { indexing, entries }, TypeKind::Array { indices, .. })
1396                if indices.len() == 1 && *indexing != structured::ArrayIndexing::StartingKey =>
1397            {
1398                let axis = self.axis(&shape.indices[0])?;
1399                if let Some(axis) = &axis
1400                    && axis.len() != entries.len()
1401                {
1402                    return Err(QueryError::input(
1403                        value.range.clone(),
1404                        "array coverage differs from the final domain; append every affected assignment",
1405                    ));
1406                }
1407                let mut keys = BTreeSet::new();
1408                for entry in entries {
1409                    if let Some(key) = &entry.key {
1410                        if matches!(axis, Some(Axis::Cardinality(_))) {
1411                            return Err(QueryError::input(
1412                                key.range.clone(),
1413                                "constructor-indexed arrays require positional entries",
1414                            ));
1415                        }
1416                        charge(
1417                            self.budget,
1418                            key.spelling.len().saturating_mul(2)
1419                                + axis.as_ref().map_or(0, Axis::len)
1420                                + 1,
1421                            self.range,
1422                        )?;
1423                        self.budget
1424                            .charge_results(2, zirql::Location::Query(self.range.clone()))
1425                            .map_err(pipeline::core_error)?;
1426                        if let (L::Member(name), TypeKind::Enum(id)) = (&key.kind, &indices[0].kind)
1427                        {
1428                            self.member_token(key, *id, name)?;
1429                        }
1430                        let key = match &key.kind {
1431                            L::Member(name) if matches!(indices[0].kind, TypeKind::Enum(_)) => {
1432                                (*name).to_owned()
1433                            }
1434                            L::Integer if indices[0].kind == TypeKind::Int => {
1435                                match comprehension::written_scalar(key, self.input, self.budget)? {
1436                                    zirql::ConstructedValue::Integer(n) => n.to_string(),
1437                                    _ => return Err(invalid()),
1438                                }
1439                            }
1440                            _ => return Err(invalid()),
1441                        };
1442                        if !keys.insert(key.clone())
1443                            || axis.as_ref().is_some_and(
1444                                |axis| matches!(axis, Axis::Keys(keys) if !keys.contains(&key)),
1445                            )
1446                        {
1447                            return Err(invalid());
1448                        }
1449                    }
1450                    self.validate(
1451                        &entry.value,
1452                        &shape.components[0].1,
1453                        comprehension::child_metadata(
1454                            Some(metadata),
1455                            &shape.components[0].1.ty,
1456                            None,
1457                        ),
1458                        depth + 1,
1459                    )?;
1460                }
1461                if *indexing == structured::ArrayIndexing::Explicit && axis.is_none() {
1462                    charge(
1463                        self.budget,
1464                        keys.len()
1465                            .saturating_mul(keys.len().max(1).ilog2() as usize + 1),
1466                        self.range,
1467                    )?;
1468                    self.budget
1469                        .charge_results(keys.len(), zirql::Location::Query(self.range.clone()))
1470                        .map_err(pipeline::core_error)?;
1471                    let mut integers = keys
1472                        .iter()
1473                        .map(|key| key.parse::<i64>().map_err(|_| invalid()))
1474                        .collect::<Result<Vec<_>, _>>()?;
1475                    integers.sort_unstable();
1476                    if integers
1477                        .windows(2)
1478                        .any(|pair| pair[0].checked_add(1) != Some(pair[1]))
1479                    {
1480                        return Err(invalid());
1481                    }
1482                }
1483                Ok(())
1484            }
1485            _ => Err(invalid()),
1486        }
1487    }
1488}
1489
1490impl SourceCopies<'_> {
1491    pub(super) fn kind(&self) -> Kind {
1492        match self.kind {
1493            CopyKind::Members { .. } => Kind::MemberCopies,
1494            CopyKind::Elements { .. } => Kind::SourceCopies,
1495        }
1496    }
1497}