Skip to main content

harn_modules/
declarations.rs

1use harn_parser::{BindingPattern, Node, SNode};
2
3/// Kind of symbol that can be exported by a module.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
5pub enum DefKind {
6    Function,
7    Pipeline,
8    Tool,
9    Skill,
10    EvalPack,
11    Struct,
12    Enum,
13    Interface,
14    Type,
15    Variable,
16    Parameter,
17}
18
19impl DefKind {
20    /// Whether an exported declaration has a runtime binding that can be
21    /// projected into an importing module. Type and interface declarations
22    /// remain valid imports, but carry only static information.
23    pub const fn has_runtime_value(self) -> bool {
24        !matches!(self, Self::Type | Self::Interface | Self::Parameter)
25    }
26}
27
28/// One public name introduced by a single top-level declaration.
29///
30/// This is the language-level export contract shared by the module graph and
31/// the VM artifact compiler. Consumers must not re-derive declaration kinds
32/// with independent AST matches.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct PublicDeclaration {
35    pub name: String,
36    pub kind: DefKind,
37}
38
39/// Return every public name introduced by one declaration.
40///
41/// Interfaces are public by language design: unlike the other declaration
42/// forms, the grammar does not accept a `pub` modifier for them. Attributes
43/// preserve the visibility of their wrapped declaration.
44pub fn public_declarations(snode: &SNode) -> Vec<PublicDeclaration> {
45    match &snode.node {
46        Node::AttributedDecl { attributes, inner } => {
47            if attributes
48                .iter()
49                .any(|attribute| attribute.name == "sibling")
50            {
51                Vec::new()
52            } else {
53                public_declarations(inner)
54            }
55        }
56        Node::FnDecl {
57            name, is_pub: true, ..
58        } => declaration(name, DefKind::Function),
59        Node::Pipeline {
60            name, is_pub: true, ..
61        } => declaration(name, DefKind::Pipeline),
62        Node::ToolDecl {
63            name, is_pub: true, ..
64        } => declaration(name, DefKind::Tool),
65        Node::SkillDecl {
66            name, is_pub: true, ..
67        } => declaration(name, DefKind::Skill),
68        Node::EvalPackDecl {
69            binding_name,
70            is_pub: true,
71            ..
72        } => declaration(binding_name, DefKind::EvalPack),
73        Node::StructDecl {
74            name, is_pub: true, ..
75        } => declaration(name, DefKind::Struct),
76        Node::EnumDecl {
77            name, is_pub: true, ..
78        } => declaration(name, DefKind::Enum),
79        Node::InterfaceDecl { name, .. } => declaration(name, DefKind::Interface),
80        Node::TypeDecl {
81            name, is_pub: true, ..
82        } => declaration(name, DefKind::Type),
83        Node::LetBinding {
84            pattern,
85            is_pub: true,
86            ..
87        }
88        | Node::ConstBinding {
89            pattern,
90            is_pub: true,
91            ..
92        } => pattern_names(pattern)
93            .into_iter()
94            .map(|name| PublicDeclaration {
95                name,
96                kind: DefKind::Variable,
97            })
98            .collect(),
99        _ => Vec::new(),
100    }
101}
102
103/// Names explicitly shared with modules in the declaring file's directory.
104/// This projection stays separate from the public surface used by package
105/// catalogs and external importers.
106pub fn sibling_declarations(snode: &SNode) -> Vec<PublicDeclaration> {
107    let Node::AttributedDecl { attributes, inner } = &snode.node else {
108        return Vec::new();
109    };
110    if !attributes
111        .iter()
112        .any(|attribute| attribute.name == "sibling")
113    {
114        return Vec::new();
115    }
116    match &inner.node {
117        Node::FnDecl { name, .. } => declaration(name, DefKind::Function),
118        _ => Vec::new(),
119    }
120}
121
122fn declaration(name: &str, kind: DefKind) -> Vec<PublicDeclaration> {
123    vec![PublicDeclaration {
124        name: name.to_string(),
125        kind,
126    }]
127}
128
129pub(crate) fn pattern_names(pattern: &BindingPattern) -> Vec<String> {
130    match pattern {
131        BindingPattern::Identifier(name) => vec![name.clone()],
132        BindingPattern::Dict(fields) => fields
133            .iter()
134            .filter_map(|field| field.alias.as_ref().or(Some(&field.key)).cloned())
135            .collect(),
136        BindingPattern::List(elements) => elements
137            .iter()
138            .map(|element| element.name.clone())
139            .collect(),
140        BindingPattern::Pair(a, b) => vec![a.clone(), b.clone()],
141    }
142}
143
144// ---------------------------------------------------------------------------
145// Graph-building declaration walks.
146//
147// The exports above answer "what is public here?" for callers outside the
148// crate. These answer "what does this file declare, and where?" for the module
149// graph itself. Both read the same AST, so they share an owner rather than
150// letting `lib.rs` grow a second syntax walk beside the graph builder.
151// ---------------------------------------------------------------------------
152
153use std::path::Path;
154
155use harn_lexer::Span;
156
157use crate::{import_recording, DefSite, ModuleInfo, PackageSnapshot};
158
159pub(crate) fn collect_module_info(
160    file: &Path,
161    snode: &SNode,
162    module: &mut ModuleInfo,
163    package_snapshots: &[PackageSnapshot],
164) {
165    if let Node::AttributedDecl { inner, .. } = &snode.node {
166        for sibling in sibling_declarations(snode) {
167            module.sibling_exports.insert(sibling.name);
168        }
169        collect_module_info(file, inner, module, package_snapshots);
170        return;
171    }
172
173    for public in public_declarations(snode) {
174        module.own_exports.insert(public.name);
175    }
176
177    match &snode.node {
178        Node::FnDecl { name, params, .. } => {
179            module.declarations.insert(
180                name.clone(),
181                decl_site(file, snode.span, name, DefKind::Function),
182            );
183            for param_name in params.iter().map(|param| param.name.clone()) {
184                module.declarations.insert(
185                    param_name.clone(),
186                    decl_site(file, snode.span, &param_name, DefKind::Parameter),
187                );
188            }
189        }
190        Node::Pipeline { name, .. } => {
191            module.declarations.insert(
192                name.clone(),
193                decl_site(file, snode.span, name, DefKind::Pipeline),
194            );
195        }
196        Node::ToolDecl { name, .. } => {
197            module.declarations.insert(
198                name.clone(),
199                decl_site(file, snode.span, name, DefKind::Tool),
200            );
201        }
202        Node::SkillDecl { name, .. } => {
203            module.declarations.insert(
204                name.clone(),
205                decl_site(file, snode.span, name, DefKind::Skill),
206            );
207        }
208        Node::EvalPackDecl { binding_name, .. } => {
209            module.declarations.insert(
210                binding_name.clone(),
211                decl_site(file, snode.span, binding_name, DefKind::EvalPack),
212            );
213        }
214        Node::StructDecl { name, .. } => {
215            module.declarations.insert(
216                name.clone(),
217                decl_site(file, snode.span, name, DefKind::Struct),
218            );
219        }
220        Node::EnumDecl { name, .. } => {
221            module.declarations.insert(
222                name.clone(),
223                decl_site(file, snode.span, name, DefKind::Enum),
224            );
225        }
226        Node::InterfaceDecl { name, .. } => {
227            module.declarations.insert(
228                name.clone(),
229                decl_site(file, snode.span, name, DefKind::Interface),
230            );
231        }
232        Node::TypeDecl { name, .. } => {
233            module.declarations.insert(
234                name.clone(),
235                decl_site(file, snode.span, name, DefKind::Type),
236            );
237        }
238        Node::LetBinding { pattern, .. } | Node::ConstBinding { pattern, .. } => {
239            for name in pattern_names(pattern) {
240                module.declarations.insert(
241                    name.clone(),
242                    decl_site(file, snode.span, &name, DefKind::Variable),
243                );
244            }
245        }
246        _ if import_recording::record_import_node(module, file, snode, package_snapshots) => {}
247        _ => {}
248    }
249}
250
251pub(crate) fn collect_type_declarations(snode: &SNode, decls: &mut Vec<SNode>) {
252    match &snode.node {
253        Node::TypeDecl { .. }
254        | Node::StructDecl { .. }
255        | Node::EnumDecl { .. }
256        | Node::InterfaceDecl { .. } => decls.push(snode.clone()),
257        Node::AttributedDecl { inner, .. } => collect_type_declarations(inner, decls),
258        _ => {}
259    }
260}
261
262pub(crate) fn collect_callable_declarations(snode: &SNode, decls: &mut Vec<SNode>) {
263    match &snode.node {
264        Node::FnDecl { .. } | Node::Pipeline { .. } | Node::ToolDecl { .. } => {
265            // The graph is a static-link surface, not an executable-program
266            // owner. Retain the parser's mechanically checked declaration
267            // shape so generic/where/default metadata cannot drift, but drop
268            // the body before it can be copied into every importer's config.
269            let mut signature = snode.clone();
270            match &mut signature.node {
271                Node::FnDecl { body, .. }
272                | Node::Pipeline { body, .. }
273                | Node::ToolDecl { body, .. } => body.clear(),
274                _ => unreachable!("matched callable declaration"),
275            }
276            decls.push(signature);
277        }
278        Node::AttributedDecl { inner, .. } => collect_callable_declarations(inner, decls),
279        _ => {}
280    }
281}
282
283pub(crate) fn type_decl_name(snode: &SNode) -> Option<&str> {
284    match &snode.node {
285        Node::TypeDecl { name, .. }
286        | Node::StructDecl { name, .. }
287        | Node::EnumDecl { name, .. }
288        | Node::InterfaceDecl { name, .. } => Some(name.as_str()),
289        _ => None,
290    }
291}
292
293pub(crate) fn callable_decl_name(snode: &SNode) -> Option<&str> {
294    match &snode.node {
295        Node::FnDecl { name, .. } | Node::Pipeline { name, .. } | Node::ToolDecl { name, .. } => {
296            Some(name.as_str())
297        }
298        Node::AttributedDecl { inner, .. } => callable_decl_name(inner),
299        _ => None,
300    }
301}
302
303pub(crate) fn decl_site(file: &Path, span: Span, name: &str, kind: DefKind) -> DefSite {
304    DefSite {
305        name: name.to_string(),
306        file: file.to_path_buf(),
307        kind,
308        span,
309    }
310}