1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
//! Shared `{code, message}` diagnostic error for the load-time lowering
//! passes (expression templates / template imports, enum lowering).
//!
//! The `code` field is a STABLE cross-binding diagnostic identifier (e.g.
//! `template_import_unknown_name`, `unknown_enum`) that the conformance
//! fixtures match on — bindings must agree on codes, while `message` prose is
//! binding-local. The per-pass public names (`ExpressionTemplateError`,
//! `EnumLoweringError`) are aliases of this one type so each pass keeps its
//! documented API surface without duplicating the struct and its impls.
/// A lowering-pass diagnostic: stable `code` plus human-readable `message`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiagnosticError {
/// Stable cross-binding diagnostic code (snake_case).
pub code: &'static str,
/// Human-readable description (binding-local prose).
pub message: String,
}
impl std::fmt::Display for DiagnosticError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] {}", self.code, self.message)
}
}
impl std::error::Error for DiagnosticError {}
/// Shorthand constructor used throughout the lowering passes.
pub(crate) fn err(code: &'static str, message: impl Into<String>) -> DiagnosticError {
DiagnosticError {
code,
message: message.into(),
}
}
/// Generates the diagnostic-code registry's two public faces from ONE
/// declaration list: the [`codes`] module of per-code `pub const`s (the form
/// raise sites reference — Rust-idiomatic, and checked at compile time) and
/// the enumerable [`ERROR_CODES`] `(name, value)` table (the form tests and
/// cross-binding vocabulary diffs iterate). One declaration per code means the
/// two faces cannot drift apart.
macro_rules! diagnostic_code_registry {
($( $(#[$doc:meta])* $NAME:ident = $value:literal; )+) => {
/// Central registry of the diagnostic code STRINGS this binding emits on a
/// [`DiagnosticError`] (the load-time lowering passes: §9.6 expression
/// templates, §9.7 template-library and coupling-library imports, §9.3 enum
/// lowering, §4.7 subsystem refs).
///
/// Cross-binding contract — **these values must never change**. They are pinned
/// by the shared conformance fixtures (`tests/invalid/expected_errors.json`)
/// and mirrored by TypeScript's `ERROR_CODES`
/// (`pkg/earthsci-ast-ts/src/errors.ts`) and Python's `ErrorCode` enum
/// (`pkg/earthsci-ast-py/src/earthsci_ast/error_handling.py`). Every constant
/// below equals, byte for byte, a literal that was already emitted somewhere in
/// `src/`; this module only CENTRALIZES the references — it does not (and must
/// not) change any emitted string. Adding a new diagnostic means adding an entry
/// to the `diagnostic_code_registry!` list AND coordinating the value across
/// every binding.
///
/// Constant names are the SCREAMING_SNAKE_CASE form of the value (mirroring the
/// TypeScript keys and the Python enum), so a reference reads as
/// `codes::TEMPLATE_IMPORT_UNKNOWN_NAME`.
///
/// Scope: every diagnostic code string this binding emits, whichever error
/// type carries it — [`DiagnosticError`], `StructuralError`
/// (`crate::validate::StructuralErrorCode::to_string` is defined off these
/// constants), `ClosedFunctionError` and the `UnitWarning` finding kinds
/// (`crate::units::UNIT_FINDING_*` are aliases of three of them). The one
/// deliberate exclusion is the DAE lowering's Rust-local `E_*` names, which
/// are not a cross-binding vocabulary — Julia excludes its `E_TREEWALK_*`
/// names from `ERROR_CODES` for the same reason.
///
/// [`ERROR_CODES`] is the enumerable form of this module, for tests and
/// cross-binding vocabulary diffs; both are generated from the single
/// `diagnostic_code_registry!` declaration list.
pub mod codes {
$( $(#[$doc])* pub const $NAME: &str = $value; )+
}
/// The [`codes`] registry in ENUMERABLE form: `(constant name, code string)`
/// for every diagnostic code this binding emits, in declaration order
/// (grouped by the pass that emits it, as the [`codes`] module is).
///
/// The Rust twin of Julia's `ERROR_CODES` `NamedTuple`
/// (`pkg/EarthSciAST.jl/src/error_codes.jl`), TypeScript's `ERROR_CODES`
/// object (`pkg/earthsci-ast-ts/src/errors.ts`), Python's `ErrorCode` enum and
/// Go's `codes.go`. The individual `pub const`s in [`codes`] are what raise
/// sites reference — that is the Rust-idiomatic form, and it is checked at
/// compile time. This table exists so the vocabulary can be *enumerated*:
/// diffed against a peer binding, or asserted over in a test.
///
/// **The code VALUES are a cross-binding contract and must never change.**
/// Every entry equals, byte for byte, a literal this crate already emitted.
/// Both this table and [`codes`] expand from the one
/// `diagnostic_code_registry!` declaration list, so the table covers every
/// constant by construction.
///
/// By convention each name is the SCREAMING_SNAKE_CASE form of its value —
/// the same convention the TypeScript keys and the Python enum members follow.
/// `error_codes_are_well_formed` in this module enforces it.
pub const ERROR_CODES: &[(&str, &str)] = &[
$( (stringify!($NAME), codes::$NAME), )+
];
};
}
diagnostic_code_registry! {
// ---- expression templates: §9.6 lowering (`lower_expression_templates.rs`)
// ----
/// An `apply_expression_template` whose supplied bindings do not match the
/// template's declared parameter list (missing, extra, or duplicated).
APPLY_EXPRESSION_TEMPLATE_BINDINGS_MISMATCH = "apply_expression_template_bindings_mismatch";
/// A malformed `expression_templates` declaration — the single most-emitted
/// code in this pass, covering every shape violation of the template
/// declaration itself.
APPLY_EXPRESSION_TEMPLATE_INVALID_DECLARATION = "apply_expression_template_invalid_declaration";
/// A template body that (directly or transitively) applies itself.
APPLY_EXPRESSION_TEMPLATE_RECURSIVE_BODY = "apply_expression_template_recursive_body";
/// An `apply_expression_template` naming a template the document does not
/// declare (and no import supplies).
APPLY_EXPRESSION_TEMPLATE_UNKNOWN_TEMPLATE = "apply_expression_template_unknown_template";
/// A template declaring a `min_spec_version` newer than this binding
/// implements.
APPLY_EXPRESSION_TEMPLATE_VERSION_TOO_OLD = "apply_expression_template_version_too_old";
/// A rewrite rule whose repeated application does not reach a fixed point.
REWRITE_RULE_NONTERMINATING = "rewrite_rule_nonterminating";
/// A rewrite-target op (§4.2) reached evaluation or compilation without
/// being lowered (esm-spec §9.6.6); refused through `OpError::Unlowered`.
UNLOWERED_OPERATOR = "unlowered_operator";
/// Template body expansion exceeded the depth budget (a runaway, but not
/// provably self-recursive, expansion).
TEMPLATE_BODY_EXPANSION_TOO_DEEP = "template_body_expansion_too_deep";
/// A template `constraints` entry naming an index set absent from the
/// document's `index_sets` registry.
TEMPLATE_CONSTRAINT_UNKNOWN_INDEX_SET = "template_constraint_unknown_index_set";
// ---- expression templates: the SURVIVING registry, checked at flatten
// (`flatten.rs::check_registry_coupling_rewrites`) ----
/// A surviving `expression_templates` registry body names a variable a
/// coupling rule rewrote out of the flattened equations (a `variable_map`
/// substitution, or an `operator_compose` renaming match). The body is
/// authored source no equation walk reaches and it expands at the BUILD
/// boundary, so this is the ONE site where such a reference is REFUSED
/// rather than resolved (esm-spec §9.6.4, CONFORMANCE_SPEC §5.35).
TEMPLATE_BODY_REFERENCES_COUPLING_REWRITTEN_VARIABLE =
"template_body_references_coupling_rewritten_variable";
// ---- templates: geometry / makearray structural folds (also emitted from
// `lower_expression_templates.rs` during template lowering) ----
/// A `geometry` manifold declaration that is not a well-formed manifold.
GEOMETRY_MANIFOLD_INVALID = "geometry_manifold_invalid";
/// A `makearray` region whose `hi` bound precedes its `lo` bound.
MAKEARRAY_REGION_INVERTED = "makearray_region_inverted";
// ---- template-library imports: §9.7 (`template_imports.rs`) ----
/// An imported metaparameter name collides with a name already in scope.
METAPARAMETER_NAME_CONFLICT = "metaparameter_name_conflict";
/// A metaparameter bound to a value of the wrong type (or used where its
/// declared type does not admit it).
METAPARAMETER_TYPE_ERROR = "metaparameter_type_error";
/// A metaparameter referenced by a template body with no binding in scope.
METAPARAMETER_UNBOUND = "metaparameter_unbound";
/// A cycle in the template-library import graph.
TEMPLATE_IMPORT_CYCLE = "template_import_cycle";
/// A `template_imports` entry resolving to a COUPLING library, which
/// exports roles rather than templates.
TEMPLATE_IMPORT_IS_COUPLING_LIBRARY = "template_import_is_coupling_library";
/// A `template_imports` entry resolving to a document that is not a library
/// at all.
TEMPLATE_IMPORT_NOT_LIBRARY = "template_import_not_library";
/// A `rebind` naming a metaparameter the imported library does not declare.
TEMPLATE_IMPORT_REBIND_UNKNOWN_NAME = "template_import_rebind_unknown_name";
/// A `rename` whose target name is already taken in the importing scope.
TEMPLATE_IMPORT_RENAME_COLLISION = "template_import_rename_collision";
/// A `rename` whose target is not a syntactically valid name.
TEMPLATE_IMPORT_RENAME_INVALID = "template_import_rename_invalid";
/// A `rename` naming a source the imported library does not export.
TEMPLATE_IMPORT_RENAME_UNKNOWN_NAME = "template_import_rename_unknown_name";
/// An explicit import `names` entry the library does not export.
TEMPLATE_IMPORT_UNKNOWN_NAME = "template_import_unknown_name";
/// A `template_imports` `ref` that does not resolve (missing file, remote
/// URL, unreadable or unparseable document).
TEMPLATE_IMPORT_UNRESOLVED = "template_import_unresolved";
/// An imported library declaring a `min_spec_version` newer than this
/// binding implements.
TEMPLATE_IMPORT_VERSION_TOO_OLD = "template_import_version_too_old";
/// A top-level `solver` block in a document declaring `esm` < 1.1.0
/// (esm-spec §2.2.4, §2.2.5).
SOLVER_VERSION_TOO_OLD = "solver_version_too_old";
/// `solver.external` in a document declaring `esm` < 1.3.0 (esm-spec
/// §2.2.4, §2.2.5).
SOLVER_EXTERNAL_VERSION_TOO_OLD = "solver_external_version_too_old";
/// A `solver.external` carrying an unexpanded `${` (esm-spec §2.2.6): it
/// is never expanded. The message names `/solver/external` and quotes the
/// string.
SOLVER_EXTERNAL_UNRESOLVED = "solver_external_unresolved";
/// `solver.time_span` in a document declaring `esm` < 1.3.0 (esm-spec
/// §2.2.4, §2.2.5).
SOLVER_TIME_SPAN_VERSION_TOO_OLD = "solver_time_span_version_too_old";
/// A `solver.time_span` that is no interval (esm-spec §2.2.7): `end` not
/// after `start`, a timestamp naming no calendar day, or an unreadable time
/// origin. The message names `/solver/time_span` (and the origin's pointer
/// when the origin is at fault).
SOLVER_TIME_SPAN_INVALID = "solver_time_span_invalid";
/// Declared `units` on an expression node in a document declaring `esm` <
/// 1.2.0 (esm-spec §4.8.5).
CONST_UNITS_VERSION_TOO_OLD = "const_units_version_too_old";
/// An `inject` whose target names a data LOADER rather than a component.
TEMPLATE_INJECT_TARGET_IS_LOADER = "template_inject_target_is_loader";
/// An `inject` whose target resolves to something that is not a component.
TEMPLATE_INJECT_TARGET_NOT_COMPONENT = "template_inject_target_not_component";
/// An `inject` whose target names nothing in the importing document.
TEMPLATE_INJECT_TARGET_UNKNOWN = "template_inject_target_unknown";
/// A document carrying top-level `expression_templates` beside a component
/// payload, which a template-library file never declares (esm-spec §9.7.1).
TEMPLATE_LIBRARY_ILLEGAL_PAYLOAD = "template_library_illegal_payload";
// ---- coupling libraries: §9.7 coupling-library imports
// (`coupling_imports.rs`) ----
/// A coupling edge naming a role the library does not declare.
COUPLING_EDGE_UNKNOWN_ROLE = "coupling_edge_unknown_role";
/// A role `bind` target that is not a component.
COUPLING_IMPORT_BIND_NOT_A_COMPONENT = "coupling_import_bind_not_a_component";
/// A `coupling_imports` entry resolving to a document that is not a
/// coupling library.
COUPLING_IMPORT_NOT_LIBRARY = "coupling_import_not_library";
/// A declared role left without a binding at the import site.
COUPLING_IMPORT_ROLE_UNBOUND = "coupling_import_role_unbound";
/// A `bind` naming a role the imported library does not declare.
COUPLING_IMPORT_UNKNOWN_ROLE = "coupling_import_unknown_role";
/// A `coupling_imports` `ref` that does not resolve.
COUPLING_IMPORT_UNRESOLVED = "coupling_import_unresolved";
/// A coupling library carrying payload a coupling library may not hold.
COUPLING_LIBRARY_ILLEGAL_PAYLOAD = "coupling_library_illegal_payload";
/// A coupling library that itself declares an import (nesting is not
/// permitted).
COUPLING_LIBRARY_NESTED_IMPORT = "coupling_library_nested_import";
/// A declared role that no imported coupling edge ever uses.
COUPLING_ROLE_UNUSED = "coupling_role_unused";
// ---- enums: §9.3 load-time enum lowering (`lower_enums.rs`) ----
/// An enum operator applied to an argument list its arity does not admit.
ENUM_INVALID_ARGS = "enum_invalid_args";
/// An enum construct survived lowering — nothing downstream can interpret
/// it, so loading must fail rather than silently discard it.
ENUM_LOWERING_RESIDUAL = "enum_lowering_residual";
/// A malformed top-level `enums` block.
INVALID_ENUMS_BLOCK = "invalid_enums_block";
/// A reference to an enum the document does not declare.
UNKNOWN_ENUM = "unknown_enum";
/// A reference to a symbol the named enum does not declare.
UNKNOWN_ENUM_SYMBOL = "unknown_enum_symbol";
// ---- function tables: §9.5 `table_lookup` lowering
// (`lower_table_lookup.rs`) ----
//
// The §9.5.5 subset this binding actually raises. Carried on
// `CompileError::TableLookupLowering` rather than `DiagnosticError`,
// because the lowering runs at BUILD (§9.5.4 requires the authored form
// to round-trip, so it cannot run at load) — but the CODES are the same
// cross-binding vocabulary either way, which is what this registry is.
/// `table_lookup.table` names an id the document's `function_tables`
/// block does not declare.
TABLE_LOOKUP_UNKNOWN_TABLE = "table_lookup_unknown_table";
/// The key set of `table_lookup.axes` does not match the axis names the
/// referenced table declares (an extra key, a missing one, or a
/// positional `args` list where the axes map belongs).
TABLE_LOOKUP_AXIS_NAME_MISMATCH = "table_lookup_axis_name_mismatch";
/// `table_lookup.output` selects a row the table's leading `data`
/// dimension does not have, or a name its `outputs` list does not carry.
TABLE_LOOKUP_OUTPUT_OUT_OF_RANGE = "table_lookup_output_out_of_range";
/// A table whose `interpolation` and axis count disagree (`linear` and
/// `nearest` require 1 axis, `bilinear` 2).
TABLE_INTERPOLATION_AXES_MISMATCH = "table_interpolation_axes_mismatch";
/// The nesting of `data` does not match the shape `axes` (and `outputs`,
/// when present) imply.
TABLE_DATA_SHAPE_MISMATCH = "table_data_shape_mismatch";
/// An axis's `values` carries a non-finite entry; §9.5.1 requires
/// strictly-increasing FINITE floats.
TABLE_AXIS_NAN = "table_axis_nan";
/// A lookup into an `out_of_bounds: "error"` table was queried outside an
/// axis range (esm-spec §9.5.1). Raised at evaluation time, through the
/// evaluators' fail-closed fault channel.
TABLE_LOOKUP_OUT_OF_BOUNDS = "table_lookup_out_of_bounds";
/// The referenced table declares `out_of_bounds: "error"` and the binding
/// does not implement that mode (esm-spec §9.5.3a). This binding
/// implements it, so it no longer raises this code; the constant stays
/// because the value is a cross-binding contract the other bindings emit.
TABLE_OUT_OF_BOUNDS_UNSUPPORTED = "table_out_of_bounds_unsupported";
// ---- subsystem refs: §4.7 reference resolution (`ref_loading.rs`) ----
//
// `unresolved_subsystem_ref` and `ambiguous_subsystem_ref` are the
// canonical cross-binding names, pinned by
// `tests/invalid/expected_errors.json` (`subsystem_ref_not_found.esm`,
// `subsystem_ref_ambiguous.esm`).
/// A subsystem `{ "ref": ... }` that does not resolve: a missing file, a
/// remote URL, a cycle, or an unreadable/unparseable document.
UNRESOLVED_SUBSYSTEM_REF = "unresolved_subsystem_ref";
/// A subsystem ref that resolved to a file holding MORE (or fewer) than one
/// top-level system; §4.7 requires exactly one, so which system to mount is
/// ambiguous. Only the resolver can raise this — it is the only layer that
/// reads the referenced file.
AMBIGUOUS_SUBSYSTEM_REF = "ambiguous_subsystem_ref";
/// A top-level `models.<k>` mount edge that does not resolve.
TOPLEVEL_MODEL_REF_UNRESOLVED = "toplevel_model_ref_unresolved";
/// A referenced subsystem file's top-level `index_sets` entry collides with
/// a non-deep-equal declaration in the importing document (§4.7).
SUBSYSTEM_INDEX_SET_CONFLICT = "subsystem_index_set_conflict";
/// A mount edge's `index_set_rename` names an index set the RESOLVED
/// mounted document does not declare (§4.7 "Mount-edge index-set
/// renaming") — the mount-edge mirror of
/// [`TEMPLATE_IMPORT_RENAME_UNKNOWN_NAME`].
SUBSYSTEM_INDEX_SET_RENAME_UNKNOWN_NAME = "subsystem_index_set_rename_unknown_name";
/// `index_set_rename` on a mount form that does not implement it (§4.7
/// "Mount-edge index-set renaming", "Where it applies"). The field is a
/// legal `SubsystemRef` property at either mount form, but a binding whose
/// top-level `models.<k>` `{ref}` inliner cannot apply it MUST say so
/// rather than merge the leaf under its pre-rename axis names.
SUBSYSTEM_INDEX_SET_RENAME_UNSUPPORTED_MOUNT_FORM =
"subsystem_index_set_rename_unsupported_mount_form";
/// A §4.7 `{ref}` mount at a form this binding does not implement — a
/// top-level `reaction_systems.<k>` `{ref}` (§4.7 "Two mount forms, one
/// mechanism"). Refused at the entry rather than failing later on an
/// incidental typed-parse error.
MOUNT_FORM_UNSUPPORTED = "mount_form_unsupported";
/// A declared working precision reaches a construct that cannot carry it
/// (esm-spec §11.3.1, §4.7 "Working precision crosses a mount"): a mounted
/// leaf's `domain.element_type` over a reaction system, whose species and
/// parameters have no per-variable `element_type`. Refused rather than
/// evaluated in the mounting document's precision.
ELEMENT_TYPE_UNCARRIED = "element_type_uncarried";
/// A `subsystem` ref pointing at a COUPLING library, which exports roles
/// rather than a mountable system.
SUBSYSTEM_REF_IS_COUPLING_LIBRARY = "subsystem_ref_is_coupling_library";
/// A `subsystem` ref pointing at a TEMPLATE library, which exports
/// templates rather than a mountable system.
SUBSYSTEM_REF_IS_TEMPLATE_LIBRARY = "subsystem_ref_is_template_library";
// ---- template-library import MERGE collisions (`template_imports.rs`) ----
/// Two imported template libraries export the same template name.
TEMPLATE_IMPORT_NAME_CONFLICT = "template_import_name_conflict";
/// Two imported template libraries export the same index-set name.
TEMPLATE_IMPORT_INDEX_SET_CONFLICT = "template_import_index_set_conflict";
// ---- structural validation: the `error_type` of a `StructuralError`
// (`validate.rs`), pinned by `tests/invalid/expected_errors.json`. ----
/// A `ranges[*]`/expression reference to an undeclared array index set.
ARRAY_SHAPE_MISMATCH = "array_shape_mismatch";
/// Inline ARRAY data as the `default` of a variable that declares no
/// `shape` (esm-spec §6.3). Inline array data is a shaped variable's value,
/// so with no shape there is nothing for the array to fill.
ARRAY_DEFAULT_WITHOUT_SHAPE = "array_default_without_shape";
/// An equation graph that depends on itself.
CIRCULAR_DEPENDENCY = "circular_dependency";
/// A parameter `update` naming no declared data source.
DATA_SOURCE_UNDEFINED = "data_source_undefined";
/// A data-fed parameter (`update` of `kind: "data"`, esm-spec §5.4/§8.5)
/// reached a build with NOTHING bound to it — no provider, no loaded
/// array, no caller-supplied `p` value. Raised at CONSTRUCTION, before any
/// right-hand side is built, naming the parameter, the `data_sources`
/// entry and what to pass. The complement of `data_source_undefined`,
/// which is the VALIDATOR's finding about an `update.source` that names no
/// declared entry; this one is the BUILD's finding about a source that
/// resolves perfectly well and that nothing supplied data for
/// (CONFORMANCE_SPEC §5.46).
DATA_SOURCE_UNBOUND = "data_source_unbound";
/// A `data_sources[*].source.url_template` (or `mirrors` entry) that
/// cannot be resolved to a URL at load time (esm-spec §8.2.1): an
/// unexpanded `${VAR}` — §8.2 does not expand environment variables
/// into a source's location at all — or a resolved path carrying a `?`
/// or `#`. The message names the offending data source and template.
DATA_SOURCE_URL_UNRESOLVED = "data_source_url_unresolved";
/// An expression ranges over a `kind: "derived"` index set whose producer
/// could not be materialized at build (esm-spec §9.6.6).
DERIVED_INDEX_SET_UNMATERIALIZED = "derived_index_set_unmaterialized";
/// A domain axis whose units disagree with the coordinate's.
DOMAIN_UNIT_MISMATCH = "domain_unit_mismatch";
/// A model whose equation count cannot match its unknown count.
EQUATION_COUNT_MISMATCH = "equation_count_mismatch";
/// An equation whose left-hand side names a parameter — bare, indexed,
/// inside a `faq`, or through a scoped reference into a subsystem
/// (esm-spec §6.3.1). An equation defines unknowns only.
EQUATION_DEFINES_PARAMETER = "equation_defines_parameter";
/// An event `affect` writing a parameter rather than an unknown.
EVENT_AFFECTS_PARAMETER = "event_affects_parameter";
/// An event referring to a variable the model does not declare.
EVENT_VAR_UNDECLARED = "event_var_undeclared";
/// A coupling `factor` carrying an expression transform.
FACTOR_WITH_EXPRESSION_TRANSFORM = "factor_with_expression_transform";
/// An `ic` block inside a reaction system (§4.7).
IC_IN_REACTION_SYSTEM = "ic_in_reaction_system";
/// A bare-index observed definition (`index(V, k…) ~ rhs`, esm-spec §6.3.1)
/// outside the runnable form: the RHS is not a `faq` whose `output_idx`
/// names the subscripts in order. Refused when the model is built.
INDEXED_DEFINITION_UNSUPPORTED_FORM = "indexed_definition_unsupported_form";
/// A `broadcast` node whose `fn` names no scalar operator.
INVALID_BROADCAST_FN = "invalid_broadcast_fn";
/// A `join.on` key of a type the join cannot compare.
JOIN_KEY_INVALID_TYPE = "join_key_invalid_type";
/// A `join.on` key whose range symbol the document does not determine —
/// its index set is drawn by more than one of the node's ranges and the
/// clause names no `syms` (CONFORMANCE_SPEC §5.5.8).
JOIN_SIDE_AMBIGUOUS = "join_side_ambiguous";
/// A `join.syms` entry that is not a range symbol of the node.
JOIN_SYMS_UNKNOWN_SYMBOL = "join_syms_unknown_symbol";
/// A null entry in a reaction list.
NULL_REACTION = "null_reaction";
/// A dependency cycle among a model's OBSERVED unknowns (esm-spec §4.9.6):
/// `a` is defined in terms of `b`, `b` (transitively) in terms of `a`, so
/// no evaluation order satisfies both definitions. The self-edge of a
/// §4.3.1.1 recurrence CANDIDATE is not such an edge and is dropped.
OBSERVED_CYCLE = "observed_cycle";
/// An output name that matches no variable exactly and whose last dotted
/// segment is shared by more than one variable: `derive_output_plan`'s
/// `observed` request (CONFORMANCE_SPEC §5.17.4). A last-segment match is
/// accepted only when it designates exactly one variable.
AMBIGUOUS_OUTPUT_NAME = "ambiguous_output_name";
/// An `operator` whose declared variable the model does not have.
OPERATOR_VARIABLE_MISSING = "operator_variable_missing";
/// A non-value-invention `faq` ranges over a `kind: "ragged"` index set
/// but its body never reads that set's `values` array (esm-spec §4.3.1
/// "Ragged ranges"): the range binds the POSITION k in 1..offsets[parent],
/// so the body reads positions where the author meant members.
RAGGED_VALUES_NOT_GATHERED = "ragged_values_not_gathered";
/// A tile's owned or storage box is malformed: an unknown index set, a
/// face set not declared against a decomposed axis, a box outside
/// `1..N`, or an owned box not inside its storage box (API_SPEC §5.8).
RANGE_BOX_INVALID = "range_box_invalid";
/// A range restriction met a rule whose locality is global along a
/// decomposed axis: a 0-d state write on a tile, or a contraction, scan or
/// data-dependent gather along the axis. Names the rule and the axis.
RANGE_GLOBAL_LOCALITY = "range_global_locality";
/// A tile reads an in-domain cell its storage box does not hold. Names the
/// rule, the variable, the axis and the missing cells.
RANGE_HALO_INSUFFICIENT = "range_halo_insufficient";
/// `select`'s `writes` omits a variable its kept terms write.
RANGE_SELECTION_WRITES_INCOMPLETE = "range_selection_writes_incomplete";
/// A causal self-read (esm-spec §4.3.1.1) that is not strictly earlier
/// along exactly one axis.
RECURRENCE_NOT_WELLFOUNDED = "recurrence_not_wellfounded";
/// A causal self-read the runtime cannot restrict to one cell.
RECURRENCE_UNSUPPORTED_FORM = "recurrence_unsupported_form";
/// A continuous or discrete event, or an implicit equation, reached an
/// evaluator that cannot run it (esm-spec §9.6.6).
UNSUPPORTED_CONSTRUCT = "unsupported_construct";
/// An equation reads a `callback` coupling variable, and no callback is
/// registered to supply it at construction (esm-spec §9.6.6).
CALLBACK_UNREGISTERED = "callback_unregistered";
/// `ProblemOptions::compiler` named a value outside the closed vocabulary
/// of API_SPEC §5.8 (`interpreter`, `native`, `xla`, `mtk`, `sympy`).
COMPILER_UNKNOWN = "compiler_unknown";
/// The named compiler is in the vocabulary, but this binding, build or
/// process cannot provide it — a compiler this binding does not implement,
/// a Cargo feature not compiled in, a runtime not configured. The message
/// names what would have to be loaded or built. Never answered by running
/// a different compiler (esm-libraries-spec §2.5.10).
COMPILER_UNAVAILABLE = "compiler_unavailable";
/// The chosen compiler cannot run this document. Raised at BUILD, naming
/// the compiler, the rule (an equation or an observed, component-qualified)
/// and the reason. A refusal, never a fallback.
COMPILER_REFUSED_RULE = "compiler_refused_rule";
/// A relational node in a continuous (ODE-position) expression.
RELATIONAL_NODE_IN_CONTINUOUS = "relational_node_in_continuous";
/// A `faq` binder (a `ranges` key or an `output_idx` entry) spelled
/// with a globally-scoped name — the document's independent variable
/// (esm-spec §11.3) or the §6.4 `_var` placeholder — which every consumer
/// resolves by name before the loop bindings, so the symbol would never
/// address the loop it declares.
RESERVED_INDEX_SYMBOL = "reserved_index_symbol";
/// A DECLARATION — a `variables` key, a reaction species, or a reaction
/// parameter — spelled with a globally-scoped name: the document's
/// independent variable (esm-spec §11.3) or the §6.4 `_var` placeholder.
/// Both are in scope in every component and are resolved by name ahead of
/// the declaration maps, so the declaration is unreachable and every reader
/// silently receives the implicit symbol instead (§4.9.1.1).
RESERVED_VARIABLE_NAME = "reserved_variable_name";
/// An inline test's override key that matches no declared name (esm-spec §6.6.2).
UNKNOWN_OVERRIDE_KEY = "unknown_override_key";
/// An assertion whose form does not match its target's declared rank (esm-spec §6.6.5).
ASSERTION_RANK_MISMATCH = "assertion_rank_mismatch";
/// A provable dimensional inconsistency, promoted from a unit finding.
UNIT_INCONSISTENCY = "unit_inconsistency";
/// A declared unit string that denotes no real unit, promoted from a
/// unit finding.
UNIT_PARSE_ERROR = "unit_parse_error";
/// A reference to an index set the document does not declare.
UNDEFINED_INDEX_SET = "undefined_index_set";
/// A reference to an operator the registry does not carry.
UNDEFINED_OPERATOR = "undefined_operator";
/// A reference to a parameter the component does not declare.
UNDEFINED_PARAMETER = "undefined_parameter";
/// A reference to a species the reaction system does not declare.
UNDEFINED_SPECIES = "undefined_species";
/// A coupling endpoint naming no declared component.
UNDEFINED_SYSTEM = "undefined_system";
/// A reference to a variable the component does not declare.
UNDEFINED_VARIABLE = "undefined_variable";
/// A string subscript of an `index` on an equation's left-hand side that
/// no enclosing left-hand `faq` binds — nor, for the bare-index definition,
/// the right-hand `faq`'s `output_idx` (esm-spec §6.3.1).
UNBOUND_INDEX_SYMBOL = "unbound_index_symbol";
/// An evaluable-core op (esm-spec §4.2) with no evaluation rule in the
/// evaluator a model was built for (esm-spec §9.6.6). Carried by
/// `CompileError::UnevaluableOperatorError`.
UNEVALUABLE_OPERATOR = "unevaluable_operator";
/// A scoped reference (`A.b`) that resolves to nothing.
UNRESOLVED_SCOPED_REF = "unresolved_scoped_ref";
// ---- unit FINDING kinds (`units.rs`): a second, smaller vocabulary
// carried on `UnitWarning.code` rather than on a `StructuralError`. The
// first two state a defect in the FILE, the third a limit of the ANALYSIS.
// Shared verbatim with Go's `UnitFinding*` and TypeScript's
// `UnitWarning['code']` union. ----
/// A PROVABLE dimensional inconsistency.
DIMENSIONAL_MISMATCH = "dimensional_mismatch";
/// A declared unit string that does not denote a real unit.
UNPARSEABLE_UNIT = "unparseable_unit";
/// The checker cannot DETERMINE a dimension — not a defect in the file.
ANALYSIS = "analysis";
// ---- closed-function registry: esm-spec §9.1-§9.2
// (`registered_functions.rs`, raised as `ClosedFunctionError`). ----
/// A call to a function the closed registry does not carry.
UNKNOWN_CLOSED_FUNCTION = "unknown_closed_function";
/// A closed-function call at an arity the function does not accept.
CLOSED_FUNCTION_ARITY = "closed_function_arity";
/// A closed-function result that would overflow.
CLOSED_FUNCTION_OVERFLOW = "closed_function_overflow";
/// A closed-function argument of a kind the function does not accept.
CLOSED_FUNCTION_ARG_TYPE = "closed_function_arg_type";
/// A `searchsorted` table that is not monotonically non-decreasing.
SEARCHSORTED_NON_MONOTONIC = "searchsorted_non_monotonic";
/// A `searchsorted` table containing a NaN.
SEARCHSORTED_NAN_IN_TABLE = "searchsorted_nan_in_table";
/// An `interp` axis that is not strictly increasing.
INTERP_NON_MONOTONIC_AXIS = "interp_non_monotonic_axis";
/// An `interp` axis whose length does not match the table's.
INTERP_AXIS_LENGTH_MISMATCH = "interp_axis_length_mismatch";
/// An `interp` axis containing a NaN.
INTERP_NAN_IN_AXIS = "interp_nan_in_axis";
/// An `interp` axis with fewer than two points.
INTERP_AXIS_TOO_SHORT = "interp_axis_too_short";
}
/// Every diagnostic code string in [`ERROR_CODES`], sorted. Handy for
/// cross-binding vocabulary diffs and for asserting in a test that a raise
/// site uses a registered code. Mirrors Julia's `error_code_names()`.
pub fn error_code_names() -> Vec<&'static str> {
let mut values: Vec<&'static str> = ERROR_CODES.iter().map(|(_, value)| *value).collect();
values.sort_unstable();
values
}
/// Parse a `major.minor.patch` version string into its numeric components.
/// Returns `None` for anything that is not exactly three dot-separated
/// non-negative integers. Shared by the load-time spec-version gates, the
/// migration module, and version-compatibility checking, so all agree on
/// what counts as a well-formed version token.
pub fn parse_semver(version: &str) -> Option<(u32, u32, u32)> {
let mut parts = version.split('.');
let major = parts.next()?.parse().ok()?;
let minor = parts.next()?.parse().ok()?;
let patch = parts.next()?.parse().ok()?;
if parts.next().is_some() {
return None;
}
Some((major, minor, patch))
}
#[cfg(test)]
mod error_code_tests {
use super::{ERROR_CODES, error_code_names};
/// The registry invariants Julia's `error_hierarchy_test.jl` and
/// TypeScript's `errors.test.ts` assert on their own registries: each name
/// is the SCREAMING_SNAKE_CASE form of its value, and no value repeats.
#[test]
fn error_codes_are_well_formed() {
for (name, value) in ERROR_CODES {
assert_eq!(
*name,
value.to_uppercase(),
"registry name {name} is not the SCREAMING_SNAKE form of {value}"
);
assert!(
value
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_'),
"code {value} is not snake_case"
);
}
let mut seen = error_code_names();
let count = seen.len();
seen.dedup();
assert_eq!(count, seen.len(), "duplicate code value in ERROR_CODES");
}
/// The full vocabulary, pinned value by value. The code strings are a
/// cross-binding contract (`tests/invalid/expected_errors.json` and the
/// peer bindings' registries match on them), so a registry edit that
/// drops, renames, or retypes one must fail here rather than surface as a
/// conformance break later.
#[test]
fn the_diagnostic_vocabulary_is_pinned() {
let expected: Vec<&str> = vec![
"ambiguous_output_name",
"ambiguous_subsystem_ref",
"analysis",
"apply_expression_template_bindings_mismatch",
"apply_expression_template_invalid_declaration",
"apply_expression_template_recursive_body",
"apply_expression_template_unknown_template",
"apply_expression_template_version_too_old",
"array_default_without_shape",
"array_shape_mismatch",
"assertion_rank_mismatch",
"callback_unregistered",
"circular_dependency",
"closed_function_arg_type",
"closed_function_arity",
"closed_function_overflow",
"compiler_refused_rule",
"compiler_unavailable",
"compiler_unknown",
"const_units_version_too_old",
"coupling_edge_unknown_role",
"coupling_import_bind_not_a_component",
"coupling_import_not_library",
"coupling_import_role_unbound",
"coupling_import_unknown_role",
"coupling_import_unresolved",
"coupling_library_illegal_payload",
"coupling_library_nested_import",
"coupling_role_unused",
"data_source_unbound",
"data_source_undefined",
"data_source_url_unresolved",
"derived_index_set_unmaterialized",
"dimensional_mismatch",
"domain_unit_mismatch",
"element_type_uncarried",
"enum_invalid_args",
"enum_lowering_residual",
"equation_count_mismatch",
"equation_defines_parameter",
"event_affects_parameter",
"event_var_undeclared",
"factor_with_expression_transform",
"geometry_manifold_invalid",
"ic_in_reaction_system",
"indexed_definition_unsupported_form",
"interp_axis_length_mismatch",
"interp_axis_too_short",
"interp_nan_in_axis",
"interp_non_monotonic_axis",
"invalid_broadcast_fn",
"invalid_enums_block",
"join_key_invalid_type",
"join_side_ambiguous",
"join_syms_unknown_symbol",
"makearray_region_inverted",
"metaparameter_name_conflict",
"metaparameter_type_error",
"metaparameter_unbound",
"mount_form_unsupported",
"null_reaction",
"observed_cycle",
"operator_variable_missing",
"ragged_values_not_gathered",
"range_box_invalid",
"range_global_locality",
"range_halo_insufficient",
"range_selection_writes_incomplete",
"recurrence_not_wellfounded",
"recurrence_unsupported_form",
"relational_node_in_continuous",
"reserved_index_symbol",
"reserved_variable_name",
"rewrite_rule_nonterminating",
"searchsorted_nan_in_table",
"searchsorted_non_monotonic",
"solver_external_unresolved",
"solver_external_version_too_old",
"solver_time_span_invalid",
"solver_time_span_version_too_old",
"solver_version_too_old",
"subsystem_index_set_conflict",
"subsystem_index_set_rename_unknown_name",
"subsystem_index_set_rename_unsupported_mount_form",
"subsystem_ref_is_coupling_library",
"subsystem_ref_is_template_library",
"table_axis_nan",
"table_data_shape_mismatch",
"table_interpolation_axes_mismatch",
"table_lookup_axis_name_mismatch",
"table_lookup_out_of_bounds",
"table_lookup_output_out_of_range",
"table_lookup_unknown_table",
"table_out_of_bounds_unsupported",
"template_body_expansion_too_deep",
"template_body_references_coupling_rewritten_variable",
"template_constraint_unknown_index_set",
"template_import_cycle",
"template_import_index_set_conflict",
"template_import_is_coupling_library",
"template_import_name_conflict",
"template_import_not_library",
"template_import_rebind_unknown_name",
"template_import_rename_collision",
"template_import_rename_invalid",
"template_import_rename_unknown_name",
"template_import_unknown_name",
"template_import_unresolved",
"template_import_version_too_old",
"template_inject_target_is_loader",
"template_inject_target_not_component",
"template_inject_target_unknown",
"template_library_illegal_payload",
"toplevel_model_ref_unresolved",
"unbound_index_symbol",
"undefined_index_set",
"undefined_operator",
"undefined_parameter",
"undefined_species",
"undefined_system",
"undefined_variable",
"unevaluable_operator",
"unit_inconsistency",
"unit_parse_error",
"unknown_closed_function",
"unknown_enum",
"unknown_enum_symbol",
"unknown_override_key",
"unlowered_operator",
"unparseable_unit",
"unresolved_scoped_ref",
"unresolved_subsystem_ref",
"unsupported_construct",
];
assert_eq!(error_code_names(), expected);
}
/// The code column of the esm-spec §9.6.6 table. That table is
/// "cross-language uniform", so every binding's registry must carry it.
fn spec_diagnostic_codes() -> Vec<String> {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../esm-spec.md");
let spec = std::fs::read_to_string(path).expect("read esm-spec.md");
let section = spec
.split("\n#### ")
.find(|part| part.starts_with("9.6.6 "))
.expect("esm-spec.md has no §9.6.6 heading");
section
.lines()
.filter_map(|line| {
let rest = line.strip_prefix("| `")?;
let code = &rest[..rest.find("` |")?];
code.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
.then(|| code.to_string())
})
.collect()
}
#[test]
fn every_spec_diagnostic_code_is_registered() {
let codes = spec_diagnostic_codes();
// Guard the extraction: a heading or table-layout change that matched
// nothing would pass the membership check vacuously.
assert!(
codes.len() >= 30,
"extracted only {} codes from the §9.6.6 table",
codes.len()
);
let registered = error_code_names();
let missing: Vec<&String> = codes
.iter()
.filter(|c| !registered.contains(&c.as_str()))
.collect();
assert!(
missing.is_empty(),
"§9.6.6 codes missing from ERROR_CODES: {missing:?}"
);
}
/// The structural-validation codes render off the registry, so the
/// `StructuralError.error_type` wire values stay pinned to it.
#[test]
fn structural_error_codes_come_from_the_registry() {
use crate::validate::StructuralErrorCode;
assert_eq!(
StructuralErrorCode::UndefinedVariable.to_string(),
super::codes::UNDEFINED_VARIABLE
);
assert_eq!(
StructuralErrorCode::ArrayShapeMismatch.to_string(),
super::codes::ARRAY_SHAPE_MISMATCH
);
assert_eq!(
StructuralErrorCode::ObservedCycle.to_string(),
super::codes::OBSERVED_CYCLE
);
}
}