@devaloop/devalang 0.0.1-alpha.3 → 0.0.1-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,46 @@
1
+ use crate::core::{ shared::{ duration::Duration, value::Value }, store::variable::VariableTable };
2
+
3
+ pub fn load_trigger(
4
+ trigger: &Value,
5
+ duration: &Duration,
6
+ base_duration: f32,
7
+ variable_table: VariableTable
8
+ ) -> (String, f32) {
9
+ let mut trigger_path = String::new();
10
+ let mut duration_as_secs = 0.0;
11
+
12
+ match trigger {
13
+ Value::String(src) => {
14
+ trigger_path = src.to_string();
15
+ }
16
+ _ => {
17
+ eprintln!("❌ Invalid trigger type. Expected a text variable.");
18
+ }
19
+ }
20
+
21
+ match duration {
22
+ Duration::Identifier(duration_identifier) => {
23
+ if duration_identifier == "auto" {
24
+ duration_as_secs = base_duration;
25
+ } else if let Some(Value::Number(num)) = variable_table.get(duration_identifier) {
26
+ duration_as_secs = *num;
27
+ } else {
28
+ eprintln!("❌ Invalid duration identifier: {}", duration_identifier);
29
+ }
30
+ }
31
+
32
+ Duration::Number(num) => {
33
+ duration_as_secs = *num;
34
+ }
35
+
36
+ Duration::Auto => {
37
+ duration_as_secs = base_duration;
38
+ }
39
+
40
+ _ => {
41
+ eprintln!("❌ Invalid duration type. Expected an identifier.");
42
+ }
43
+ }
44
+
45
+ (trigger_path, duration_as_secs)
46
+ }
@@ -0,0 +1,5 @@
1
+ pub mod engine;
2
+ pub mod interpreter;
3
+ pub mod loader;
4
+ pub mod player;
5
+ pub mod render;
@@ -0,0 +1,54 @@
1
+ use rodio::{ Decoder, OutputStream, OutputStreamHandle, Sink, Source };
2
+ use std::{ fs::File, io::BufReader };
3
+
4
+ pub struct AudioPlayer {
5
+ _stream: OutputStream,
6
+ handle: OutputStreamHandle,
7
+ sink: Sink,
8
+ last_path: Option<String>,
9
+ }
10
+
11
+ impl AudioPlayer {
12
+ pub fn new() -> Self {
13
+ let (stream, handle) = OutputStream::try_default().unwrap();
14
+ let sink = Sink::try_new(&handle).unwrap();
15
+
16
+ Self {
17
+ _stream: stream,
18
+ handle,
19
+ sink,
20
+ last_path: None,
21
+ }
22
+ }
23
+
24
+ fn load_source(&self, path: &str) -> impl Source<Item = f32> + Send + 'static {
25
+ let file = File::open(path).unwrap();
26
+ let reader = BufReader::new(file);
27
+
28
+ Decoder::new(reader).unwrap().convert_samples()
29
+ }
30
+
31
+ pub fn play_file_once(&mut self, path: &str) {
32
+ self.sink.stop();
33
+ self.sink = Sink::try_new(&self.handle).unwrap();
34
+
35
+ self.sink.set_volume(1.0);
36
+
37
+ let source = self.load_source(path);
38
+
39
+ self.sink.append(source);
40
+ self.last_path = Some(path.to_string());
41
+ }
42
+
43
+ pub fn replay_last(&mut self) {
44
+ if let Some(path) = self.last_path.clone() {
45
+ self.play_file_once(&path);
46
+ } else {
47
+ eprintln!("⚠️ No previous audio to replay.");
48
+ }
49
+ }
50
+
51
+ pub fn wait_until_end(&self) {
52
+ self.sink.sleep_until_end();
53
+ }
54
+ }
@@ -0,0 +1,57 @@
1
+ use std::collections::HashMap;
2
+
3
+ use crate::{
4
+ audio::{ engine::AudioEngine, interpreter::interprete_statements },
5
+ core::{
6
+ parser::statement::Statement,
7
+ store::global::GlobalStore,
8
+ },
9
+ utils::logger::{ LogLevel, Logger },
10
+ };
11
+
12
+ pub fn render_audio_with_modules(
13
+ modules: HashMap<String, Vec<Statement>>,
14
+ output_dir: &str,
15
+ global_store: &mut GlobalStore
16
+ ) -> HashMap<String, AudioEngine> {
17
+ let mut result = HashMap::new();
18
+
19
+ for (module_name, statements) in modules {
20
+ let mut global_max_end_time = 0.0;
21
+ let mut audio_engine = AudioEngine::new();
22
+
23
+ // Apply the module's variable table if it exists
24
+ if let Some(module) = global_store.get_module(&module_name) {
25
+ audio_engine.set_variables(module.variable_table.clone());
26
+ }
27
+
28
+ // Interpret the statements to fill the audio buffer
29
+ let (mut audio_engine, module_base_bpm, module_max_end_time) = interprete_statements(
30
+ &statements,
31
+ audio_engine,
32
+ module_name.clone(),
33
+ output_dir.to_string()
34
+ );
35
+
36
+ // Calculate the module's maximum duration
37
+ global_max_end_time = module_max_end_time.max(global_max_end_time);
38
+ audio_engine.set_duration(global_max_end_time);
39
+
40
+ // Check if the buffer contains at least one non-zero sample
41
+ if audio_engine.buffer.iter().all(|&s| s == 0) {
42
+ let logger = Logger::new();
43
+
44
+ logger.log_message(
45
+ LogLevel::Warning,
46
+ format!("Module '{}' ignored: silent buffer (no non-zero samples)", module_name).as_str()
47
+ );
48
+
49
+ continue;
50
+ }
51
+
52
+ // Insert only if the module produces sound
53
+ result.insert(module_name, audio_engine);
54
+ }
55
+
56
+ result
57
+ }
package/rust/cli/build.rs CHANGED
@@ -1,7 +1,9 @@
1
1
  use crate::{
2
+ audio::render::render_audio_with_modules,
2
3
  config::Config,
3
4
  core::{
4
5
  builder::Builder,
6
+ debugger::{ lexer::write_lexer_log_file, preprocessor::write_preprocessor_log_file },
5
7
  preprocessor::loader::ModuleLoader,
6
8
  store::global::GlobalStore,
7
9
  utils::path::{ find_entry_file, normalize_path },
@@ -102,20 +104,29 @@ fn begin_build(entry: String, output: String) {
102
104
 
103
105
  // SECTION Load
104
106
  // NOTE: We use modules in the build command, so we need to load them
105
- let modules = module_loader.load_all(&mut global_store);
107
+ let (modules_tokens, modules_statements) = module_loader.load_all(&mut global_store);
108
+
109
+ // SECTION Write logs
110
+ write_lexer_log_file(&normalized_output_dir, "lexer_tokens.log", modules_tokens.clone());
111
+ write_preprocessor_log_file(
112
+ &normalized_output_dir,
113
+ "resolved_statements.log",
114
+ modules_statements.clone()
115
+ );
106
116
 
107
- // SECTION Build
117
+ // SECTION Building AST and Audio
108
118
  let builder = Builder::new();
109
- builder.build_ast(&modules);
110
-
111
- // TODO: Implement debugging
119
+ builder.build_ast(&modules_statements);
120
+ builder.build_audio(&modules_statements, &normalized_output_dir, &mut global_store);
112
121
 
122
+ // SECTION Logging
123
+ let logger = Logger::new();
124
+
113
125
  let success_message = format!(
114
126
  "Build completed successfully in {:.2?}. Output files written to: '{}'",
115
127
  duration.elapsed(),
116
128
  normalized_output_dir
117
129
  );
118
130
 
119
- let logger = Logger::new();
120
131
  logger.log_message(LogLevel::Success, &success_message);
121
132
  }
package/rust/cli/mod.rs CHANGED
@@ -2,6 +2,7 @@ pub mod check;
2
2
  pub mod build;
3
3
  pub mod init;
4
4
  pub mod template;
5
+ pub mod play;
5
6
 
6
7
  use clap::{ Parser, Subcommand };
7
8
  use crate::utils::version::get_version;
@@ -173,4 +174,32 @@ pub enum Commands {
173
174
  ///
174
175
  debug: bool,
175
176
  },
177
+
178
+ Play {
179
+ #[arg(short, long)]
180
+ /// The entry point of the program to play.
181
+ ///
182
+ entry: Option<String>,
183
+
184
+ #[arg(short, long)]
185
+ /// The directory where the output files will be generated.
186
+ ///
187
+ output: Option<String>,
188
+
189
+ #[arg(long, default_value_t = false)]
190
+ /// Whether to watch for changes and re-play.
191
+ ///
192
+ /// ### Default value
193
+ /// - `false`
194
+ ///
195
+ watch: bool,
196
+
197
+ #[arg(long, default_value_t = false)]
198
+ /// Whether to replay the program after it finishes.
199
+ ///
200
+ /// ### Default value
201
+ /// - `false`
202
+ ///
203
+ repeat: bool,
204
+ },
176
205
  }
@@ -0,0 +1,191 @@
1
+ use crate::{
2
+ audio::player::AudioPlayer,
3
+ config::Config,
4
+ core::{
5
+ builder::Builder,
6
+ debugger::{ lexer::write_lexer_log_file, preprocessor::write_preprocessor_log_file },
7
+ preprocessor::loader::ModuleLoader,
8
+ store::global::GlobalStore,
9
+ utils::path::{ find_entry_file, normalize_path },
10
+ },
11
+ utils::{ logger::{ LogLevel, Logger }, spinner::with_spinner, watcher::watch_directory },
12
+ };
13
+
14
+ use std::{ path::Path, sync::mpsc::channel, thread, time::Duration };
15
+ use std::fs;
16
+ use std::collections::HashMap;
17
+
18
+ pub fn handle_play_command(
19
+ config: Option<Config>,
20
+ entry: Option<String>,
21
+ output: Option<String>,
22
+ watch: bool,
23
+ repeat: bool
24
+ ) {
25
+ let logger = Logger::new();
26
+
27
+ let entry_path = entry
28
+ .or_else(|| config.as_ref().and_then(|c| c.defaults.entry.clone()))
29
+ .unwrap_or_else(|| "".to_string());
30
+
31
+ let output_path = output
32
+ .or_else(|| config.as_ref().and_then(|c| c.defaults.output.clone()))
33
+ .unwrap_or_else(|| "".to_string());
34
+
35
+ let fetched_repeat = if repeat {
36
+ true
37
+ } else {
38
+ config
39
+ .as_ref()
40
+ .and_then(|c| c.defaults.repeat)
41
+ .unwrap_or(false)
42
+ };
43
+
44
+ if entry_path.is_empty() || output_path.is_empty() {
45
+ logger.log_message(LogLevel::Error, "Entry or output path not specified.");
46
+ std::process::exit(1);
47
+ }
48
+
49
+ let entry_file = find_entry_file(&entry_path).unwrap_or_else(|| {
50
+ logger.log_message(LogLevel::Error, "index.deva not found");
51
+ std::process::exit(1);
52
+ });
53
+
54
+ let audio_file = format!("{}/audio/index.wav", normalize_path(&output_path));
55
+ let mut audio_player = AudioPlayer::new();
56
+
57
+ if watch && fetched_repeat {
58
+ logger.log_message(
59
+ LogLevel::Error,
60
+ "Watch and repeat cannot be used together. Use repeat instead."
61
+ );
62
+ std::process::exit(1);
63
+ }
64
+
65
+ if watch {
66
+ let (tx, rx) = channel::<()>();
67
+
68
+ // Thread 1 : Watcher sending changes
69
+ let entry_clone = entry_path.clone();
70
+ thread::spawn(move || {
71
+ let _ = watch_directory(entry_clone, move || {
72
+ let _ = tx.send(()); // signal a change
73
+ });
74
+ });
75
+
76
+ // Main thread: build + play in a loop
77
+ begin_play(&config, &entry_file, &output_path);
78
+ audio_player.play_file_once(&audio_file);
79
+
80
+ logger.log_message(LogLevel::Watcher, "Watching for changes... Press Ctrl+C to exit.");
81
+
82
+ while let Ok(_) = rx.recv() {
83
+ logger.log_message(LogLevel::Watcher, "Change detected, rebuilding...");
84
+
85
+ begin_play(&config, &entry_file, &output_path);
86
+
87
+ logger.log_message(LogLevel::Info, "🎵 Playback started (once mode)...");
88
+
89
+ audio_player.play_file_once(&audio_file);
90
+ }
91
+ } else if fetched_repeat {
92
+ // Initial build to start from a clean slate
93
+ begin_play(&config, &entry_file, &output_path);
94
+
95
+ logger.log_message(LogLevel::Info, "🎵 Playback started (repeat mode)...");
96
+
97
+ let mut last_snapshot = snapshot_files(&entry_path);
98
+ let mut audio_player = AudioPlayer::new();
99
+ audio_player.play_file_once(&audio_file);
100
+
101
+ loop {
102
+ let current_snapshot = snapshot_files(&entry_path);
103
+ let has_changed = files_changed(&last_snapshot, &current_snapshot);
104
+
105
+ if has_changed {
106
+ logger.log_message(LogLevel::Info, "Change detected, rebuilding in background...");
107
+ let entry_file = entry_file.clone();
108
+ let output_path = output_path.clone();
109
+ let config_clone = config.clone();
110
+
111
+ // Rebuild in a separate thread
112
+ std::thread::spawn(move || {
113
+ begin_play(&config_clone, &entry_file, &output_path);
114
+ });
115
+
116
+ last_snapshot = current_snapshot;
117
+ }
118
+
119
+ // Wait for the audio to finish without blocking the current playback
120
+ audio_player.wait_until_end();
121
+
122
+ // Then replay the audio (rebuilt or not)
123
+ audio_player.play_file_once(&audio_file);
124
+ }
125
+ } else {
126
+ // Single execution
127
+ begin_play(&config, &entry_file, &output_path);
128
+
129
+ logger.log_message(LogLevel::Info, "🎵 Playback started (once mode)...");
130
+
131
+ audio_player.play_file_once(&audio_file);
132
+ audio_player.wait_until_end();
133
+ }
134
+ }
135
+
136
+ fn begin_play(config: &Option<Config>, entry_file: &str, output: &str) {
137
+ let spinner = with_spinner("Building...", || {
138
+ thread::sleep(Duration::from_millis(800));
139
+ });
140
+
141
+ let normalized_entry = normalize_path(entry_file);
142
+ let normalized_output_dir = normalize_path(&output);
143
+
144
+ let duration = std::time::Instant::now();
145
+ let mut global_store = GlobalStore::new();
146
+ let loader = ModuleLoader::new(&normalized_entry, &normalized_output_dir);
147
+ let (modules_tokens, modules_statements) = loader.load_all(&mut global_store);
148
+
149
+ // SECTION Write logs
150
+ write_lexer_log_file(&normalized_output_dir, "lexer_tokens.log", modules_tokens.clone());
151
+ write_preprocessor_log_file(
152
+ &normalized_output_dir,
153
+ "resolved_statements.log",
154
+ modules_statements.clone()
155
+ );
156
+
157
+ // SECTION Building AST and Audio
158
+ let builder = Builder::new();
159
+ builder.build_ast(&modules_statements);
160
+ builder.build_audio(&modules_statements, &output, &mut global_store);
161
+
162
+ // SECTION Logging
163
+ let logger = Logger::new();
164
+ let success_message = format!(
165
+ "Build completed successfully in {:.2?}. Output files written to: '{}'",
166
+ duration.elapsed(),
167
+ normalized_output_dir
168
+ );
169
+
170
+ logger.log_message(LogLevel::Success, &success_message);
171
+ }
172
+
173
+ fn snapshot_files<P: AsRef<Path>>(dir: P) -> HashMap<String, u64> {
174
+ let mut map = HashMap::new();
175
+ if let Ok(entries) = fs::read_dir(dir) {
176
+ for entry in entries.flatten() {
177
+ if let Ok(meta) = entry.metadata() {
178
+ if let Ok(mtime) = meta.modified() {
179
+ if let Ok(duration) = mtime.duration_since(std::time::UNIX_EPOCH) {
180
+ map.insert(entry.path().display().to_string(), duration.as_secs());
181
+ }
182
+ }
183
+ }
184
+ }
185
+ }
186
+ map
187
+ }
188
+
189
+ fn files_changed(old: &HashMap<String, u64>, new: &HashMap<String, u64>) -> bool {
190
+ old != new
191
+ }
@@ -2,14 +2,15 @@ pub mod loader;
2
2
 
3
3
  use serde::Deserialize;
4
4
 
5
- #[derive(Debug, Deserialize)]
5
+ #[derive(Debug, Deserialize, Clone)]
6
6
  pub struct Config {
7
7
  pub defaults: ConfigDefaults,
8
8
  }
9
9
 
10
- #[derive(Debug, Deserialize)]
10
+ #[derive(Debug, Deserialize, Clone)]
11
11
  pub struct ConfigDefaults {
12
12
  pub entry: Option<String>,
13
13
  pub output: Option<String>,
14
14
  pub watch: Option<bool>,
15
+ pub repeat: Option<bool>,
15
16
  }
@@ -1,4 +1,7 @@
1
+ use crate::audio::render::render_audio_with_modules;
1
2
  use crate::core::parser::statement::Statement;
3
+ use crate::core::store::global::GlobalStore;
4
+ use crate::utils::logger::Logger;
2
5
  use std::{ collections::HashMap, fs::create_dir_all };
3
6
  use std::io::Write;
4
7
 
@@ -28,4 +31,49 @@ impl Builder {
28
31
  file.write_all(content.as_bytes()).expect("Failed to write AST to file");
29
32
  }
30
33
  }
