1use std::collections::BTreeMap;
11use std::fmt;
12use std::path::{Path, PathBuf};
13use std::sync::Arc;
14
15use serde::{Deserialize, Serialize};
16
17use crate::bytecode_cache;
18use crate::chunk::{CachedChunk, Chunk};
19use crate::module_artifact::ModuleArtifact;
20use crate::prepared_module::PreparedModuleArtifact;
21
22pub const LINKED_PROGRAM_SCHEMA_VERSION: u32 = 1;
23pub const LINKED_PROGRAM_ARCHIVE_PATH: &str = "artifacts/program.harnlink";
24pub const LINKER_ALGORITHM_VERSION: u32 = 1;
25const MAGIC: &[u8; 8] = b"HARNLINK";
26
27pub fn link_program(
31 entrypoint: &Path,
32 project_root: &Path,
33) -> Result<LinkedProgramArtifact, LinkedProgramError> {
34 let entrypoint = harn_modules::canonical_path(entrypoint);
35 let project_root = harn_modules::canonical_path(project_root);
36 let build = harn_modules::build_closed_program(std::slice::from_ref(&entrypoint));
37 let reachability = harn_modules::closed_program_reachability(&build, &entrypoint);
38 let entry_source = build.parsed_sources.get(&entrypoint).ok_or_else(|| {
39 LinkedProgramError::invalid(format!(
40 "entrypoint {} was not parsed by the closed graph",
41 entrypoint.display()
42 ))
43 })?;
44 let imported_enums = build
45 .graph
46 .imported_names_by_kind_for_file(&entrypoint, harn_modules::DefKind::Enum)
47 .unwrap_or_default();
48 let entry_chunk = crate::Compiler::new()
49 .with_imported_enum_candidates(imported_enums)
50 .compile(&entry_source.program)
51 .map_err(|error| {
52 LinkedProgramError::invalid(format!(
53 "entrypoint compile failed for {}: {error}",
54 entrypoint.display()
55 ))
56 })?
57 .freeze_for_cache();
58
59 let entrypoint_rel = entrypoint.strip_prefix(&project_root).map_err(|_| {
60 LinkedProgramError::invalid(format!(
61 "entrypoint {} is outside package root {}",
62 entrypoint.display(),
63 project_root.display()
64 ))
65 })?;
66 let entry_bytes = postcard::to_allocvec(&entry_chunk)
67 .map_err(|error| LinkedProgramError::invalid(format!("entry size failed: {error}")))?;
68 let mut report = LinkReport {
69 linker_algorithm_version: LINKER_ALGORITHM_VERSION,
70 harn_version: bytecode_cache::HARN_VERSION.to_string(),
71 codegen_fingerprint: bytecode_cache::CODEGEN_FINGERPRINT.to_string(),
72 input_bytecode_bytes: entry_bytes.len() as u64,
73 output_bytecode_bytes: entry_bytes.len() as u64,
74 user_input_bytes: entry_bytes.len() as u64,
75 user_output_bytes: entry_bytes.len() as u64,
76 modules: vec![LinkModuleReport {
77 path: entrypoint_rel.to_path_buf(),
78 demand: LinkModuleDemand::WholeNamespace,
79 input_bytes: entry_bytes.len() as u64,
80 output_bytes: entry_bytes.len() as u64,
81 initializer_bytes: 0,
82 type_schema_bytes: 0,
83 widening_reason: None,
84 retained_symbols: vec![LinkSymbolReason {
85 symbol: "<entry>".to_string(),
86 reason: "typed entry chunk".to_string(),
87 }],
88 removed_symbols: Vec::new(),
89 }],
90 ..LinkReport::default()
91 };
92
93 let mut modules = BTreeMap::new();
94 let mut digest_inputs = Vec::new();
95 for path in build.graph.module_paths() {
96 let path = harn_modules::canonical_path(&path);
97 let Some(parsed) = build.parsed_sources.get(&path) else {
103 continue;
104 };
105 let archive_path = archive_module_path(&project_root, &path)?;
106 digest_inputs.push((archive_path.clone(), parsed.source.as_bytes().to_vec()));
107 if path == entrypoint {
108 continue;
109 }
110 let imported_enums = build
111 .graph
112 .imported_names_by_kind_for_file(&path, harn_modules::DefKind::Enum)
113 .unwrap_or_default();
114 let compile_path = runtime_compile_path(&path);
115 let full = crate::module_artifact::compile_module_artifact_from_source_with_imported_enums(
116 &compile_path,
117 &parsed.source,
118 imported_enums,
119 )
120 .map_err(|error| {
121 LinkedProgramError::invalid(format!(
122 "module compile failed for {}: {error}",
123 path.display()
124 ))
125 })?;
126 let full_symbols = artifact_symbols(&full);
127 let input_bytes = postcard::to_allocvec(&full)
128 .map_err(|error| LinkedProgramError::invalid(format!("module size failed: {error}")))?
129 .len() as u64;
130 let requested = reachability.demand_for(&path);
131 let widening_reason = full.imports.iter().any(|import| import.is_pub).then(|| {
132 "public re-export shares the module's local import projection; retained whole namespace"
133 .to_string()
134 });
135 let effective = if widening_reason.is_some() {
136 harn_modules::ExportDemand::WholeNamespace
137 } else {
138 requested
139 };
140 let selected = crate::module_artifact::specialize_module_artifact(
141 &parsed.program,
142 Some(compile_path.display().to_string()),
143 full,
144 &effective,
145 )
146 .map_err(|error| {
147 LinkedProgramError::invalid(format!(
148 "module specialization failed for {}: {error}",
149 path.display()
150 ))
151 })?;
152 let selected_symbols = artifact_symbols(&selected);
153 let output_bytes = postcard::to_allocvec(&selected)
154 .map_err(|error| LinkedProgramError::invalid(format!("module size failed: {error}")))?
155 .len() as u64;
156 let mut retained_symbols = selected_symbols
157 .iter()
158 .map(|symbol| LinkSymbolReason {
159 symbol: symbol.clone(),
160 reason: if effective.contains(symbol) {
161 "observable export".to_string()
162 } else {
163 "initializer or private callable dependency".to_string()
164 },
165 })
166 .collect::<Vec<_>>();
167 let initializer_bytes = selected.init_chunk.as_ref().map_or(0, |chunk| {
168 postcard::to_allocvec(chunk).map_or(0, |bytes| bytes.len() as u64)
169 });
170 let type_schema_bytes = selected
171 .type_schema_init_chunks
172 .iter()
173 .map(|chunk| postcard::to_allocvec(chunk).map_or(0, |bytes| bytes.len() as u64))
174 .sum();
175 if initializer_bytes > 0 {
176 retained_symbols.push(LinkSymbolReason {
177 symbol: "<module_initializer>".to_string(),
178 reason: "module effects are preserved conservatively".to_string(),
179 });
180 }
181 let removed_symbols = full_symbols
182 .difference(&selected_symbols)
183 .cloned()
184 .collect::<Vec<_>>();
185 report.input_bytecode_bytes += input_bytes;
186 report.output_bytecode_bytes += output_bytes;
187 if archive_path
188 .to_str()
189 .is_some_and(|path| path.starts_with("<std>/"))
190 {
191 report.stdlib_input_bytes += input_bytes;
192 report.stdlib_output_bytes += output_bytes;
193 } else {
194 report.user_input_bytes += input_bytes;
195 report.user_output_bytes += output_bytes;
196 }
197 report.retained_symbols += retained_symbols.len() as u64;
198 report.removed_symbols += removed_symbols.len() as u64;
199 report.modules.push(LinkModuleReport {
200 path: archive_path.clone(),
201 demand: match effective {
202 harn_modules::ExportDemand::InitializationOnly => {
203 LinkModuleDemand::InitializationOnly
204 }
205 harn_modules::ExportDemand::Members(_) => LinkModuleDemand::Members,
206 harn_modules::ExportDemand::WholeNamespace => LinkModuleDemand::WholeNamespace,
207 },
208 input_bytes,
209 output_bytes,
210 initializer_bytes,
211 type_schema_bytes,
212 widening_reason,
213 retained_symbols,
214 removed_symbols,
215 });
216 modules.insert(archive_path, selected);
217 }
218 digest_inputs.sort_by(|left, right| left.0.cmp(&right.0));
219 let graph_digest_blake3 = graph_digest_from_sources(&digest_inputs);
220 report.graph_digest_blake3.clone_from(&graph_digest_blake3);
221 report
222 .modules
223 .sort_by(|left, right| left.path.cmp(&right.path));
224
225 Ok(LinkedProgramArtifact {
226 schema_version: LINKED_PROGRAM_SCHEMA_VERSION,
227 identity: LinkedProgramIdentity::current(graph_digest_blake3),
228 entrypoint: entrypoint_rel.to_path_buf(),
229 entry_chunk,
230 modules,
231 report,
232 })
233}
234
235fn archive_module_path(project_root: &Path, path: &Path) -> Result<PathBuf, LinkedProgramError> {
236 if path.to_str().is_some_and(|path| path.starts_with("<std>/")) {
237 return Ok(path.to_path_buf());
238 }
239 path.strip_prefix(project_root)
240 .map(Path::to_path_buf)
241 .map_err(|_| {
242 LinkedProgramError::invalid(format!(
243 "module {} is outside package root {}",
244 path.display(),
245 project_root.display()
246 ))
247 })
248}
249
250fn runtime_compile_path(path: &Path) -> PathBuf {
251 path.to_str()
252 .and_then(|path| path.strip_prefix("<std>/"))
253 .map_or_else(
254 || path.to_path_buf(),
255 |module| PathBuf::from(format!("<stdlib>/{module}.harn")),
256 )
257}
258
259fn artifact_symbols(artifact: &ModuleArtifact) -> std::collections::BTreeSet<String> {
260 artifact
261 .functions
262 .keys()
263 .chain(artifact.public_exports.keys())
264 .cloned()
265 .collect()
266}
267
268pub fn graph_digest_from_sources(sources: &[(PathBuf, Vec<u8>)]) -> String {
272 let mut hasher = blake3::Hasher::new();
273 hasher.update(b"harn-linked-program-graph-v1\0");
274 for (path, source) in sources {
275 hasher.update(path.to_string_lossy().as_bytes());
276 hasher.update(&[0]);
277 hasher.update(&(source.len() as u64).to_le_bytes());
278 hasher.update(source);
279 }
280 format!("blake3:{}", hasher.finalize().to_hex())
281}
282
283pub fn verify_graph_binding(
288 report: &LinkReport,
289 expected_digest: &str,
290 mut user_source: impl FnMut(&Path) -> Option<Vec<u8>>,
291) -> Result<(), LinkedProgramError> {
292 let mut sources = Vec::new();
293 for module in &report.modules {
294 let bytes = if let Some(name) = module
295 .path
296 .to_str()
297 .and_then(|path| path.strip_prefix("<std>/"))
298 {
299 crate::stdlib_modules::get_stdlib_source(name)
300 .ok_or_else(|| {
301 LinkedProgramError::invalid(format!(
302 "runtime has no embedded stdlib module std/{name}"
303 ))
304 })?
305 .as_bytes()
306 .to_vec()
307 } else {
308 user_source(&module.path).ok_or_else(|| {
309 LinkedProgramError::invalid(format!(
310 "verified source graph has no {}",
311 module.path.display()
312 ))
313 })?
314 };
315 sources.push((module.path.clone(), bytes));
316 }
317 sources.sort_by(|left, right| left.0.cmp(&right.0));
318 let actual = graph_digest_from_sources(&sources);
319 if actual != expected_digest {
320 return Err(LinkedProgramError::invalid(format!(
321 "linked graph digest mismatch: manifest {expected_digest}, verified sources {actual}"
322 )));
323 }
324 Ok(())
325}
326
327#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
328pub struct LinkedProgramIdentity {
329 pub graph_digest_blake3: String,
330 pub harn_version: String,
331 pub codegen_fingerprint: String,
332 pub bytecode_schema_version: u32,
333 pub linker_algorithm_version: u32,
334 pub optimizations_enabled: bool,
335}
336
337impl LinkedProgramIdentity {
338 pub fn current(graph_digest_blake3: String) -> Self {
339 Self {
340 graph_digest_blake3,
341 harn_version: bytecode_cache::HARN_VERSION.to_string(),
342 codegen_fingerprint: bytecode_cache::CODEGEN_FINGERPRINT.to_string(),
343 bytecode_schema_version: bytecode_cache::SCHEMA_VERSION,
344 linker_algorithm_version: LINKER_ALGORITHM_VERSION,
345 optimizations_enabled: crate::CompilerOptions::from_env().optimizations_enabled(),
346 }
347 }
348
349 fn validate_current(&self) -> Result<(), LinkedProgramError> {
350 let expected = Self::current(self.graph_digest_blake3.clone());
351 if self.harn_version != expected.harn_version {
352 return Err(LinkedProgramError::incompatible(format!(
353 "linked program was built by harn {}; this runtime is {}",
354 self.harn_version, expected.harn_version
355 )));
356 }
357 if self.codegen_fingerprint != expected.codegen_fingerprint
358 || self.bytecode_schema_version != expected.bytecode_schema_version
359 || self.linker_algorithm_version != expected.linker_algorithm_version
360 || self.optimizations_enabled != expected.optimizations_enabled
361 {
362 return Err(LinkedProgramError::incompatible(
363 "linked program compiler, bytecode, or linker identity does not match this runtime",
364 ));
365 }
366 Ok(())
367 }
368}
369
370#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
371pub struct LinkReport {
372 pub graph_digest_blake3: String,
373 pub linker_algorithm_version: u32,
374 pub harn_version: String,
375 pub codegen_fingerprint: String,
376 pub input_bytecode_bytes: u64,
377 pub output_bytecode_bytes: u64,
378 pub user_input_bytes: u64,
379 pub user_output_bytes: u64,
380 pub stdlib_input_bytes: u64,
381 pub stdlib_output_bytes: u64,
382 pub retained_symbols: u64,
383 pub removed_symbols: u64,
384 pub modules: Vec<LinkModuleReport>,
385}
386
387#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
388pub struct LinkModuleReport {
389 pub path: PathBuf,
390 pub demand: LinkModuleDemand,
391 pub input_bytes: u64,
392 pub output_bytes: u64,
393 pub initializer_bytes: u64,
394 pub type_schema_bytes: u64,
395 pub widening_reason: Option<String>,
396 pub retained_symbols: Vec<LinkSymbolReason>,
397 pub removed_symbols: Vec<String>,
398}
399
400#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
401#[serde(rename_all = "snake_case")]
402pub enum LinkModuleDemand {
403 InitializationOnly,
404 Members,
405 WholeNamespace,
406}
407
408#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
409pub struct LinkSymbolReason {
410 pub symbol: String,
411 pub reason: String,
412}
413
414#[derive(Clone, Debug, Serialize, Deserialize)]
415pub struct LinkedProgramArtifact {
416 pub schema_version: u32,
417 pub identity: LinkedProgramIdentity,
418 pub entrypoint: PathBuf,
420 pub entry_chunk: CachedChunk,
421 pub modules: BTreeMap<PathBuf, ModuleArtifact>,
423 pub report: LinkReport,
424}
425
426impl LinkedProgramArtifact {
427 pub fn encode(&self) -> Result<Vec<u8>, LinkedProgramError> {
428 let payload = postcard::to_allocvec(self)
429 .map_err(|error| LinkedProgramError::invalid(format!("encode failed: {error}")))?;
430 let mut bytes = Vec::with_capacity(MAGIC.len() + 4 + payload.len());
431 bytes.extend_from_slice(MAGIC);
432 bytes.extend_from_slice(&LINKED_PROGRAM_SCHEMA_VERSION.to_le_bytes());
433 bytes.extend_from_slice(&payload);
434 Ok(bytes)
435 }
436
437 pub fn decode(bytes: &[u8]) -> Result<Self, LinkedProgramError> {
438 let Some((magic, rest)) = bytes.split_at_checked(MAGIC.len()) else {
439 return Err(LinkedProgramError::invalid(
440 "linked program header is truncated",
441 ));
442 };
443 if magic != MAGIC {
444 return Err(LinkedProgramError::invalid(
445 "linked program magic is invalid",
446 ));
447 }
448 let Some((schema, payload)) = rest.split_at_checked(4) else {
449 return Err(LinkedProgramError::invalid(
450 "linked program schema header is truncated",
451 ));
452 };
453 let actual = u32::from_le_bytes(schema.try_into().expect("four-byte schema"));
454 if actual != LINKED_PROGRAM_SCHEMA_VERSION {
455 return Err(LinkedProgramError::incompatible(format!(
456 "linked program schema {actual} is unsupported; expected {LINKED_PROGRAM_SCHEMA_VERSION}"
457 )));
458 }
459 let (artifact, trailing): (Self, &[u8]) = postcard::take_from_bytes(payload)
460 .map_err(|error| LinkedProgramError::invalid(format!("decode failed: {error}")))?;
461 if !trailing.is_empty() {
462 return Err(LinkedProgramError::invalid(
463 "linked program contains trailing bytes",
464 ));
465 }
466 if artifact.schema_version != LINKED_PROGRAM_SCHEMA_VERSION {
467 return Err(LinkedProgramError::invalid(format!(
468 "linked program payload schema {} disagrees with its header",
469 artifact.schema_version
470 )));
471 }
472 artifact.identity.validate_current()?;
473 Ok(artifact)
474 }
475
476 pub fn into_runtime(self, source_root: &Path) -> LinkedProgramRuntime {
477 let modules = self
478 .modules
479 .into_iter()
480 .map(|(path, mut artifact)| {
481 let runtime_path = runtime_module_path(source_root, &path);
482 artifact.bind_source_file(&runtime_path);
483 (
484 runtime_path,
485 Arc::new(PreparedModuleArtifact::from_cached(artifact)),
486 )
487 })
488 .collect();
489 LinkedProgramRuntime {
490 digest: self.identity.graph_digest_blake3,
491 entry_chunk: Chunk::from_cached(self.entry_chunk),
492 repository: Arc::new(LinkedProgramRepository { modules }),
493 report: self.report,
494 }
495 }
496}
497
498fn runtime_module_path(source_root: &Path, path: &Path) -> PathBuf {
499 if let Some(path) = path.to_str().and_then(|path| path.strip_prefix("<std>/")) {
500 return PathBuf::from(format!("<stdlib>/{path}.harn"));
501 }
502 let path = source_root.join(path);
503 path.canonicalize().unwrap_or(path)
504}
505
506pub struct LinkedProgramRuntime {
507 pub digest: String,
508 pub entry_chunk: Chunk,
509 pub report: LinkReport,
510 pub(crate) repository: Arc<LinkedProgramRepository>,
511}
512
513pub(crate) struct LinkedProgramRepository {
514 modules: BTreeMap<PathBuf, Arc<PreparedModuleArtifact>>,
515}
516
517impl LinkedProgramRepository {
518 pub(crate) fn get(&self, path: &Path) -> Option<Arc<PreparedModuleArtifact>> {
519 self.modules.get(path).cloned()
520 }
521}
522
523#[derive(Clone, Debug, PartialEq, Eq)]
524pub struct LinkedProgramError {
525 pub code: &'static str,
526 pub message: String,
527}
528
529impl LinkedProgramError {
530 fn invalid(message: impl Into<String>) -> Self {
531 Self {
532 code: "linked_program.invalid",
533 message: message.into(),
534 }
535 }
536
537 fn incompatible(message: impl Into<String>) -> Self {
538 Self {
539 code: "linked_program.incompatible",
540 message: message.into(),
541 }
542 }
543}
544
545impl fmt::Display for LinkedProgramError {
546 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
547 formatter.write_str(&self.message)
548 }
549}
550
551impl std::error::Error for LinkedProgramError {}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556 use std::fs;
557
558 #[test]
559 fn identity_rejects_codegen_drift() {
560 let mut identity = LinkedProgramIdentity::current("blake3:test".to_string());
561 identity.codegen_fingerprint.push_str("-different");
562 let error = identity.validate_current().unwrap_err();
563 assert_eq!(error.code, "linked_program.incompatible");
564 }
565
566 #[test]
567 fn closed_link_retains_private_callable_closure_and_initializer_roots() {
568 let dir = tempfile::tempdir().unwrap();
569 let library = dir.path().join("library.harn");
570 let entry = dir.path().join("entry.harn");
571 fs::write(
572 &library,
573 r#"
574 fn helper_a() { helper_b() }
575 fn helper_b() { 7 }
576 fn init_helper() { "initialized" }
577 const init_hook = init_helper
578 pub fn kept() { helper_a() }
579 pub fn dead() { "dead" }
580 pub type KeptShape = { value: int }
581 pub type DeadShape = { value: string }
582 "#,
583 )
584 .unwrap();
585 fs::write(
586 &entry,
587 r#"
588 import * as lib from "./library.harn"
589 fn main() { println(lib.kept()) }
590 "#,
591 )
592 .unwrap();
593
594 let linked = link_program(&entry, dir.path()).expect("link succeeds");
595 let library = &linked.modules[Path::new("library.harn")];
596 assert!(library.functions.contains_key("kept"));
597 assert!(library.functions.contains_key("helper_a"));
598 assert!(library.functions.contains_key("helper_b"));
599 assert!(library.functions.contains_key("init_helper"));
600 assert!(!library.functions.contains_key("dead"));
601 assert_eq!(
602 library.public_exports.keys().cloned().collect::<Vec<_>>(),
603 ["kept"]
604 );
605 let report = linked
606 .report
607 .modules
608 .iter()
609 .find(|module| module.path == Path::new("library.harn"))
610 .unwrap();
611 assert!(report.removed_symbols.iter().any(|name| name == "dead"));
612 assert!(report.initializer_bytes > 0);
613 assert!(report.output_bytes < report.input_bytes);
614 }
615
616 #[test]
617 fn selective_type_import_retains_only_its_schema_initializer() {
618 let dir = tempfile::tempdir().unwrap();
619 let library = dir.path().join("types.harn");
620 let entry = dir.path().join("entry.harn");
621 fs::write(
622 &library,
623 r"
624 pub type KeptShape = { value: int }
625 pub type DeadShape = { value: string }
626 ",
627 )
628 .unwrap();
629 fs::write(
630 &entry,
631 r#"
632 import { KeptShape } from "./types.harn"
633 fn accept(value: KeptShape) { value.value }
634 fn main() { accept({ value: 7 }) }
635 "#,
636 )
637 .unwrap();
638
639 let linked = link_program(&entry, dir.path()).expect("link succeeds");
640 let types = &linked.modules[Path::new("types.harn")];
641 assert_eq!(
642 types.public_type_names.iter().cloned().collect::<Vec<_>>(),
643 ["KeptShape"]
644 );
645 assert_eq!(types.type_schema_init_chunks.len(), 1);
646 let report = linked
647 .report
648 .modules
649 .iter()
650 .find(|module| module.path == Path::new("types.harn"))
651 .unwrap();
652 assert!(report.type_schema_bytes > 0);
653 assert!(report
654 .removed_symbols
655 .iter()
656 .any(|name| name == "DeadShape"));
657 }
658
659 #[test]
660 fn public_reexport_records_conservative_widening() {
661 let dir = tempfile::tempdir().unwrap();
662 let inner = dir.path().join("inner.harn");
663 let facade = dir.path().join("facade.harn");
664 let entry = dir.path().join("entry.harn");
665 fs::write(&inner, "pub fn kept() { 7 }\npub fn dead() { 8 }\n").unwrap();
666 fs::write(
667 &facade,
668 r#"
669 pub import { kept } from "./inner.harn"
670 pub fn local_dead() { 9 }
671 "#,
672 )
673 .unwrap();
674 fs::write(
675 &entry,
676 r#"
677 import { kept } from "./facade.harn"
678 fn main() { println(kept()) }
679 "#,
680 )
681 .unwrap();
682
683 let linked = link_program(&entry, dir.path()).expect("link succeeds");
684 let facade_report = linked
685 .report
686 .modules
687 .iter()
688 .find(|module| module.path == Path::new("facade.harn"))
689 .unwrap();
690 assert_eq!(facade_report.demand, LinkModuleDemand::WholeNamespace);
691 assert!(facade_report
692 .widening_reason
693 .as_deref()
694 .is_some_and(|reason| reason.contains("public re-export")));
695 assert!(linked.modules[Path::new("facade.harn")]
696 .functions
697 .contains_key("local_dead"));
698 }
699}