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