// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// Compile AST to IR
///
/// This module converts the parsed AST into IR, resolving:
/// - Variable substitutions
/// - Import statements
/// - Glob patterns
///|
/// Compilation error
pub(all) suberror CompileError {
InvalidKey(String)
InvalidValue(String)
DuplicateField(String)
InvalidSubstitution(String)
ImportError(String)
} derive(Eq, Debug)
///|
pub impl Show for CompileError with fn output(self, logger) {
match self {
InvalidKey(message) => {
logger.write_string("InvalidKey(")
logger.write_object(message)
logger.write_char(')')
}
InvalidValue(message) => {
logger.write_string("InvalidValue(")
logger.write_object(message)
logger.write_char(')')
}
DuplicateField(message) => {
logger.write_string("DuplicateField(")
logger.write_object(message)
logger.write_char(')')
}
InvalidSubstitution(message) => {
logger.write_string("InvalidSubstitution(")
logger.write_object(message)
logger.write_char(')')
}
ImportError(message) => {
logger.write_string("ImportError(")
logger.write_object(message)
logger.write_char(')')
}
}
}
///|
priv struct ResolvedPath {
names : Array[String]
syntax : Array[@ast.StringValue]
}
///|
priv struct RecursiveEdgeTarget {
parent : Map
edge : Edge
src_path : ResolvedPath
dst_path : ResolvedPath
}
///|
/// One lexical object scope between the map currently being compiled and its
/// parent. `parent` is the map reached by one leading `_`; `child_syntax` is the
/// object path segment that must be restored on an endpoint which does not
/// climb while the other endpoint does.
priv struct CompileScopeFrame {
parent : Map
child_syntax : @ast.StringValue
}
///|
fn CompileScopeFrame::new(
parent : Map,
child_syntax : @ast.StringValue,
) -> CompileScopeFrame {
{ parent, child_syntax }
}
///|
fn ResolvedPath::new(
names : Array[String],
syntax : Array[@ast.StringValue],
) -> ResolvedPath {
{ names, syntax }
}
///|
fn path_is_recursive_glob(path : @ast.KeyPath) -> Bool {
if path.path.length() != 1 {
return false
}
match path.path[0] {
Unquoted(value) =>
is_double_glob(value.pattern) || is_triple_glob(value.pattern)
_ => false
}
}
///|
fn append_resolved_paths(
prefix : ResolvedPath,
suffix_names : Array[String],
suffix_syntax : Array[@ast.StringValue],
) -> ResolvedPath {
let result = ResolvedPath::new(prefix.names.copy(), prefix.syntax.copy())
for i = 0; i < suffix_names.length(); i = i + 1 {
result.names.push(suffix_names[i])
result.syntax.push(suffix_syntax[i])
}
result
}
///|
fn collect_recursive_edge_targets(
ir : Map,
prefix : ResolvedPath,
include_boards : Bool,
targets : Array[RecursiveEdgeTarget],
) -> Unit {
for edge in ir.edges {
targets.push({
parent: ir,
edge,
src_path: append_resolved_paths(
prefix,
edge.id.src_path,
edge.id.src_path_syntax,
),
dst_path: append_resolved_paths(
prefix,
edge.id.dst_path,
edge.id.dst_path_syntax,
),
})
}
for field in ir.fields {
guard field.composite is Some(Map(child)) else { continue }
if field.is_board_keyword_name() {
if include_boards {
for board in child.fields {
match board.composite {
Some(Map(board_map)) =>
collect_recursive_edge_targets(
board_map,
ResolvedPath::new([], []),
include_boards,
targets,
)
_ => ()
}
}
}
continue
}
if field.is_reserved_keyword_name() {
continue
}
let syntax = match field.name_syntax() {
Some(syntax) => syntax
None => synthetic_unquoted_string(field.name)
}
collect_recursive_edge_targets(
child,
append_path_segment(prefix, field.name, syntax),
include_boards,
targets,
)
}
}
///|
/// Compile an AST Map to IR Map
pub fn compile(ast : @ast.Map) -> Map raise CompileError {
let ir = Map::from_ast(ast)
compile_map_non_glob_nodes(ir, ast, ast.nodes, [], None)
resolve_boards(ir)
// Root *** globs must run against materialized boards. Board resolution
// recompiles each board patch, so applying them earlier loses the defaults.
compile_map_glob_nodes(ir, ast, ast.nodes, [], None)
// Resolve variable substitutions
ir.resolve_substitutions()
apply_suspensions(ir)
ir.reindex_paths()
}
///|
fn compile_map_nodes(
ir : Map,
scope_ast : @ast.Map,
nodes : Array[@ast.MapNode],
source_path? : String? = None,
) -> Unit raise CompileError {
compile_map_nodes_in_scope(ir, scope_ast, nodes, [], source_path)
}
///|
fn compile_map_nodes_in_scope(
ir : Map,
scope_ast : @ast.Map,
nodes : Array[@ast.MapNode],
scope_frames : Array[CompileScopeFrame],
source_path : String?,
) -> Unit raise CompileError {
compile_map_non_glob_nodes(ir, scope_ast, nodes, scope_frames, source_path)
compile_map_glob_nodes(ir, scope_ast, nodes, scope_frames, source_path)
}
///|
fn compile_map_non_glob_nodes(
ir : Map,
scope_ast : @ast.Map,
nodes : Array[@ast.MapNode],
scope_frames : Array[CompileScopeFrame],
source_path : String?,
) -> Unit raise CompileError {
for node in nodes {
match node {
Key(key) =>
if !key_uses_glob(key) {
compile_key(ir, key, scope_ast, scope_frames, source_path)
}
Comment(_, _) => ()
BlockComment(_, _) => ()
Substitution(sub) =>
ir.add_field(
Field::new("", Some(substitution_scalar(sub, source_path)), None, []),
)
Import(_) =>
raise ImportError(
"imports require a resolver; use compile_with_imports",
)
}
}
}
///|
fn compile_map_glob_nodes(
ir : Map,
scope_ast : @ast.Map,
nodes : Array[@ast.MapNode],
scope_frames : Array[CompileScopeFrame],
source_path : String?,
) -> Unit raise CompileError {
for node in nodes {
match node {
Key(key) =>
if key_uses_glob(key) {
compile_key(ir, key, scope_ast, scope_frames, source_path)
}
_ => ()
}
}
}
///|
fn key_uses_glob(key : @ast.Key) -> Bool {
match key.key {
Some(path) => if path.has_glob() { return true }
None => ()
}
match key.edge_index {
Some(index) => if index.glob { return true }
None => ()
}
for edge in key.edges {
if edge.src.has_glob() || edge.dst.has_glob() {
return true
}
}
false
}
///|
fn is_parent_segment(segment : @ast.StringValue) -> Bool {
match segment {
Unquoted(value) => value.pattern.is_empty() && value.to_string() == "_"
_ => false
}
}
///|
fn leading_parent_count(path : @ast.KeyPath) -> Int {
let mut count = 0
for segment in path.path {
if !is_parent_segment(segment) {
break
}
count = count + 1
}
count
}
///|
fn scalar_suspension(scalar : @ast.Scalar) -> Bool? {
match scalar {
String(Unquoted(value)) =>
if value.pattern.is_empty() {
match value.to_string() {
"suspend" => Some(true)
"unsuspend" => Some(false)
_ => None
}
} else {
None
}
_ => None
}
}
///|
fn key_suspension(key : @ast.Key) -> Bool? {
match key.primary {
Some(scalar) =>
match scalar_suspension(scalar) {
Some(value) => return Some(value)
None => ()
}
None => ()
}
match key.value {
Some(Scalar(scalar)) => scalar_suspension(scalar)
Some(BlockScalar(Scalar(scalar), _)) => scalar_suspension(scalar)
_ => None
}
}
///|
fn record_field_reference(
m : Map,
f : Field,
elem : @ast.StringValue,
key_path : @ast.KeyPath,
context : RefContext,
key_path_index : Int,
primary : Bool,
due_to_glob? : Bool = false,
due_to_lazy_glob? : Bool = false,
) -> Field {
let refs = f.references.copy()
refs.push(
FieldReference::from_ast(
elem,
key_path,
key_path_index,
primary,
context,
due_to_glob~,
due_to_lazy_glob~,
),
)
let updated = Field::new(
f.name,
f.primary,
f.composite,
refs,
name_syntax=f.name_syntax(),
)
m.set_field(updated)
updated
}
///|
fn compile_key(
ir : Map,
key : @ast.Key,
scope_ast : @ast.Map,
scope_frames : Array[CompileScopeFrame],
source_path : String?,
) -> Unit raise CompileError {
// Handle ampersand filters (they are evaluated during glob expansion)
if key.ampersand || key.not_ampersand {
// Filters are collected and applied during glob expansion
// They don't create fields themselves
return
}
// Handle edges first
if key.edges.length() > 0 {
compile_edges(ir, key, scope_ast, scope_frames, source_path)
return
}
// Handle regular key
guard key.key is Some(key_path) else { return }
// Check for glob pattern
if key_path.has_glob() {
compile_glob_key(ir, key_path, key, scope_ast, source_path)
return
}
let context = RefContext::new(None, Some(key), Some(scope_ast), source_path~)
let path = key_path.to_strings()
if path.length() == 0 {
return
}
let parent_count = leading_parent_count(key_path)
if parent_count > scope_frames.length() {
raise InvalidKey("invalid underscore: no parent")
}
if parent_count == path.length() {
raise InvalidKey("field key must contain more than underscores")
}
// Navigate/create the field path
let mut current_map = if parent_count == 0 {
ir
} else {
scope_frames[scope_frames.length() - parent_count].parent
}
let current_frames : Array[CompileScopeFrame] = []
for i = 0; i < scope_frames.length() - parent_count; i = i + 1 {
current_frames.push(scope_frames[i])
}
for i = parent_count; i < path.length() - 1; i = i + 1 {
let elem = key_path.path[i]
let field = record_field_reference(
current_map,
current_map.ensure_field_by_syntax(elem),
elem,
key_path,
context,
i,
false,
)
// Ensure field has a map composite
current_frames.push(CompileScopeFrame::new(current_map, elem))
match field.composite {
Some(Map(m)) => current_map = m
None => {
let new_map = Map::new()
let field = Field::new(
field.name,
field.primary,
Some(Map(new_map)),
field.references,
name_syntax=field.name_syntax(),
)
current_map.set_field(field)
current_map = new_map
}
Some(_) => raise InvalidKey("field already has non-map value")
}
}
// Set the final field
let last_index = path.length() - 1
let last_elem = key_path.path[last_index]
let field0 = current_map.ensure_field_by_syntax(last_elem)
let field1 = if key.primary is Some(_) || key.value is None {
record_field_reference(
current_map,
field0,
last_elem,
key_path,
context,
last_index,
key.primary is Some(_),
)
} else {
field0
}
let field = if key.value is Some(_) {
record_field_reference(
current_map, field1, last_elem, key_path, context, last_index, true,
)
} else {
field1
}
if key_suspension(key) is Some(_) {
current_map.set_field(field)
return
}
// Set primary value
let field = match key.primary {
Some(scalar) =>
Field::new(
field.name,
Some(compile_scalar(scalar, source_path)),
field.composite,
field.references,
name_syntax=field.name_syntax(),
)
None => field
}
// Set value
let field = match key.value {
Some(value) =>
compile_value_in_scope(
field, value, current_map, current_frames, source_path,
)
None => field
}
current_map.set_field(field)
}
///|
/// Compile a key with glob pattern
fn compile_glob_key(
ir : Map,
key_path : @ast.KeyPath,
key : @ast.Key,
scope_ast : @ast.Map,
source_path : String?,
) -> Unit raise CompileError {
// Find the glob element and extract pattern
let (pattern, glob_index) = extract_glob_and_path(key_path)
if pattern.length() == 0 || glob_index < 0 {
return // No valid glob pattern
}
// Collect filters from the value map if present
let filters = collect_filters_from_value(key)
// Build properties map from the remaining path and value
let context = RefContext::new(None, Some(key), Some(scope_ast), source_path~)
let suspension = key_suspension(key)
let properties = build_nested_properties(
key_path, glob_index, key, context, source_path,
)
// A glob may be qualified by a literal path, for example
// `cluster.services.pod*.class`. Match the glob in that path's map instead
// of silently treating it as a root-level pattern.
let mut target_map = ir
for i = 0; i < glob_index; i = i + 1 {
guard target_map.get_field_by_syntax(key_path.path[i]) is Some(field) else {
return
}
match field.composite {
Some(Map(next)) => target_map = next
_ => return
}
}
let targets = target_map.glob_targets(pattern)
// Apply descendants before ancestors. Applying a property clones a field's
// composite map, so parent-first updates would detach the child maps captured
// by `glob_targets` (most visibly for *** defaults inside boards).
let mut target_index = targets.length() - 1
while target_index >= 0 {
let target = targets[target_index]
let field = target.field
let parent_map = target.parent_map
let live_field = parent_map.get_field(field.name).unwrap_or(field)
let filter_context = FilterContext::new(live_field, parent_map)
let mut passes = true
for filter in filters {
let (negated, filter_key, filter_value) = filter
let matched = evaluate_filter_at_level(
filter_context,
filter_key,
filter_value,
target.level,
)
if (if negated { matched } else { !matched }) {
passes = false
break
}
}
if passes {
match suspension {
Some(_) =>
if glob_operation_follows_field(live_field, key, source_path) {
let syntax = match live_field.name_syntax() {
Some(syntax) => syntax
None => synthetic_unquoted_string(live_field.name)
}
let _ = record_field_reference(
parent_map,
live_field,
syntax,
key_path,
context,
glob_index,
true,
due_to_glob=true,
)
}
None => {
let updated = apply_properties_to_field(live_field, properties)
parent_map.set_field(updated)
apply_nested_globs(updated, key.value, source_path)
}
}
}
target_index = target_index - 1
}
}
///|
fn glob_operation_follows_field(
field : Field,
key : @ast.Key,
source_path : String?,
) -> Bool {
guard field.last_ref() is Some(reference) else { return true }
match reference.context() {
Some(existing_context) =>
if existing_context.source_path != source_path {
return true
}
None => return true
}
key.range.start.offset >= reference_operation_range(reference).start.offset
}
///|
fn apply_nested_globs(
field : Field,
value : @ast.Value?,
source_path : String?,
) -> Unit raise CompileError {
guard value is Some(Map(ast_map)) else { return }
guard field.composite is Some(Map(field_map)) else { return }
for node in ast_map.nodes {
match node {
Key(nested_key) =>
if !nested_key.ampersand &&
!nested_key.not_ampersand &&
key_uses_glob(nested_key) {
compile_key(field_map, nested_key, ast_map, [], source_path)
}
_ => ()
}
}
}
///|
/// Extract glob pattern and remaining path from key path
/// Returns (pattern, remaining_path) where pattern is from the glob element
/// and remaining_path is the rest of the key path after the glob
fn extract_glob_and_path(key_path : @ast.KeyPath) -> (Array[String], Int) {
let path = key_path.path
let mut glob_index = -1
let mut pattern : Array[String] = []
// Find the first element with a glob pattern
for i, elem in path {
match elem {
Unquoted(us) =>
if us.pattern.length() > 0 {
pattern = us.pattern
glob_index = i
break
}
_ => ()
}
}
if glob_index < 0 {
return ([], -1)
}
(pattern, glob_index)
}
///|
/// Build nested properties map from remaining path and key value
fn build_nested_properties(
key_path : @ast.KeyPath,
glob_index : Int,
key : @ast.Key,
context : RefContext,
source_path : String?,
) -> Map raise CompileError {
let props = Map::new()
let remaining_path : Array[@ast.StringValue] = []
for i = glob_index + 1; i < key_path.path.length(); i = i + 1 {
remaining_path.push(key_path.path[i])
}
if remaining_path.length() == 0 {
// Compile non-glob properties exactly as ordinary keys so dotted paths,
// maps, and arrays retain their normal semantics. Nested globs are applied
// separately to each matched field.
match key.value {
Some(Map(m)) =>
for node in m.nodes {
match node {
Key(prop_key) =>
if !prop_key.ampersand &&
!prop_key.not_ampersand &&
!key_uses_glob(prop_key) {
compile_key(props, prop_key, m, [], source_path)
}
_ => ()
}
}
_ => ()
}
} else {
// Build nested structure: style.fill: red -> { style: { fill: red } }
let mut current = props
for i = 0; i < remaining_path.length() - 1; i = i + 1 {
let elem = remaining_path[i]
let name = elem.content()
let field = Field::new(
name,
None,
None,
[
FieldReference::from_ast(
elem,
key_path,
glob_index + 1 + i,
false,
context,
due_to_glob=true,
),
],
name_syntax=Some(elem),
)
let inner = Map::new()
let field = Field::new(
field.name,
field.primary,
Some(Map(inner)),
field.references,
name_syntax=field.name_syntax(),
)
current.add_field(field)
current = inner
}
// Set the final value
let final_elem = remaining_path[remaining_path.length() - 1]
let final_name = final_elem.content()
let final_index = glob_index + remaining_path.length()
let final_field = Field::new(
final_name,
None,
None,
[
FieldReference::from_ast(
final_elem,
key_path,
final_index,
true,
context,
due_to_glob=true,
),
],
name_syntax=Some(final_elem),
)
// Compile glob properties through the same value path as ordinary keys so
// arrays and maps retain their full IR representation.
let final_field = match key.primary {
Some(scalar) =>
Field::new(
final_field.name,
Some(compile_scalar(scalar, source_path)),
final_field.composite,
{
let refs = final_field.references.copy()
refs.push(
FieldReference::from_ast(
final_elem,
key_path,
final_index,
true,
context,
due_to_glob=true,
),
)
refs
},
name_syntax=final_field.name_syntax(),
)
None => final_field
}
let final_field = match key.value {
Some(value) => compile_value(final_field, value, source_path)
None => final_field
}
current.add_field(final_field)
}
props
}
///|
/// Collect filter conditions from the key's value map
fn collect_filters_from_value(key : @ast.Key) -> Array[(Bool, String, String)] {
let filters : Array[(Bool, String, String)] = []
match key.value {
Some(Map(m)) | Some(BlockScalar(_, m)) =>
for node in m.nodes {
match node {
Key(filter_key) =>
if filter_key.ampersand || filter_key.not_ampersand {
// This is a filter
match (filter_key.key, filter_key.value) {
(Some(fk), Some(Scalar(sv))) => {
let filter_name = fk.to_strings().join(".")
let filter_value = compile_scalar(sv, None).to_string()
filters.push(
(filter_key.not_ampersand, filter_name, filter_value),
)
}
_ => ()
}
}
_ => ()
}
}
_ => ()
}
filters
}
///|
fn edge_endpoint_paths(
ir : Map,
key_path : @ast.KeyPath,
) -> Array[ResolvedPath] {
if !key_path.has_glob() {
return [ResolvedPath::new(key_path.to_strings(), key_path.path.copy())]
}
let out : Array[ResolvedPath] = []
expand_edge_endpoint_paths(
ir,
key_path.path,
0,
ResolvedPath::new([], []),
out,
)
out
}
///|
fn expand_edge_endpoint_paths(
current_map : Map,
segments : Array[@ast.StringValue],
index : Int,
current_path : ResolvedPath,
out : Array[ResolvedPath],
) -> Unit {
if index >= segments.length() {
out.push(current_path)
return
}
let segment = segments[index]
let is_last = index == segments.length() - 1
match segment {
Unquoted(us) =>
if is_double_glob(us.pattern) || is_triple_glob(us.pattern) {
expand_multi_glob_edge_endpoint_paths(
current_map,
segments,
index,
current_path,
out,
is_triple_glob(us.pattern),
)
} else if us.pattern.length() > 0 {
let candidates = current_map.single_glob(us.pattern)
for field in candidates {
let next_path = append_path_segment(
current_path,
field.name,
match field.name_syntax() {
Some(name_syntax) => name_syntax
None => synthetic_unquoted_string(field.name)
},
)
if is_last {
out.push(next_path)
} else {
match field.composite {
Some(Map(m)) =>
expand_edge_endpoint_paths(
m,
segments,
index + 1,
next_path,
out,
)
_ => ()
}
}
}
} else {
let segment_name = segment.content()
match current_map.get_field_by_syntax(segment) {
Some(field) => {
let next_path = append_path_segment(
current_path, segment_name, segment,
)
if is_last {
out.push(next_path)
} else {
match field.composite {
Some(Map(m)) =>
expand_edge_endpoint_paths(
m,
segments,
index + 1,
next_path,
out,
)
_ => ()
}
}
}
None => ()
}
}
_ => {
let segment_name = segment.content()
match current_map.get_field_by_syntax(segment) {
Some(field) => {
let next_path = append_path_segment(
current_path, segment_name, segment,
)
if is_last {
out.push(next_path)
} else {
match field.composite {
Some(Map(m)) =>
expand_edge_endpoint_paths(
m,
segments,
index + 1,
next_path,
out,
)
_ => ()
}
}
}
None => ()
}
}
}
}
///|
fn expand_multi_glob_edge_endpoint_paths(
current_map : Map,
segments : Array[@ast.StringValue],
index : Int,
current_path : ResolvedPath,
out : Array[ResolvedPath],
include_boards : Bool,
) -> Unit {
let is_last = index == segments.length() - 1
for field in current_map.fields {
let child_map = match field.composite {
Some(Map(map)) => Some(map)
_ => None
}
if field.is_board_keyword_name() {
if include_boards {
match child_map {
Some(boards) =>
for board in boards.fields {
match board.composite {
Some(Map(board_map)) =>
expand_multi_glob_edge_endpoint_paths(
board_map,
segments,
index,
ResolvedPath::new([], []),
out,
include_boards,
)
_ => ()
}
}
None => ()
}
}
continue
}
if field.is_reserved_keyword_name() {
continue
}
let syntax = match field.name_syntax() {
Some(name_syntax) => name_syntax
None => synthetic_unquoted_string(field.name)
}
let next_path = append_path_segment(current_path, field.name, syntax)
if is_last {
if field_is_leaf(field) {
out.push(next_path)
}
} else {
match child_map {
Some(map) =>
expand_edge_endpoint_paths(map, segments, index + 1, next_path, out)
None => ()
}
}
match child_map {
Some(map) =>
expand_multi_glob_edge_endpoint_paths(
map, segments, index, next_path, out, include_boards,
)
None => ()
}
}
}
///|
fn append_path_segment(
path : ResolvedPath,
segment : String,
syntax : @ast.StringValue,
) -> ResolvedPath {
let names : Array[String] = []
for p in path.names {
names.push(p)
}
names.push(segment)
let syntax_path : Array[@ast.StringValue] = []
for part in path.syntax {
syntax_path.push(part)
}
syntax_path.push(syntax)
ResolvedPath::new(names, syntax_path)
}
///|
fn string_path_equal(a : ResolvedPath, b : ResolvedPath) -> Bool {
if a.names.length() != b.names.length() {
return false
}
for i = 0; i < a.names.length(); i = i + 1 {
if a.names[i] != b.names[i] {
return false
}
}
true
}
///|
fn edge_path_after_parent_resolution(
path : @ast.KeyPath,
scope_frames : Array[CompileScopeFrame],
climb : Int,
) -> @ast.KeyPath {
let resolved : Array[@ast.StringValue] = []
let mut consumed = 0
for step = 0; step < climb; step = step + 1 {
if consumed < path.path.length() && is_parent_segment(path.path[consumed]) {
consumed = consumed + 1
} else {
let frame = scope_frames[scope_frames.length() - 1 - step]
resolved.insert(0, frame.child_syntax)
}
}
for i = consumed; i < path.path.length(); i = i + 1 {
resolved.push(path.path[i])
}
@ast.KeyPath::new(path.range, resolved)
}
///|
fn parent_edge_scope(
ir : Map,
scope_frames : Array[CompileScopeFrame],
climb : Int,
) -> Map raise CompileError {
if climb == 0 {
return ir
}
if climb > scope_frames.length() {
raise InvalidKey("invalid underscore: no parent")
}
scope_frames[scope_frames.length() - climb].parent
}
///|
fn compile_edges(
scope_ir : Map,
key : @ast.Key,
scope_ast : @ast.Map,
scope_frames : Array[CompileScopeFrame],
source_path : String?,
) -> Unit raise CompileError {
for edge in key.edges {
let src_parent_count = leading_parent_count(edge.src)
let dst_parent_count = leading_parent_count(edge.dst)
let climb = if src_parent_count > dst_parent_count {
src_parent_count
} else {
dst_parent_count
}
let ir = parent_edge_scope(scope_ir, scope_frames, climb)
let src = edge_path_after_parent_resolution(edge.src, scope_frames, climb)
let dst = edge_path_after_parent_resolution(edge.dst, scope_frames, climb)
let edge_context = RefContext::new(
Some(edge),
Some(key),
Some(scope_ast),
source_path~,
)
let src_paths = edge_endpoint_paths(ir, src)
let dst_paths = edge_endpoint_paths(ir, dst)
let filters = collect_filters_from_value(key)
let src_has_glob = edge.src.has_glob()
let dst_has_glob = edge.dst.has_glob()
if src_paths.is_empty() || dst_paths.is_empty() {
continue
}
fn next_index_for_group(ir : Map, base_id : EdgeID) -> Int {
let existing = ir.find_edges(base_id)
let mut max_idx = -1
for e in existing {
match e.id.index {
Some(i) => if i > max_idx { max_idx = i }
None => if max_idx < 0 { max_idx = 0 }
}
}
max_idx + 1
}
fn find_exact_edge(ir : Map, id : EdgeID) -> Edge? {
for e in ir.edges {
if e.id.matches(id) {
return Some(e)
}
}
None
}
fn compile_edge_value(
existing : Edge?,
value : @ast.Value?,
) -> (Scalar?, Map?) raise CompileError {
if key_suspension(key) is Some(_) {
return match existing {
Some(edge) => (edge.primary, edge.map)
None => (None, None)
}
}
let (patch_primary, patch_map) = match value {
Some(Scalar(s)) => (Some(compile_scalar(s, source_path)), None)
Some(Map(m)) => {
let ir_map = Map::from_ast(m, source_path~)
compile_map_nodes(ir_map, m, m.nodes, source_path~)
(None, Some(ir_map))
}
Some(BlockScalar(label, m)) => {
let primary = match label {
Scalar(s) => Some(compile_scalar(s, source_path))
_ => None
}
let ir_map = Map::from_ast(m, source_path~)
compile_map_nodes(ir_map, m, m.nodes, source_path~)
(primary, Some(ir_map))
}
_ => (None, None)
}
let index_glob = match key.edge_index {
Some(index) => index.glob
None => false
}
let is_glob_update = src_has_glob || dst_has_glob || index_glob
let patch_overrides = !is_glob_update ||
glob_edge_patch_follows_existing(existing, key.range.start.offset)
apply_edge_value(
existing,
key.edge_key,
patch_primary,
patch_map,
patch_overrides~,
)
}
if path_is_recursive_glob(src) &&
path_is_recursive_glob(dst) &&
key.edge_index.map(fn(index) { index.glob }).unwrap_or(false) {
let targets : Array[RecursiveEdgeTarget] = []
let include_boards = match (src.path[0], dst.path[0]) {
(Unquoted(a), Unquoted(b)) =>
is_triple_glob(a.pattern) || is_triple_glob(b.pattern)
_ => false
}
collect_recursive_edge_targets(
ir,
ResolvedPath::new([], []),
include_boards,
targets,
)
for target in targets {
if target.edge.id.src_arrow != (edge.src_arrow == "<") ||
target.edge.id.dst_arrow != (edge.dst_arrow == ">") {
continue
}
if key_suspension(key) is Some(_) &&
!glob_edge_patch_follows_existing(
Some(target.edge),
key.range.start.offset,
) {
continue
}
if !edge_glob_filters_pass(
ir,
Some(target.edge),
target.src_path,
target.dst_path,
filters,
) {
continue
}
let references = target.edge.references.copy()
references.push(
EdgeReference::from_ast(
edge,
target.edge.id.index.unwrap_or(0),
true,
edge_context,
due_to_glob=true,
),
)
let (primary, edge_map) = compile_edge_value(
Some(target.edge),
key.value,
)
target.parent.set_edge(
Edge::new(target.edge.id, primary, edge_map, references),
)
}
continue
}
for src_path in src_paths {
for dst_path in dst_paths {
if !edge_glob_filters_pass(ir, None, src_path, dst_path, filters) {
continue
}
if (src_has_glob || dst_has_glob) &&
string_path_equal(src_path, dst_path) {
continue
}
let base_id = EdgeID::new(
src_path.names,
dst_path.names,
edge.src_arrow == "<",
edge.dst_arrow == ">",
src_path_syntax=Some(src_path.syntax),
dst_path_syntax=Some(dst_path.syntax),
)
// The reference preserves multiple edges between the same endpoints. When no explicit
// `[index]` is given, allocate the next index and create a new edge.
// When `[index]` is provided, update that edge if it exists; otherwise
// create it at that index.
match key.edge_index {
Some(ei) =>
if ei.glob {
// Apply the value block to all edges in the group.
let existing = ir.find_edges(base_id)
for e in existing {
if key_suspension(key) is Some(_) &&
!glob_edge_patch_follows_existing(
Some(e),
key.range.start.offset,
) {
continue
}
if !edge_glob_filters_pass(
ir,
Some(e),
src_path,
dst_path,
filters,
) {
continue
}
e.references.push(
EdgeReference::from_ast(
edge,
e.id.index.unwrap_or(0),
true,
edge_context,
due_to_glob=true,
),
)
ensure_field_path(ir, src_path)
ensure_field_path(ir, dst_path)
append_field_references_for_path(
ir, src_path, edge_context, true,
)
append_field_references_for_path(
ir, dst_path, edge_context, true,
)
let (primary, edge_map) = compile_edge_value(Some(e), key.value)
ir.set_edge(Edge::new(e.id, primary, edge_map, e.references))
}
} else {
let idx = ei.index.unwrap_or(0)
let edge_id = EdgeID::with_index(
base_id.src_path,
base_id.dst_path,
base_id.src_arrow,
base_id.dst_arrow,
idx,
src_path_syntax=Some(base_id.src_path_syntax),
dst_path_syntax=Some(base_id.dst_path_syntax),
)
match find_exact_edge(ir, edge_id) {
Some(e) => {
e.references.push(
EdgeReference::from_ast(
edge,
idx,
true,
edge_context,
due_to_glob=src_has_glob || dst_has_glob,
),
)
ensure_field_path(ir, src_path)
ensure_field_path(ir, dst_path)
append_field_references_for_path(
ir,
src_path,
edge_context,
src_has_glob || dst_has_glob,
)
append_field_references_for_path(
ir,
dst_path,
edge_context,
src_has_glob || dst_has_glob,
)
let (primary, edge_map) = compile_edge_value(
Some(e),
key.value,
)
ir.set_edge(Edge::new(e.id, primary, edge_map, e.references))
}
None => {
let (primary, edge_map) = compile_edge_value(None, key.value)
let new_edge = Edge::new(edge_id, primary, edge_map, [
EdgeReference::from_ast(
edge,
idx,
true,
edge_context,
due_to_glob=src_has_glob || dst_has_glob,
),
])
ir.add_edge(new_edge)
ensure_field_path(ir, src_path)
ensure_field_path(ir, dst_path)
append_field_references_for_path(
ir,
src_path,
edge_context,
src_has_glob || dst_has_glob,
)
append_field_references_for_path(
ir,
dst_path,
edge_context,
src_has_glob || dst_has_glob,
)
}
}
}
None => {
let idx = next_index_for_group(ir, base_id)
let edge_id = EdgeID::with_index(
base_id.src_path,
base_id.dst_path,
base_id.src_arrow,
base_id.dst_arrow,
idx,
src_path_syntax=Some(base_id.src_path_syntax),
dst_path_syntax=Some(base_id.dst_path_syntax),
)
let (primary, edge_map) = compile_edge_value(None, key.value)
let new_edge = Edge::new(edge_id, primary, edge_map, [
EdgeReference::from_ast(
edge,
idx,
true,
edge_context,
due_to_glob=src_has_glob || dst_has_glob,
),
])
ir.add_edge(new_edge)
ensure_field_path(ir, src_path)
ensure_field_path(ir, dst_path)
append_field_references_for_path(
ir,
src_path,
edge_context,
src_has_glob || dst_has_glob,
)
append_field_references_for_path(
ir,
dst_path,
edge_context,
src_has_glob || dst_has_glob,
)
}
}
}
}
}
}
///|
fn field_at_resolved_path(ir : Map, path : ResolvedPath) -> (Field, Map)? {
let mut current = ir
for i, syntax in path.syntax {
guard current.get_field_by_syntax(syntax) is Some(field) else {
return None
}
if i == path.syntax.length() - 1 {
return Some((field, current))
}
match field.composite {
Some(Map(next)) => current = next
_ => return None
}
}
None
}
///|
fn edge_glob_filters_pass(
ir : Map,
edge : Edge?,
src_path : ResolvedPath,
dst_path : ResolvedPath,
filters : Array[(Bool, String, String)],
) -> Bool {
for filter in filters {
let (negated, key, value) = filter
let parts = key.split(".").collect()
let mut result = false
if !parts.is_empty() &&
(parts[0].to_owned() == "src" || parts[0].to_owned() == "dst") {
let endpoint_path = if parts[0].to_owned() == "src" {
src_path
} else {
dst_path
}
if parts.length() == 1 {
result = filter_value_matches(endpoint_path.names.join("."), value)
} else {
let property_parts : Array[String] = []
for i = 1; i < parts.length(); i = i + 1 {
property_parts.push(parts[i].to_owned())
}
match field_at_resolved_path(ir, endpoint_path) {
Some((field, parent_map)) =>
result = evaluate_filter_at_level(
FilterContext::new(field, parent_map),
property_parts.join("."),
value,
endpoint_path.names.length() - 1,
)
None => ()
}
}
} else {
match edge {
Some(edge) => result = edge_filter_property_matches(edge, key, value)
None => result = true
}
}
let passes = if negated { !result } else { result }
if !passes {
return false
}
}
true
}
///|
fn edge_filter_property_matches(
edge : Edge,
key : String,
value : String,
) -> Bool {
if key == "label" {
return match edge.primary {
Some(label) => filter_value_matches(label.to_string(), value)
None => filter_value_matches("", value)
}
}
guard edge.map is Some(edge_map) else { return false }
let parts = key.split(".").collect()
let mut current = edge_map
for i = 0; i < parts.length(); i = i + 1 {
guard current.get_field(parts[i].to_owned()) is Some(field) else {
return false
}
if i == parts.length() - 1 {
return match field.primary {
Some(property) => filter_value_matches(property.to_string(), value)
None => false
}
}
match field.composite {
Some(Map(next)) => current = next
_ => return false
}
}
false
}
///|
fn ensure_field_path(ir : Map, path : ResolvedPath) -> Unit {
let mut current = ir
for i, _ in path.names {
let field = current.ensure_field_by_syntax(path.syntax[i])
match field.composite {
Some(Map(m)) => current = m
None => {
let new_map = Map::new()
let field = Field::new(
field.name,
field.primary,
Some(Map(new_map)),
field.references,
name_syntax=field.name_syntax(),
)
current.set_field(field)
current = new_map
}
Some(_) => break // Field has non-map composite, stop
}
}
}
///|
fn append_field_references_for_path(
ir : Map,
path : ResolvedPath,
context : RefContext,
due_to_glob : Bool,
) -> Unit {
if path.syntax.is_empty() {
return
}
let key_path = @ast.KeyPath::new(path.syntax[0].range(), path.syntax.copy())
append_field_references_for_path_at(
ir, path, key_path, context, 0, due_to_glob,
)
}
///|
fn append_field_references_for_path_at(
current : Map,
path : ResolvedPath,
key_path : @ast.KeyPath,
context : RefContext,
index : Int,
due_to_glob : Bool,
) -> Unit {
if index >= path.syntax.length() {
return
}
let field = match current.get_field_by_syntax(path.syntax[index]) {
Some(field) => field
None => return
}
let updated = record_field_reference(
current,
field,
path.syntax[index],
key_path,
context,
index,
false,
due_to_glob~,
)
if index + 1 >= path.syntax.length() {
return
}
match updated.composite {
Some(Map(next)) =>
append_field_references_for_path_at(
next,
path,
key_path,
context,
index + 1,
due_to_glob,
)
_ => ()
}
}
///|
fn compile_value(
field : Field,
value : @ast.Value,
source_path : String?,
) -> Field raise CompileError {
compile_value_detached(field, value, source_path)
}
///|
fn compile_value_in_scope(
field : Field,
value : @ast.Value,
parent_map : Map,
scope_frames : Array[CompileScopeFrame],
source_path : String?,
) -> Field raise CompileError {
match value {
Map(m) => {
let ir_map = match field.composite {
Some(Map(existing)) => existing
_ => Map::from_ast(m, source_path~)
}
let nested_frames = scope_frames.copy()
let child_syntax = match field.name_syntax() {
Some(syntax) => syntax
None => synthetic_unquoted_string(field.name)
}
nested_frames.push(CompileScopeFrame::new(parent_map, child_syntax))
compile_map_nodes_in_scope(ir_map, m, m.nodes, nested_frames, source_path)
Field::new(
field.name,
field.primary,
Some(Map(ir_map)),
field.references,
name_syntax=field.name_syntax(),
)
}
BlockScalar(label, m) => {
let field = match label {
Scalar(s) =>
Field::new(
field.name,
Some(compile_scalar(s, source_path)),
field.composite,
field.references,
name_syntax=field.name_syntax(),
)
_ => field
}
let ir_map = match field.composite {
Some(Map(existing)) => existing
_ => Map::from_ast(m, source_path~)
}
let nested_frames = scope_frames.copy()
let child_syntax = match field.name_syntax() {
Some(syntax) => syntax
None => synthetic_unquoted_string(field.name)
}
nested_frames.push(CompileScopeFrame::new(parent_map, child_syntax))
compile_map_nodes_in_scope(ir_map, m, m.nodes, nested_frames, source_path)
Field::new(
field.name,
field.primary,
Some(Map(ir_map)),
field.references,
name_syntax=field.name_syntax(),
)
}
_ => compile_value_detached(field, value, source_path)
}
}
///|
fn compile_value_detached(
field : Field,
value : @ast.Value,
source_path : String?,
) -> Field raise CompileError {
match value {
Scalar(s) =>
Field::new(
field.name,
Some(compile_scalar(s, source_path)),
match field.composite {
Some(Array(_)) => None
other => other
},
field.references,
name_syntax=field.name_syntax(),
)
Map(m) => {
let ir_map = match field.composite {
Some(Map(existing)) => existing
_ => Map::from_ast(m, source_path~)
}
compile_map_nodes(ir_map, m, m.nodes, source_path~)
Field::new(
field.name,
field.primary,
Some(Map(ir_map)),
field.references,
name_syntax=field.name_syntax(),
)
}
Array(a) => {
let ir_array = compile_array(a, source_path)
Field::new(
field.name,
None,
Some(Array(ir_array)),
field.references,
name_syntax=field.name_syntax(),
)
}
Import(_) =>
raise ImportError("imports require a resolver; use compile_with_imports")
BlockScalar(label, m) => {
// "name: Label { props }" - Label is primary, props is composite
let field = match label {
Scalar(s) =>
Field::new(
field.name,
Some(compile_scalar(s, source_path)),
field.composite,
field.references,
name_syntax=field.name_syntax(),
)
_ => field
}
let ir_map = match field.composite {
Some(Map(existing)) => existing
_ => Map::from_ast(m, source_path~)
}
compile_map_nodes(ir_map, m, m.nodes, source_path~)
Field::new(
field.name,
field.primary,
Some(Map(ir_map)),
field.references,
name_syntax=field.name_syntax(),
)
}
}
}
///|
fn compile_array(
arr : @ast.ArrayValue,
source_path : String?,
) -> IRArray raise CompileError {
let values : Array[Value] = []
for node in arr.nodes {
match node {
Comment(_, _) => ()
BlockComment(_, _) => ()
Substitution(sub) =>
values.push(Scalar(substitution_scalar(sub, source_path)))
Import(_) =>
raise ImportError(
"imports require a resolver; use compile_with_imports",
)
Value(v) => values.push(compile_ast_value(v, source_path))
}
}
IRArray::from_ast(arr, values, source_path~)
}
///|
fn substitution_scalar(
sub : @ast.Substitution,
source_path : String?,
) -> Scalar {
Scalar::from_ast(
Substitution(sub),
String(Unquoted(@ast.UnquotedString::new(sub.range, [Sub(sub)], []))),
source_path~,
)
}
///|
fn compile_ast_value(
value : @ast.Value,
source_path : String?,
) -> Value raise CompileError {
match value {
Scalar(s) => Scalar(compile_scalar(s, source_path))
Map(m) => {
let ir_map = Map::from_ast(m, source_path~)
compile_map_nodes(ir_map, m, m.nodes, source_path~)
Map(ir_map)
}
Array(a) => Array(compile_array(a, source_path))
Import(_) =>
raise ImportError("imports require a resolver; use compile_with_imports")
BlockScalar(_, m) => {
// For arrays, just use the map part (label is ignored in array context)
let ir_map = Map::from_ast(m, source_path~)
compile_map_nodes(ir_map, m, m.nodes, source_path~)
Map(ir_map)
}
}
}
///|
fn compile_scalar(scalar : @ast.Scalar, source_path : String?) -> Scalar {
match scalar {
Null(_r) => Scalar::from_ast(Scalar(scalar), Null, source_path~)
Boolean(_r, b) =>
Scalar::from_ast(
Scalar(scalar),
Bool(if b { "true" } else { "false" }),
source_path~,
)
Number(_r, n) => Scalar::from_ast(Scalar(scalar), Number(n), source_path~)
String(sv) => Scalar::from_ast(StringValue(sv), String(sv), source_path~)
}
}