34
+
35
+ pub fn build_audio(
36
+ &self,
37
+ modules: &HashMap<String, Vec<Statement>>,
38
+ normalized_output_dir: &str,
39
+ global_store: &mut GlobalStore
40
+ ) {
41
+ let logger = Logger::new();
42
+
43
+ let audio_engines = render_audio_with_modules(
44
+ modules.clone(),
45
+ &normalized_output_dir,
46
+ global_store
47
+ );
48
+
49
+ create_dir_all(format!("{}/audio", normalized_output_dir)).expect("Failed to create audio directory");
50
+
51
+ for (module_name, mut audio_engine) in audio_engines {
52
+ let formatted_module_name = module_name
53
+ .split('/')
54
+ .last()
55
+ .unwrap_or(&module_name)
56
+ .replace(".deva", "");
57
+
58
+ let output_path = format!(
59
+ "{}/audio/{}.wav",
60
+ normalized_output_dir,
61
+ formatted_module_name
62
+ );
63
+
64
+ match audio_engine.generate_wav_file(&output_path) {
65
+ Ok(_) => {}
66
+ Err(msg) => {
67
+ logger.log_error_with_stacktrace(
68
+ &format!(
69
+ "Unable to generate WAV file for module '{}': {}",
70
+ formatted_module_name,
71
+ msg
72
+ ),
73
+ &module_name
74
+ );
75
+ }
76
+ }
77
+ }
78
+ }
31
79
  }
