@devaloop/devalang 0.0.1-alpha.1 → 0.0.1-alpha.10
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.
- package/.devalang +4 -0
- package/Cargo.toml +49 -45
- package/README.md +127 -46
- package/docs/CHANGELOG.md +172 -0
- package/docs/COMMANDS.md +60 -6
- package/docs/CONFIG.md +30 -0
- package/docs/ROADMAP.md +10 -7
- package/docs/SYNTAX.md +100 -18
- package/docs/TODO.md +31 -28
- package/examples/condition.deva +20 -0
- package/examples/group.deva +12 -0
- package/examples/index.deva +13 -4
- package/examples/loop.deva +16 -0
- package/examples/samples/hat-808.wav +0 -0
- package/examples/synth.deva +14 -0
- package/examples/variables.deva +9 -0
- package/out-tsc/bin/devalang.exe +0 -0
- package/out-tsc/scripts/version/fetch.js +1 -5
- package/package.json +5 -4
- package/project-version.json +3 -3
- package/rust/cli/build.rs +114 -28
- package/rust/cli/check.rs +96 -103
- package/rust/cli/init.rs +79 -0
- package/rust/cli/mod.rs +203 -1
- package/rust/cli/play.rs +193 -0
- package/rust/cli/template.rs +57 -0
- package/rust/config/loader.rs +13 -0
- package/rust/config/mod.rs +16 -0
- package/rust/core/audio/engine.rs +214 -0
- package/rust/core/audio/evaluator.rs +31 -0
- package/rust/core/audio/interpreter/arrow_call.rs +129 -0
- package/rust/core/audio/interpreter/call.rs +70 -0
- package/rust/core/audio/interpreter/condition.rs +69 -0
- package/rust/core/audio/interpreter/driver.rs +236 -0
- package/rust/core/audio/interpreter/let_.rs +19 -0
- package/rust/core/audio/interpreter/load.rs +18 -0
- package/rust/core/audio/interpreter/loop_.rs +67 -0
- package/rust/core/audio/interpreter/mod.rs +12 -0
- package/rust/core/audio/interpreter/sleep.rs +36 -0
- package/rust/core/audio/interpreter/spawn.rs +84 -0
- package/rust/core/audio/interpreter/tempo.rs +16 -0
- package/rust/core/audio/interpreter/trigger.rs +69 -0
- package/rust/core/audio/loader/mod.rs +1 -0
- package/rust/core/audio/loader/trigger.rs +52 -0
- package/rust/core/audio/mod.rs +6 -0
- package/rust/core/audio/player.rs +54 -0
- package/rust/core/audio/renderer.rs +54 -0
- package/rust/core/builder/mod.rs +70 -27
- package/rust/core/debugger/lexer.rs +27 -0
- package/rust/core/debugger/mod.rs +13 -49
- package/rust/core/debugger/preprocessor.rs +27 -0
- package/rust/core/debugger/store.rs +25 -0
- package/rust/core/error/mod.rs +60 -0
- package/rust/core/lexer/handler/arrow.rs +31 -0
- package/rust/core/lexer/handler/at.rs +21 -0
- package/rust/core/lexer/handler/brace.rs +41 -0
- package/rust/core/lexer/handler/colon.rs +21 -0
- package/rust/core/lexer/handler/comment.rs +30 -0
- package/rust/core/lexer/handler/dot.rs +21 -0
- package/rust/core/lexer/handler/driver.rs +230 -0
- package/rust/core/lexer/handler/identifier.rs +41 -0
- package/rust/core/lexer/handler/indent.rs +52 -0
- package/rust/core/lexer/handler/mod.rs +14 -0
- package/rust/core/lexer/handler/newline.rs +23 -0
- package/rust/core/lexer/handler/number.rs +31 -0
- package/rust/core/lexer/handler/operator.rs +44 -0
- package/rust/core/lexer/handler/string.rs +63 -0
- package/rust/core/lexer/mod.rs +37 -319
- package/rust/core/lexer/token.rs +86 -0
- package/rust/core/mod.rs +6 -2
- package/rust/core/parser/driver.rs +331 -0
- package/rust/core/parser/handler/arrow_call.rs +126 -0
- package/rust/core/parser/handler/at.rs +162 -0
- package/rust/core/parser/handler/bank.rs +41 -0
- package/rust/core/parser/handler/condition.rs +74 -0
- package/rust/core/parser/handler/dot.rs +112 -0
- package/rust/core/parser/handler/identifier/call.rs +41 -0
- package/rust/core/parser/handler/identifier/group.rs +75 -0
- package/rust/core/parser/handler/identifier/let_.rs +133 -0
- package/rust/core/parser/handler/identifier/mod.rs +51 -0
- package/rust/core/parser/handler/identifier/sleep.rs +33 -0
- package/rust/core/parser/handler/identifier/spawn.rs +41 -0
- package/rust/core/parser/handler/identifier/synth.rs +65 -0
- package/rust/core/parser/handler/loop_.rs +72 -0
- package/rust/core/parser/handler/mod.rs +8 -0
- package/rust/core/parser/handler/tempo.rs +47 -0
- package/rust/core/parser/mod.rs +3 -200
- package/rust/core/parser/statement.rs +96 -0
- package/rust/core/preprocessor/loader.rs +229 -0
- package/rust/core/preprocessor/mod.rs +2 -24
- package/rust/core/preprocessor/module.rs +42 -56
- package/rust/core/preprocessor/processor.rs +76 -0
- package/rust/core/preprocessor/resolver/bank.rs +41 -51
- package/rust/core/preprocessor/resolver/call.rs +123 -0
- package/rust/core/preprocessor/resolver/condition.rs +92 -0
- package/rust/core/preprocessor/resolver/driver.rs +227 -0
- package/rust/core/preprocessor/resolver/group.rs +61 -0
- package/rust/core/preprocessor/resolver/let_.rs +31 -0
- package/rust/core/preprocessor/resolver/loop_.rs +76 -67
- package/rust/core/preprocessor/resolver/mod.rs +12 -111
- package/rust/core/preprocessor/resolver/spawn.rs +58 -0
- package/rust/core/preprocessor/resolver/synth.rs +50 -0
- package/rust/core/preprocessor/resolver/tempo.rs +40 -61
- package/rust/core/preprocessor/resolver/trigger.rs +90 -154
- package/rust/core/preprocessor/resolver/value.rs +78 -0
- package/rust/core/shared/duration.rs +8 -0
- package/rust/core/shared/mod.rs +2 -0
- package/rust/core/shared/value.rs +28 -0
- package/rust/core/store/export.rs +28 -0
- package/rust/core/store/global.rs +39 -0
- package/rust/core/store/import.rs +28 -0
- package/rust/core/store/mod.rs +4 -0
- package/rust/core/store/variable.rs +28 -0
- package/rust/core/utils/mod.rs +2 -0
- package/rust/core/utils/path.rs +31 -0
- package/rust/core/utils/validation.rs +37 -0
- package/rust/lib.rs +161 -1
- package/rust/main.rs +46 -30
- package/rust/utils/file.rs +35 -0
- package/rust/utils/logger.rs +108 -34
- package/rust/utils/mod.rs +3 -2
- package/rust/utils/{loader.rs → spinner.rs} +2 -0
- package/rust/utils/watcher.rs +33 -0
- package/templates/minimal/.devalang +5 -0
- package/templates/minimal/README.md +202 -0
- package/templates/minimal/src/index.deva +2 -0
- package/templates/welcome/.devalang +5 -0
- package/templates/welcome/README.md +202 -0
- package/templates/welcome/samples/kick-808.wav +0 -0
- package/templates/welcome/src/index.deva +13 -0
- package/templates/welcome/src/variables.deva +5 -0
- package/typescript/scripts/version/fetch.ts +1 -6
- package/examples/exported.deva +0 -7
- package/rust/audio/mod.rs +0 -1
- package/rust/cli/new.rs +0 -1
- package/rust/core/parser/at.rs +0 -142
- package/rust/core/parser/bank.rs +0 -42
- package/rust/core/parser/dot.rs +0 -107
- package/rust/core/parser/identifer.rs +0 -91
- package/rust/core/parser/loop_.rs +0 -62
- package/rust/core/parser/tempo.rs +0 -42
- package/rust/core/parser/variable.rs +0 -129
- package/rust/core/preprocessor/dependencies.rs +0 -54
- package/rust/core/preprocessor/resolver/at.rs +0 -24
- package/rust/core/types/cli.rs +0 -160
- package/rust/core/types/mod.rs +0 -7
- package/rust/core/types/module.rs +0 -41
- package/rust/core/types/parser.rs +0 -73
- package/rust/core/types/statement.rs +0 -105
- package/rust/core/types/store.rs +0 -116
- package/rust/core/types/token.rs +0 -83
- package/rust/core/types/variable.rs +0 -32
- package/rust/runner/executer.rs +0 -44
- package/rust/runner/mod.rs +0 -1
- package/rust/utils/path.rs +0 -46
package/rust/cli/check.rs
CHANGED
|
@@ -1,124 +1,117 @@
|
|
|
1
|
-
use std::{ thread, time::Duration };
|
|
2
|
-
|
|
3
1
|
use crate::{
|
|
2
|
+
config::Config,
|
|
4
3
|
core::{
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
statement::{
|
|
9
|
-
Statement,
|
|
10
|
-
StatementIterator,
|
|
11
|
-
StatementKind,
|
|
12
|
-
StatementResolved,
|
|
13
|
-
StatementResolvedValue,
|
|
14
|
-
},
|
|
15
|
-
variable::VariableValue,
|
|
16
|
-
},
|
|
4
|
+
preprocessor::loader::ModuleLoader,
|
|
5
|
+
store::global::GlobalStore,
|
|
6
|
+
utils::path::{ find_entry_file, normalize_path },
|
|
17
7
|
},
|
|
18
|
-
|
|
19
|
-
utils::{ loader::with_spinner, logger::log_message, path::{ find_entry_file, normalize_path } },
|
|
8
|
+
utils::{ logger::{ LogLevel, Logger }, spinner::with_spinner, watcher::watch_directory },
|
|
20
9
|
};
|
|
10
|
+
use std::{ thread, time::Duration };
|
|
21
11
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
12
|
+
#[cfg(feature = "cli")]
|
|
13
|
+
pub fn handle_check_command(
|
|
14
|
+
config: Option<Config>,
|
|
15
|
+
entry: Option<String>,
|
|
16
|
+
output: Option<String>,
|
|
17
|
+
watch: bool
|
|
18
|
+
) {
|
|
19
|
+
let fetched_entry = if entry.is_none() {
|
|
20
|
+
config
|
|
21
|
+
.as_ref()
|
|
22
|
+
.and_then(|c| c.defaults.entry.clone())
|
|
23
|
+
.unwrap_or_else(|| "".to_string())
|
|
24
|
+
} else {
|
|
25
|
+
entry.clone().unwrap_or_else(|| "".to_string())
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
let fetched_output = if output.is_none() {
|
|
29
|
+
config
|
|
30
|
+
.as_ref()
|
|
31
|
+
.and_then(|c| c.defaults.output.clone())
|
|
32
|
+
.unwrap_or_else(|| "".to_string())
|
|
33
|
+
} else {
|
|
34
|
+
output.clone().unwrap_or_else(|| "".to_string())
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
let fetched_watch = if watch {
|
|
38
|
+
watch
|
|
39
|
+
} else {
|
|
40
|
+
config
|
|
41
|
+
.as_ref()
|
|
42
|
+
.and_then(|c| c.defaults.watch)
|
|
43
|
+
.unwrap_or(false)
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
let logger = Logger::new();
|
|
47
|
+
|
|
48
|
+
if fetched_entry.is_empty() {
|
|
49
|
+
logger.log_message(
|
|
50
|
+
LogLevel::Error,
|
|
51
|
+
"Entry path is not specified. Please provide a valid entry path."
|
|
52
|
+
);
|
|
25
53
|
std::process::exit(1);
|
|
26
|
-
}
|
|
54
|
+
}
|
|
55
|
+
if fetched_output.is_empty() {
|
|
56
|
+
logger.log_message(
|
|
57
|
+
LogLevel::Error,
|
|
58
|
+
"Output directory is not specified. Please provide a valid output directory."
|
|
59
|
+
);
|
|
60
|
+
std::process::exit(1);
|
|
61
|
+
}
|
|
27
62
|
|
|
28
|
-
let
|
|
29
|
-
|
|
63
|
+
let entry_file = find_entry_file(&fetched_entry).unwrap_or_else(|| {
|
|
64
|
+
logger.log_message(
|
|
65
|
+
LogLevel::Error,
|
|
66
|
+
&format!("❌ index.deva not found in directory: {}", fetched_entry)
|
|
67
|
+
);
|
|
68
|
+
std::process::exit(1);
|
|
30
69
|
});
|
|
31
70
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
let normalized_output_dir = normalize_path(&output);
|
|
36
|
-
|
|
37
|
-
let global_store = load_all_modules(&normalized_entry_file);
|
|
71
|
+
// SECTION Begin check
|
|
72
|
+
if fetched_watch {
|
|
73
|
+
begin_check(entry_file.clone(), fetched_output.clone());
|
|
38
74
|
|
|
39
|
-
|
|
40
|
-
|
|
75
|
+
logger.log_message(
|
|
76
|
+
LogLevel::Watcher,
|
|
77
|
+
&format!("Watching for changes in '{}'...", fetched_entry)
|
|
78
|
+
);
|
|
41
79
|
|
|
42
|
-
|
|
80
|
+
watch_directory(entry_file.clone(), move || {
|
|
81
|
+
logger.log_message(LogLevel::Watcher, "Detected changes, re-checking...");
|
|
43
82
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
83
|
+
begin_check(entry_file.clone(), fetched_output.clone());
|
|
84
|
+
}).unwrap();
|
|
85
|
+
} else {
|
|
86
|
+
begin_check(entry_file.clone(), fetched_output.clone());
|
|
87
|
+
}
|
|
88
|
+
}
|
|
47
89
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
90
|
+
fn begin_check(entry: String, output: String) {
|
|
91
|
+
let spinner = with_spinner("Checking...", || {
|
|
92
|
+
thread::sleep(Duration::from_millis(800));
|
|
93
|
+
});
|
|
51
94
|
|
|
52
|
-
|
|
53
|
-
let warning_message = format!(
|
|
54
|
-
"Check completed with errors in {:.2?}. Output files written to: '{}'",
|
|
55
|
-
duration.elapsed(),
|
|
56
|
-
normalized_output_dir
|
|
57
|
-
);
|
|
95
|
+
let duration = std::time::Instant::now();
|
|
58
96
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
let success_message = format!(
|
|
62
|
-
"Check completed successfully in {:.2?}. Output files written to: '{}'",
|
|
63
|
-
duration.elapsed(),
|
|
64
|
-
normalized_output_dir
|
|
65
|
-
);
|
|
97
|
+
let normalized_entry_file = normalize_path(&entry);
|
|
98
|
+
let normalized_output_dir = normalize_path(&output);
|
|
66
99
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
}
|
|
100
|
+
let mut global_store = GlobalStore::new();
|
|
101
|
+
let module_loader = ModuleLoader::new(&normalized_entry_file, &normalized_output_dir);
|
|
71
102
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
StatementResolvedValue::Map(map) => {
|
|
77
|
-
for (key, value) in map {
|
|
78
|
-
if match_error_recursively_resolved_value(&value) {
|
|
79
|
-
return true;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
StatementResolvedValue::Array(array) => {
|
|
85
|
-
for item in array {
|
|
86
|
-
if match_error_recursively_resolved(&item) {
|
|
87
|
-
return true;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
_ => {
|
|
93
|
-
if let StatementKind::Error = stmt.kind {
|
|
94
|
-
eprintln!("❌ Error found in statement: {:?}", stmt);
|
|
95
|
-
return true;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
103
|
+
// SECTION Load
|
|
104
|
+
// NOTE: We don't use modules in the check command, but we still need to load them
|
|
105
|
+
let modules = module_loader.load_all_modules(&mut global_store);
|
|
99
106
|
|
|
100
|
-
|
|
101
|
-
}
|
|
107
|
+
// TODO: Implement debugging
|
|
102
108
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return true;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
StatementResolvedValue::Array(array) => {
|
|
114
|
-
for item in array {
|
|
115
|
-
if match_error_recursively_resolved(item) {
|
|
116
|
-
return true;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
_ => {}
|
|
121
|
-
}
|
|
109
|
+
let success_message = format!(
|
|
110
|
+
"Check completed successfully in {:.2?}. Output files written to: '{}'",
|
|
111
|
+
duration.elapsed(),
|
|
112
|
+
normalized_output_dir
|
|
113
|
+
);
|
|
122
114
|
|
|
123
|
-
|
|
115
|
+
let logger = Logger::new();
|
|
116
|
+
logger.log_message(LogLevel::Success, &success_message);
|
|
124
117
|
}
|
package/rust/cli/init.rs
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
use std::{ fs, path::Path };
|
|
2
|
+
use include_dir::{ include_dir, Dir };
|
|
3
|
+
use crate::{ cli::template::get_available_templates, utils::file::copy_dir_recursive };
|
|
4
|
+
|
|
5
|
+
#[cfg(feature = "cli")]
|
|
6
|
+
use inquire::{ Select, Confirm };
|
|
7
|
+
|
|
8
|
+
static TEMPLATES_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates");
|
|
9
|
+
|
|
10
|
+
#[cfg(feature = "cli")]
|
|
11
|
+
pub fn handle_init_command(name: Option<String>, template: Option<String>) {
|
|
12
|
+
let current_dir = std::env::current_dir().unwrap();
|
|
13
|
+
let project_name = name
|
|
14
|
+
.clone()
|
|
15
|
+
.unwrap_or_else(|| {
|
|
16
|
+
current_dir.file_name().unwrap_or_default().to_string_lossy().to_string()
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Select a template if not provided
|
|
20
|
+
let selected_template = template.unwrap_or_else(|| {
|
|
21
|
+
Select::new("Select a template for your project:", get_available_templates())
|
|
22
|
+
.prompt()
|
|
23
|
+
.unwrap_or_else(|_| {
|
|
24
|
+
eprintln!("No template selected. Exiting...");
|
|
25
|
+
std::process::exit(1);
|
|
26
|
+
})
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if selected_template.is_empty() {
|
|
30
|
+
eprintln!("Template cannot be empty.");
|
|
31
|
+
std::process::exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if name.is_none() {
|
|
35
|
+
// Case of initialization in the current directory
|
|
36
|
+
if fs::read_dir(¤t_dir).unwrap().next().is_some() {
|
|
37
|
+
let confirm = Confirm::new(
|
|
38
|
+
"The current directory is not empty. Do you want to continue?"
|
|
39
|
+
)
|
|
40
|
+
.with_default(false)
|
|
41
|
+
.prompt()
|
|
42
|
+
.unwrap_or(false);
|
|
43
|
+
|
|
44
|
+
if !confirm {
|
|
45
|
+
eprintln!("Operation cancelled by the user.");
|
|
46
|
+
std::process::exit(0);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
scaffold_project_current_dir(current_dir.as_path(), selected_template);
|
|
51
|
+
println!(
|
|
52
|
+
"✅ Initialized '{}' project in current directory: {}",
|
|
53
|
+
project_name,
|
|
54
|
+
current_dir.display()
|
|
55
|
+
);
|
|
56
|
+
} else {
|
|
57
|
+
// Case of initialization in a new directory
|
|
58
|
+
let target_path = current_dir.join(&project_name);
|
|
59
|
+
|
|
60
|
+
if target_path.exists() {
|
|
61
|
+
eprintln!("❌ A folder named '{}' already exists.", project_name);
|
|
62
|
+
std::process::exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fs::create_dir_all(&target_path).expect("Error creating project directory");
|
|
66
|
+
|
|
67
|
+
scaffold_project_current_dir(&target_path, selected_template);
|
|
68
|
+
println!("✅ Initialized '{}' project in: {}", project_name, target_path.display());
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fn scaffold_project_current_dir(path: &Path, template: String) {
|
|
73
|
+
let template_dir = TEMPLATES_DIR.get_dir(&template).unwrap_or_else(|| {
|
|
74
|
+
eprintln!("❌ The template '{}' doesn't exist.", template);
|
|
75
|
+
std::process::exit(1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
copy_dir_recursive(template_dir, path, &template_dir.path());
|
|
79
|
+
}
|
package/rust/cli/mod.rs
CHANGED
|
@@ -1,3 +1,205 @@
|
|
|
1
1
|
pub mod check;
|
|
2
|
-
pub mod new;
|
|
3
2
|
pub mod build;
|
|
3
|
+
pub mod init;
|
|
4
|
+
pub mod template;
|
|
5
|
+
pub mod play;
|
|
6
|
+
|
|
7
|
+
use clap::{ Parser, Subcommand };
|
|
8
|
+
use crate::utils::version::get_version;
|
|
9
|
+
|
|
10
|
+
#[derive(Parser)]
|
|
11
|
+
#[command(name = "devalang")]
|
|
12
|
+
#[command(author = "Devaloop")]
|
|
13
|
+
#[command(version = get_version())]
|
|
14
|
+
#[command(about = "🦊 Devalang – A programming language for music and sound.")]
|
|
15
|
+
pub struct Cli {
|
|
16
|
+
#[arg(long, global = true)]
|
|
17
|
+
/// Skips loading the configuration file.
|
|
18
|
+
pub no_config: bool,
|
|
19
|
+
|
|
20
|
+
#[command(subcommand)]
|
|
21
|
+
pub command: Commands,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#[derive(Subcommand)]
|
|
25
|
+
pub enum TemplateCommand {
|
|
26
|
+
/// Lists all available templates for Devalang projects.
|
|
27
|
+
List,
|
|
28
|
+
/// Displays information about a specific template.
|
|
29
|
+
Info {
|
|
30
|
+
name: String,
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#[derive(Subcommand)]
|
|
35
|
+
pub enum Commands {
|
|
36
|
+
/// Create a new Devalang project.
|
|
37
|
+
///
|
|
38
|
+
/// ### Arguments
|
|
39
|
+
/// - `name` - The name of the project to create.
|
|
40
|
+
/// - `template` - The template to use for the project. Defaults to "default".
|
|
41
|
+
///
|
|
42
|
+
/// ### Example
|
|
43
|
+
/// ```bash
|
|
44
|
+
/// devalang init --name my_project --template default
|
|
45
|
+
///
|
|
46
|
+
Init {
|
|
47
|
+
#[arg(short, long)]
|
|
48
|
+
/// The optional name (directory) of the project to create.
|
|
49
|
+
name: Option<String>,
|
|
50
|
+
|
|
51
|
+
#[arg(short, long)]
|
|
52
|
+
/// The template to use for the project.
|
|
53
|
+
///
|
|
54
|
+
/// ### Default value
|
|
55
|
+
/// - `default`
|
|
56
|
+
///
|
|
57
|
+
template: Option<String>,
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
Template {
|
|
61
|
+
#[command(subcommand)]
|
|
62
|
+
/// The template command to execute.
|
|
63
|
+
command: TemplateCommand,
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
/// Build the program and generate output files.
|
|
67
|
+
///
|
|
68
|
+
/// ### Arguments
|
|
69
|
+
/// - `entry` - The entry point of the program to build. Defaults to "./src".
|
|
70
|
+
/// - `output` - The directory where the output files will be generated. Defaults to "./output".
|
|
71
|
+
/// - `watch` - Whether to watch for changes and rebuild. Defaults to "true".
|
|
72
|
+
///
|
|
73
|
+
/// ### Example
|
|
74
|
+
/// ```bash
|
|
75
|
+
/// devalang build --entry ./src --output ./output --watch true
|
|
76
|
+
/// ```
|
|
77
|
+
///
|
|
78
|
+
Build {
|
|
79
|
+
#[arg(short, long)]
|
|
80
|
+
/// The entry point of the program to build.
|
|
81
|
+
///
|
|
82
|
+
entry: Option<String>,
|
|
83
|
+
|
|
84
|
+
#[arg(short, long)]
|
|
85
|
+
/// The directory where the output files will be generated.
|
|
86
|
+
///
|
|
87
|
+
output: Option<String>,
|
|
88
|
+
|
|
89
|
+
#[arg(long, default_value_t = false)]
|
|
90
|
+
/// Whether to watch for changes and rebuild.
|
|
91
|
+
///
|
|
92
|
+
/// ### Default value
|
|
93
|
+
/// - `false`
|
|
94
|
+
///
|
|
95
|
+
watch: bool,
|
|
96
|
+
|
|
97
|
+
#[arg(long, default_value = "real-time")]
|
|
98
|
+
/// The mode of compilation.
|
|
99
|
+
///
|
|
100
|
+
/// ### Default value
|
|
101
|
+
/// - `real-time`
|
|
102
|
+
///
|
|
103
|
+
/// ### Possible values
|
|
104
|
+
/// - `real-time` - Compiles files as soon as possible.
|
|
105
|
+
/// - `batch` - Compiles files one by one.
|
|
106
|
+
/// - `check` - Analyzes the code without compiling it.
|
|
107
|
+
///
|
|
108
|
+
compilation_mode: String,
|
|
109
|
+
|
|
110
|
+
#[arg(short, long, default_value_t = false)]
|
|
111
|
+
/// Whether to print debug information.
|
|
112
|
+
///
|
|
113
|
+
/// ### Default value
|
|
114
|
+
/// - `false`
|
|
115
|
+
///
|
|
116
|
+
debug: bool,
|
|
117
|
+
|
|
118
|
+
#[arg(short, long, default_value_t = false)]
|
|
119
|
+
/// Whether to compress the output files.
|
|
120
|
+
///
|
|
121
|
+
/// ### Default value
|
|
122
|
+
/// - `false`
|
|
123
|
+
///
|
|
124
|
+
compress: bool,
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
/// Analyze the program for errors and warnings.
|
|
128
|
+
///
|
|
129
|
+
/// ### Arguments
|
|
130
|
+
/// - `entry` - The entry point of the program to analyze. Defaults to "./src".
|
|
131
|
+
/// - `watch` - Whether to watch for changes and re-analyze. Defaults to "true".
|
|
132
|
+
///
|
|
133
|
+
/// ### Example
|
|
134
|
+
/// ```bash
|
|
135
|
+
/// devalang check --entry ./src --watch true --compilation-mode real-time
|
|
136
|
+
/// ```
|
|
137
|
+
Check {
|
|
138
|
+
#[arg(short, long)]
|
|
139
|
+
/// The entry point of the program to analyze.
|
|
140
|
+
///
|
|
141
|
+
entry: Option<String>,
|
|
142
|
+
|
|
143
|
+
#[arg(short, long)]
|
|
144
|
+
/// The directory where the output files will be generated.
|
|
145
|
+
///
|
|
146
|
+
output: Option<String>,
|
|
147
|
+
|
|
148
|
+
#[arg(long, default_value_t = false)]
|
|
149
|
+
/// Whether to watch for changes and re-analyze.
|
|
150
|
+
///
|
|
151
|
+
/// ### Default value
|
|
152
|
+
/// - `false`
|
|
153
|
+
///
|
|
154
|
+
watch: bool,
|
|
155
|
+
|
|
156
|
+
#[arg(short, long, default_value = "real-time")]
|
|
157
|
+
/// The mode of compilation.
|
|
158
|
+
///
|
|
159
|
+
/// ### Default value
|
|
160
|
+
/// - `real-time`
|
|
161
|
+
///
|
|
162
|
+
/// ### Possible values
|
|
163
|
+
/// - `real-time` - Analyzes files as soon as possible.
|
|
164
|
+
/// - `batch` - Analyzes files one by one.
|
|
165
|
+
/// - `check` - Analyzes the code without compiling it.
|
|
166
|
+
///
|
|
167
|
+
compilation_mode: String,
|
|
168
|
+
|
|
169
|
+
#[arg(short, long, default_value_t = false)]
|
|
170
|
+
/// Whether to print debug information.
|
|
171
|
+
///
|
|
172
|
+
/// ### Default value
|
|
173
|
+
/// - `false`
|
|
174
|
+
///
|
|
175
|
+
debug: bool,
|
|
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
|
+
},
|
|
205
|
+
}
|
package/rust/cli/play.rs
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
use crate::{
|
|
2
|
+
config::Config,
|
|
3
|
+
core::{
|
|
4
|
+
builder::Builder,
|
|
5
|
+
debugger::{ lexer::write_lexer_log_file, preprocessor::write_preprocessor_log_file },
|
|
6
|
+
preprocessor::loader::ModuleLoader,
|
|
7
|
+
store::global::GlobalStore,
|
|
8
|
+
utils::path::{ find_entry_file, normalize_path },
|
|
9
|
+
},
|
|
10
|
+
utils::{ logger::{ LogLevel, Logger }, spinner::with_spinner, watcher::watch_directory },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
use std::{ path::Path, sync::mpsc::channel, thread, time::Duration };
|
|
14
|
+
use std::fs;
|
|
15
|
+
use std::collections::HashMap;
|
|
16
|
+
|
|
17
|
+
#[cfg(feature = "cli")]
|
|
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
|
+
use crate::core::audio::player::AudioPlayer;
|
|
26
|
+
|
|
27
|
+
let logger = Logger::new();
|
|
28
|
+
|
|
29
|
+
let entry_path = entry
|
|
30
|
+
.or_else(|| config.as_ref().and_then(|c| c.defaults.entry.clone()))
|
|
31
|
+
.unwrap_or_else(|| "".to_string());
|
|
32
|
+
|
|
33
|
+
let output_path = output
|
|
34
|
+
.or_else(|| config.as_ref().and_then(|c| c.defaults.output.clone()))
|
|
35
|
+
.unwrap_or_else(|| "".to_string());
|
|
36
|
+
|
|
37
|
+
let fetched_repeat = if repeat {
|
|
38
|
+
true
|
|
39
|
+
} else {
|
|
40
|
+
config
|
|
41
|
+
.as_ref()
|
|
42
|
+
.and_then(|c| c.defaults.repeat)
|
|
43
|
+
.unwrap_or(false)
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if entry_path.is_empty() || output_path.is_empty() {
|
|
47
|
+
logger.log_message(LogLevel::Error, "Entry or output path not specified.");
|
|
48
|
+
std::process::exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let entry_file = find_entry_file(&entry_path).unwrap_or_else(|| {
|
|
52
|
+
logger.log_message(LogLevel::Error, "index.deva not found");
|
|
53
|
+
std::process::exit(1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
let audio_file = format!("{}/audio/index.wav", normalize_path(&output_path));
|
|
57
|
+
let mut audio_player = AudioPlayer::new();
|
|
58
|
+
|
|
59
|
+
if watch && fetched_repeat {
|
|
60
|
+
logger.log_message(
|
|
61
|
+
LogLevel::Error,
|
|
62
|
+
"Watch and repeat cannot be used together. Use repeat instead."
|
|
63
|
+
);
|
|
64
|
+
std::process::exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if watch {
|
|
68
|
+
let (tx, rx) = channel::<()>();
|
|
69
|
+
|
|
70
|
+
// Thread 1 : Watcher sending changes
|
|
71
|
+
let entry_clone = entry_path.clone();
|
|
72
|
+
thread::spawn(move || {
|
|
73
|
+
let _ = watch_directory(entry_clone, move || {
|
|
74
|
+
let _ = tx.send(()); // signal a change
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Main thread: build + play in a loop
|
|
79
|
+
begin_play(&config, &entry_file, &output_path);
|
|
80
|
+
audio_player.play_file_once(&audio_file);
|
|
81
|
+
|
|
82
|
+
logger.log_message(LogLevel::Watcher, "Watching for changes... Press Ctrl+C to exit.");
|
|
83
|
+
|
|
84
|
+
while let Ok(_) = rx.recv() {
|
|
85
|
+
logger.log_message(LogLevel::Watcher, "Change detected, rebuilding...");
|
|
86
|
+
|
|
87
|
+
begin_play(&config, &entry_file, &output_path);
|
|
88
|
+
|
|
89
|
+
logger.log_message(LogLevel::Info, "🎵 Playback started (once mode)...");
|
|
90
|
+
|
|
91
|
+
audio_player.play_file_once(&audio_file);
|
|
92
|
+
}
|
|
93
|
+
} else if fetched_repeat {
|
|
94
|
+
// Initial build to start from a clean slate
|
|
95
|
+
begin_play(&config, &entry_file, &output_path);
|
|
96
|
+
|
|
97
|
+
logger.log_message(LogLevel::Info, "🎵 Playback started (repeat mode)...");
|
|
98
|
+
|
|
99
|
+
let mut last_snapshot = snapshot_files(&entry_path);
|
|
100
|
+
let mut audio_player = AudioPlayer::new();
|
|
101
|
+
audio_player.play_file_once(&audio_file);
|
|
102
|
+
|
|
103
|
+
loop {
|
|
104
|
+
let current_snapshot = snapshot_files(&entry_path);
|
|
105
|
+
let has_changed = files_changed(&last_snapshot, ¤t_snapshot);
|
|
106
|
+
|
|
107
|
+
if has_changed {
|
|
108
|
+
logger.log_message(LogLevel::Info, "Change detected, rebuilding in background...");
|
|
109
|
+
let entry_file = entry_file.clone();
|
|
110
|
+
let output_path = output_path.clone();
|
|
111
|
+
let config_clone = config.clone();
|
|
112
|
+
|
|
113
|
+
// Rebuild in a separate thread
|
|
114
|
+
std::thread::spawn(move || {
|
|
115
|
+
begin_play(&config_clone, &entry_file, &output_path);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
last_snapshot = current_snapshot;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Wait for the audio to finish without blocking the current playback
|
|
122
|
+
audio_player.wait_until_end();
|
|
123
|
+
|
|
124
|
+
// Then replay the audio (rebuilt or not)
|
|
125
|
+
audio_player.play_file_once(&audio_file);
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
// Single execution
|
|
129
|
+
begin_play(&config, &entry_file, &output_path);
|
|
130
|
+
|
|
131
|
+
logger.log_message(LogLevel::Info, "🎵 Playback started (once mode)...");
|
|
132
|
+
|
|
133
|
+
audio_player.play_file_once(&audio_file);
|
|
134
|
+
audio_player.wait_until_end();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fn begin_play(config: &Option<Config>, entry_file: &str, output: &str) {
|
|
139
|
+
let spinner = with_spinner("Building...", || {
|
|
140
|
+
thread::sleep(Duration::from_millis(800));
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
let normalized_entry = normalize_path(entry_file);
|
|
144
|
+
let normalized_output_dir = normalize_path(&output);
|
|
145
|
+
|
|
146
|
+
let duration = std::time::Instant::now();
|
|
147
|
+
let mut global_store = GlobalStore::new();
|
|
148
|
+
let loader = ModuleLoader::new(&normalized_entry, &normalized_output_dir);
|
|
149
|
+
let (modules_tokens, modules_statements) = loader.load_all_modules(&mut global_store);
|
|
150
|
+
|
|
151
|
+
// SECTION Write logs
|
|
152
|
+
write_lexer_log_file(&normalized_output_dir, "lexer_tokens.log", modules_tokens.clone());
|
|
153
|
+
write_preprocessor_log_file(
|
|
154
|
+
&normalized_output_dir,
|
|
155
|
+
"resolved_statements.log",
|
|
156
|
+
modules_statements.clone()
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// SECTION Building AST and Audio
|
|
160
|
+
let builder = Builder::new();
|
|
161
|
+
builder.build_ast(&modules_statements, &output);
|
|
162
|
+
builder.build_audio(&modules_statements, &output, &mut global_store);
|
|
163
|
+
|
|
164
|
+
// SECTION Logging
|
|
165
|
+
let logger = Logger::new();
|
|
166
|
+
let success_message = format!(
|
|
167
|
+
"Build completed successfully in {:.2?}. Output files written to: '{}'",
|
|
168
|
+
duration.elapsed(),
|
|
169
|
+
normalized_output_dir
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
logger.log_message(LogLevel::Success, &success_message);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
fn snapshot_files<P: AsRef<Path>>(dir: P) -> HashMap<String, u64> {
|
|
176
|
+
let mut map = HashMap::new();
|
|
177
|
+
if let Ok(entries) = fs::read_dir(dir) {
|
|
178
|
+
for entry in entries.flatten() {
|
|
179
|
+
if let Ok(meta) = entry.metadata() {
|
|
180
|
+
if let Ok(mtime) = meta.modified() {
|
|
181
|
+
if let Ok(duration) = mtime.duration_since(std::time::UNIX_EPOCH) {
|
|
182
|
+
map.insert(entry.path().display().to_string(), duration.as_secs());
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
map
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
fn files_changed(old: &HashMap<String, u64>, new: &HashMap<String, u64>) -> bool {
|
|
192
|
+
old != new
|
|
193
|
+
}
|