@@ -1,12 +1,27 @@
1
- use crate::core::{ debugger::Debugger, lexer::token::Token };
1
+ use std::{ collections::HashMap, fs::create_dir_all };
2
+ use crate::core::{ debugger::Debugger, lexer::token::Token, parser::statement::Statement };
2
3
 
3
- pub fn write_lexer_log_file(output_dir: &str, file_name: &str, tokens: Vec<Token>) {
4
+ pub fn write_lexer_log_file(
5
+ output_dir: &str,
6
+ file_name: &str,
7
+ modules: HashMap<String, Vec<Token>>
8
+ ) {
4
9
  let debugger = Debugger::new();
5
10
  let mut content = String::new();
6
11
 
7
- for token in tokens {
8
- content.push_str(&format!("{:?}\n", token));
12
+ let log_directory = format!("{}/logs", output_dir);
13
+
14
+ create_dir_all(&log_directory).expect("Failed to create log directory");
15
+
16
+ for (path, tokens) in modules {
17
+ content.push_str(&format!("--- Resolved Tokens for {} ---\n", path));
18
+
19
+ for token in tokens {
20
+ content.push_str(&format!("{:?}\n", token));
21
+ }
22
+
23
+ content.push_str("\n");
9
24
  }
10
25
 
11
- debugger.write_log_file(output_dir, file_name, &content);
26
+ debugger.write_log_file(&log_directory, file_name, &content);
12
27
  }
@@ -1,17 +1,21 @@
1
- use std::collections::HashMap;
1
+ use std::{ collections::HashMap, fs::create_dir_all };
2
2
  use crate::core::{ debugger::Debugger, parser::statement::Statement };
3
3
 
4
4
  pub fn write_preprocessor_log_file(
5
5
  output_dir: &str,
6
6
  file_name: &str,
7
- statements: HashMap<String, Vec<Statement>>
7
+ modules: HashMap<String, Vec<Statement>>
8
8
  ) {
9
9
  let debugger = Debugger::new();
10
10
  let mut content = String::new();
11
11
 
12
- for (path, stmts) in statements {
12
+ let log_directory = format!("{}/logs", output_dir);
13
+
14
+ create_dir_all(&log_directory).expect("Failed to create log directory");
15
+
16
+ for (path, stmts) in modules {
13
17
  content.push_str(&format!("--- Resolved Statements for {} ---\n", path));
14
-
18
+
15
19
  for stmt in stmts {
16
20
  content.push_str(&format!("{:?}\n", stmt));
17
21
  }
@@ -19,5 +23,5 @@ pub fn write_preprocessor_log_file(
19
23
  content.push_str("\n");
20
24
  }
21
25
 
22
- debugger.write_log_file(output_dir, file_name, &content);
26
+ debugger.write_log_file(&log_directory, file_name, &content);
23
27
  }
@@ -2,9 +2,8 @@ use std::collections::HashMap;
2
2
 
3
3
  use crate::{
4
4
  core::{
5
- debugger::{ lexer::write_lexer_log_file, preprocessor::write_preprocessor_log_file },
6
5
  error::ErrorHandler,
7
- lexer::Lexer,
6
+ lexer::{ token::Token, Lexer },
8
7
  parser::{ statement::{ Statement, StatementKind }, Parser },
9
8
  preprocessor::{
10
9
  module::Module,
@@ -13,7 +12,7 @@ use crate::{
13
12
  },
14
13
  store::global::GlobalStore,
15
14
  },
16
- utils::logger::{ LogLevel, Logger },
15
+ utils::logger::{ Logger },
17
16
  };
18
17
 
19
18
  pub struct ModuleLoader {
@@ -29,26 +28,29 @@ impl ModuleLoader {
29
28
  }
30
29
  }
31
30
 
32
- pub fn load_all(&self, global_store: &mut GlobalStore) -> HashMap<String, Vec<Statement>> {
31
+ pub fn load_all(
32
+ &self,
33
+ global_store: &mut GlobalStore
34
+ ) -> (HashMap<String, Vec<Token>>, HashMap<String, Vec<Statement>>) {
33
35
  // SECTION Load the entry module and its dependencies
34
- self.load_module_recursively(&self.entry, global_store);
36
+ let tokens_by_module = self.load_module_recursively(&self.entry, global_store);
35
37
 
36
38
  // SECTION Process and resolve modules
37
39
  process_modules(self, global_store);
38
40
  resolve_all_modules(self, global_store);
39
41
 
40
- let resolved = resolve_and_flatten_all_modules(global_store);
42
+ let statemnts_by_module = resolve_and_flatten_all_modules(global_store);
41
43
 
42
- // SECTION Write resolved statements to log file
43
- write_preprocessor_log_file(&self.output, "resolved_statements.log", resolved.clone());
44
-
45
- // Return the resolved statements
46
- resolved
44
+ (tokens_by_module, statemnts_by_module)
47
45
  }
48
46
 
49
- fn load_module_recursively(&self, path: &str, global_store: &mut GlobalStore) {
47
+ fn load_module_recursively(
48
+ &self,
49
+ path: &str,
50
+ global_store: &mut GlobalStore
51
+ ) -> HashMap<String, Vec<Token>> {
50
52
  if global_store.modules.contains_key(path) {
51
- return;
53
+ return HashMap::new();
52
54
  }
53
55
 
54
56
  let lexer = Lexer::new();
@@ -75,13 +77,22 @@ impl ModuleLoader {
75
77
 
76
78
  // SECTION Module creation
77
79
  let mut module = Module::new(path);
78
- module.tokens = tokens;
79
- module.statements = statements;
80
+ module.tokens = tokens.clone();
81
+ module.statements = statements.clone();
80
82
 
81
83
  global_store.insert_module(path.to_string(), module);
82
84
 
83
85
  // Then load the imports recursively
84
86
  self.load_module_imports(&path.to_string(), global_store);
87
+
88
+ // Return all tokens by module
89
+ let mut tokens_by_module = HashMap::new();
90
+
91
+ global_store.modules.iter().for_each(|(path, module)| {
92
+ tokens_by_module.insert(path.clone(), module.tokens.clone());
93
+ });
94
+
95
+ tokens_by_module
85
96
  }
86
97
 
87
98
  fn load_module_imports(&self, path: &String, global_store: &mut GlobalStore) {