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

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.
Files changed (52) hide show
  1. package/.devalang +1 -1
  2. package/Cargo.toml +9 -7
  3. package/README.md +49 -25
  4. package/docs/CHANGELOG.md +30 -0
  5. package/docs/COMMANDS.md +31 -0
  6. package/docs/CONFIG.md +6 -4
  7. package/docs/ROADMAP.md +4 -4
  8. package/docs/TODO.md +4 -4
  9. package/examples/index.deva +9 -2
  10. package/examples/samples/hat-808.wav +0 -0
  11. package/out-tsc/bin/devalang.exe +0 -0
  12. package/package.json +44 -42
  13. package/project-version.json +6 -6
  14. package/rust/audio/engine.rs +126 -0
  15. package/rust/audio/interpreter.rs +143 -0
  16. package/rust/audio/loader.rs +46 -0
  17. package/rust/audio/mod.rs +5 -0
  18. package/rust/audio/player.rs +54 -0
  19. package/rust/audio/render.rs +57 -0
  20. package/rust/cli/build.rs +17 -5
  21. package/rust/cli/check.rs +2 -1
  22. package/rust/cli/init.rs +4 -2
  23. package/rust/cli/mod.rs +29 -0
  24. package/rust/cli/play.rs +192 -0
  25. package/rust/cli/template.rs +2 -1
  26. package/rust/config/loader.rs +0 -1
  27. package/rust/config/mod.rs +3 -2
  28. package/rust/core/builder/mod.rs +54 -6
  29. package/rust/core/debugger/lexer.rs +20 -5
  30. package/rust/core/debugger/preprocessor.rs +9 -5
  31. package/rust/core/lexer/handler/mod.rs +2 -2
  32. package/rust/core/lexer/handler/newline.rs +5 -1
  33. package/rust/core/lexer/mod.rs +10 -5
  34. package/rust/core/parser/handler/loop_.rs +11 -0
  35. package/rust/core/parser/mod.rs +0 -1
  36. package/rust/core/preprocessor/loader.rs +89 -16
  37. package/rust/core/preprocessor/module.rs +2 -0
  38. package/rust/core/preprocessor/resolver/bank.rs +46 -0
  39. package/rust/core/preprocessor/resolver/loop_.rs +148 -0
  40. package/rust/core/preprocessor/resolver/mod.rs +151 -0
  41. package/rust/core/preprocessor/resolver/tempo.rs +49 -0
  42. package/rust/core/preprocessor/resolver/trigger.rs +114 -0
  43. package/rust/lib.rs +118 -0
  44. package/rust/main.rs +8 -0
  45. package/rust/utils/logger.rs +45 -6
  46. package/rust/utils/spinner.rs +2 -0
  47. package/rust/utils/watcher.rs +10 -2
  48. package/templates/minimal/.devalang +2 -1
  49. package/templates/minimal/README.md +202 -0
  50. package/templates/welcome/.devalang +2 -1
  51. package/templates/welcome/README.md +48 -31
  52. package/rust/core/preprocessor/resolver.rs +0 -372
@@ -0,0 +1,151 @@
1
+ pub mod trigger;
2
+ pub mod loop_;
3
+ pub mod bank;
4
+ pub mod tempo;
5
+
6
+ use std::collections::HashMap;
7
+ use crate::{
8
+ core::{
9
+ parser::statement::{ self, Statement, StatementKind },
10
+ preprocessor::{
11
+ loader::ModuleLoader,
12
+ resolver::{
13
+ bank::resolve_bank,
14
+ loop_::resolve_loop,
15
+ tempo::resolve_tempo,
16
+ trigger::resolve_trigger,
17
+ },
18
+ },
19
+ shared::{ duration::Duration, value::Value },
20
+ store::global::GlobalStore,
21
+ utils::validation::{ is_valid_entity, is_valid_identifier },
22
+ },
23
+ utils::logger::Logger,
24
+ };
25
+
26
+ pub fn resolve_all_modules(module_loader: &ModuleLoader, global_store: &mut GlobalStore) {
27
+ for module in global_store.clone().modules.values_mut() {
28
+ resolve_imports(module_loader, global_store);
29
+ }
30
+ }
31
+
32
+ pub fn resolve_imports(module_loader: &ModuleLoader, global_store: &mut GlobalStore) {
33
+ for (module_path, module) in global_store.clone().modules.iter_mut() {
34
+ for (name, source_path) in &module.import_table.imports {
35
+ match source_path {
36
+ Value::String(source_path) => {
37
+ if let Some(source_module) = global_store.modules.get(source_path) {
38
+ if let Some(value) = source_module.export_table.get_export(name) {
39
+ module.variable_table.set(name.clone(), value.clone());
40
+ } else {
41
+ println!(
42
+ "[warn] '{module_path}': '{name}' not found in exports of '{source_path}'"
43
+ );
44
+ }
45
+ } else {
46
+ println!(
47
+ "[warn] '{module_path}': cannot find source module '{source_path}'"
48
+ );
49
+ }
50
+ }
51
+ _ => {
52
+ println!(
53
+ "[warn] '{module_path}': expected string for import source, found {:?}",
54
+ source_path
55
+ );
56
+ }
57
+ }
58
+ }
59
+ }
60
+ }
61
+
62
+ pub fn resolve_and_flatten_all_modules(
63
+ global_store: &mut GlobalStore
64
+ ) -> HashMap<String, Vec<Statement>> {
65
+ let logger = Logger::new();
66
+ let snapshot = global_store.clone();
67
+
68
+ // 1. Imports resolution
69
+ for (module_path, module) in global_store.modules.iter_mut() {
70
+ for (name, source_path) in &module.import_table.imports {
71
+ if let Value::String(source_path_str) = source_path {
72
+ match snapshot.modules.get(source_path_str) {
73
+ Some(source_module) => {
74
+ if let Some(value) = source_module.export_table.get_export(name) {
75
+ module.variable_table.set(name.clone(), value.clone());
76
+ } else {
77
+ logger.log_error_with_stacktrace(
78
+ &format!("'{name}' not found in exports of '{source_path_str}'"),
79
+ module_path
80
+ );
81
+ }
82
+ }
83
+ None => {
84
+ logger.log_error_with_stacktrace(
85
+ &format!("Cannot find source module '{source_path_str}'"),
86
+ module_path
87
+ );
88
+ }
89
+ }
90
+ } else {
91
+ logger.log_error_with_stacktrace(
92
+ &format!("Expected string for import source, found {:?}", source_path),
93
+ module_path
94
+ );
95
+ }
96
+ }
97
+ }
98
+
99
+ // 2. Statements resolution
100
+ let mut resolved_map: HashMap<String, Vec<Statement>> = HashMap::new();
101
+ let store_snapshot = global_store.clone();
102
+
103
+ for (path, module) in &store_snapshot.modules {
104
+ let mut resolved = Vec::new();
105
+
106
+ for stmt in &module.statements {
107
+ let mut stmt = stmt.clone();
108
+
109
+ match &stmt.kind {
110
+ StatementKind::Trigger { entity, duration } => {
111
+ let resolved_stmt = resolve_trigger(
112
+ &stmt,
113
+ entity.as_str(),
114
+ &mut duration.clone(),
115
+ &module,
116
+ &path,
117
+ &store_snapshot
118
+ );
119
+ resolved.push(resolved_stmt);
120
+ }
121
+
122
+ StatementKind::Loop => {
123
+ let resolved_stmt = resolve_loop(&stmt, &module, &path, &store_snapshot);
124
+ resolved.push(resolved_stmt);
125
+ }
126
+
127
+ StatementKind::Bank => {
128
+ let resolved_stmt = resolve_bank(&stmt, &module, &path, &store_snapshot);
129
+ resolved.push(resolved_stmt);
130
+ }
131
+
132
+ StatementKind::Tempo => {
133
+ let resolved_stmt = resolve_tempo(&stmt, &module, &path, &store_snapshot);
134
+ resolved.push(resolved_stmt);
135
+ }
136
+
137
+ StatementKind::Import { .. } | StatementKind::Export { .. } => {
138
+ resolved.push(stmt.clone());
139
+ }
140
+
141
+ _ => {
142
+ resolved.push(stmt);
143
+ }
144
+ }
145
+ }
146
+
147
+ resolved_map.insert(path.clone(), resolved);
148
+ }
149
+
150
+ resolved_map
151
+ }
@@ -0,0 +1,49 @@
1
+ use crate::{
2
+ core::{
3
+ parser::statement::{ Statement, StatementKind },
4
+ preprocessor::module::Module,
5
+ shared::value::Value,
6
+ store::global::GlobalStore,
7
+ },
8
+ utils::logger::Logger,
9
+ };
10
+
11
+ pub fn resolve_tempo(
12
+ stmt: &Statement,
13
+ module: &Module,
14
+ path: &str,
15
+ _global_store: &GlobalStore
16
+ ) -> Statement {
17
+ let mut new_stmt = stmt.clone();
18
+ let logger = Logger::new();
19
+
20
+ match &stmt.value {
21
+ Value::Identifier(ident) => {
22
+ if let Some(val) = module.variable_table.get(ident) {
23
+ new_stmt.value = val.clone();
24
+ } else {
25
+ let message = format!("Tempo identifier '{ident}' not found in variable table");
26
+ logger.log_error_with_stacktrace(&message, &module.path);
27
+ new_stmt.kind = StatementKind::Error {
28
+ message,
29
+ };
30
+ new_stmt.value = Value::Null;
31
+ }
32
+ }
33
+
34
+ Value::Number(_) => {
35
+ // Already resolved, no modification needed
36
+ }
37
+
38
+ other => {
39
+ let message = format!("Expected a number or identifier for tempo, found {:?}", other);
40
+ logger.log_error_with_stacktrace(&message, &module.path);
41
+ new_stmt.kind = StatementKind::Error {
42
+ message: "Expected a number or identifier for tempo".to_string(),
43
+ };
44
+ new_stmt.value = Value::Null;
45
+ }
46
+ }
47
+
48
+ new_stmt
49
+ }
@@ -0,0 +1,114 @@
1
+ use std::collections::HashMap;
2
+
3
+ use crate::{
4
+ core::{
5
+ parser::statement::{ Statement, StatementKind },
6
+ preprocessor::module::Module,
7
+ shared::{ duration::Duration, value::Value },
8
+ store::global::GlobalStore,
9
+ utils::validation::is_valid_entity,
10
+ },
11
+ utils::logger::Logger,
12
+ };
13
+
14
+ pub fn resolve_trigger(
15
+ stmt: &Statement,
16
+ entity: &str,
17
+ duration: &mut Duration,
18
+ module: &Module,
19
+ path: &str,
20
+ global_store: &GlobalStore
21
+ ) -> Statement {
22
+ let logger = Logger::new();
23
+
24
+ let mut final_duration = duration.clone();
25
+ let mut final_value = stmt.value.clone();
26
+
27
+ if !is_valid_entity(entity, module, global_store) {
28
+ let message = format!("Invalid entity '{}', expected a valid identifier", entity);
29
+ let stacktrace = format!("{}:{}:{}", module.path, stmt.line, stmt.column);
30
+ logger.log_error_with_stacktrace(&message, &stacktrace);
31
+
32
+ return Statement {
33
+ kind: stmt.kind.clone(),
34
+ value: Value::Null,
35
+ line: stmt.line,
36
+ column: stmt.column,
37
+ indent: stmt.indent,
38
+ };
39
+ }
40
+
41
+ // ✅ Résolution de duration si c'est un identifiant
42
+ if let Duration::Identifier(ident) = duration {
43
+ if let Some(val) = module.variable_table.get(ident) {
44
+ match val {
45
+ Value::Number(num) => {
46
+ final_duration = Duration::Number(*num);
47
+ }
48
+ Value::String(s) => {
49
+ final_duration = Duration::Identifier(s.clone());
50
+ }
51
+ Value::Identifier(id) if id == "auto" => {
52
+ final_duration = Duration::Auto;
53
+ }
54
+ _ => {}
55
+ }
56
+ }
57
+ }
58
+
59
+ // ✅ Résolution de value (params, effets)
60
+ final_value = match &stmt.value {
61
+ Value::Identifier(ident) => {
62
+ match module.variable_table.get(ident) {
63
+ Some(val) => val.clone(),
64
+ None => {
65
+ let stacktrace = format!("{}:{}:{}", module.path, stmt.line, stmt.column);
66
+ let message = format!(
67
+ "'{path}': value identifier '{ident}' not found in variable table"
68
+ );
69
+ logger.log_error_with_stacktrace(&message, &stacktrace);
70
+ Value::Null
71
+ }
72
+ }
73
+ }
74
+ Value::Map(map) => {
75
+ let mut resolved_map = HashMap::new();
76
+ for (k, v) in map.iter() {
77
+ let resolved_v = match v {
78
+ Value::Identifier(id) => {
79
+ module.variable_table.get(id).cloned().unwrap_or(Value::Null)
80
+ }
81
+ other => other.clone(),
82
+ };
83
+ resolved_map.insert(k.clone(), resolved_v);
84
+ }
85
+ Value::Map(resolved_map)
86
+ }
87
+ other => other.clone(),
88
+ };
89
+
90
+ // ✅ On reconstruit le Statement avec Trigger résolu
91
+ if let StatementKind::Trigger { entity, .. } = &stmt.kind {
92
+ return Statement {
93
+ kind: StatementKind::Trigger {
94
+ entity: entity.to_string(),
95
+ duration: final_duration,
96
+ },
97
+ value: final_value,
98
+ line: stmt.line,
99
+ column: stmt.column,
100
+ indent: stmt.indent,
101
+ };
102
+ }
103
+
104
+ return Statement {
105
+ kind: StatementKind::Trigger {
106
+ entity: entity.to_string(),
107
+ duration: final_duration,
108
+ },
109
+ value: final_value,
110
+ line: stmt.line,
111
+ column: stmt.column,
112
+ indent: stmt.indent,
113
+ };
114
+ }
package/rust/lib.rs CHANGED
@@ -0,0 +1,118 @@
1
+ pub mod core;
2
+ pub mod utils;
3
+ pub mod config;
4
+ pub mod audio;
5
+
6
+ use serde::{ Deserialize, Serialize };
7
+ use wasm_bindgen::prelude::*;
8
+ use serde_wasm_bindgen::to_value;
9
+
10
+ use crate::core::{
11
+ parser::statement::{ Statement, StatementKind },
12
+ preprocessor::loader::ModuleLoader,
13
+ shared::value::Value,
14
+ store::global::GlobalStore,
15
+ utils::path::normalize_path,
16
+ };
17
+
18
+ #[derive(Serialize, Deserialize)]
19
+ struct ParseResult {
20
+ ok: bool,
21
+ ast: String,
22
+ errors: Vec<ErrorResult>,
23
+ }
24
+
25
+ #[derive(Serialize, Deserialize)]
26
+ struct ErrorResult {
27
+ message: String,
28
+ line: usize,
29
+ column: usize,
30
+ }
31
+
32
+ #[wasm_bindgen]
33
+ pub fn parse(entry_path: &str, source: &str) -> Result<JsValue, JsValue> {
34
+ let statements = parse_internal_from_string(entry_path, source);
35
+
36
+ match statements {
37
+ Ok(value) => {
38
+ let ast_string = value;
39
+ to_value(&ast_string).map_err(|e|
40
+ JsValue::from_str(&format!("Error converting AST to JS value: {}", e))
41
+ )
42
+ }
43
+ Err(e) => { Err(JsValue::from_str(&format!("Error: {}", e))) }
44
+ }
45
+ }
46
+
47
+ fn parse_internal_from_string(virtual_path: &str, source: &str) -> Result<ParseResult, String> {
48
+ let entry_path = normalize_path(virtual_path);
49
+ let output_path = normalize_path("./temp");
50
+
51
+ let mut global_store = GlobalStore::new();
52
+ let loader = ModuleLoader::from_raw_source(
53
+ &entry_path,
54
+ &output_path,
55
+ source,
56
+ &mut global_store
57
+ );
58
+
59
+ let module = loader
60
+ .load_single_module(&mut global_store)
61
+ .map_err(|e| format!("Error loading module: {}", e))?;
62
+
63
+ let raw_ast = ast_to_string(module.statements.clone());
64
+
65
+ let found_errors = collect_errors_recursively(&module.statements);
66
+
67
+ let result = ParseResult {
68
+ ok: true,
69
+ ast: raw_ast,
70
+ errors: found_errors,
71
+ };
72
+
73
+ Ok(result)
74
+ }
75
+
76
+ fn collect_errors_recursively(statements: &[Statement]) -> Vec<ErrorResult> {
77
+ let mut errors: Vec<ErrorResult> = Vec::new();
78
+
79
+ for stmt in statements {
80
+ match &stmt.kind {
81
+ StatementKind::Unknown => {
82
+ errors.push(ErrorResult {
83
+ message: format!("Unknown statement at line {}:{}", stmt.line, stmt.column),
84
+ line: stmt.line,
85
+ column: stmt.column,
86
+ });
87
+ }
88
+ StatementKind::Error { message } => {
89
+ errors.push(ErrorResult {
90
+ message: message.clone(),
91
+ line: stmt.line,
92
+ column: stmt.column,
93
+ });
94
+ }
95
+ StatementKind::Loop => {
96
+ if let Some(body_statements) = extract_loop_body_statements(&stmt.value) {
97
+ errors.extend(collect_errors_recursively(body_statements));
98
+ }
99
+ }
100
+ _ => {}
101
+ }
102
+ }
103
+
104
+ errors
105
+ }
106
+
107
+ fn extract_loop_body_statements(value: &Value) -> Option<&[Statement]> {
108
+ if let Value::Map(map) = value {
109
+ if let Some(Value::Block(statements)) = map.get("body") {
110
+ return Some(statements);
111
+ }
112
+ }
113
+ None
114
+ }
115
+
116
+ fn ast_to_string(statements: Vec<Statement>) -> String {
117
+ serde_json::to_string_pretty(&statements).expect("Failed to serialize AST")
118
+ }
package/rust/main.rs CHANGED
@@ -1,7 +1,10 @@
1
+ #![cfg(feature = "cli")]
2
+
1
3
  pub mod core;
2
4
  pub mod cli;
3
5
  pub mod utils;
4
6
  pub mod config;
7
+ pub mod audio;
5
8
 
6
9
  use std::io;
7
10
  use cli::{ Cli };
@@ -11,6 +14,7 @@ use crate::{
11
14
  build::handle_build_command,
12
15
  check::handle_check_command,
13
16
  init::handle_init_command,
17
+ play::handle_play_command,
14
18
  template::{ handle_template_info_command, handle_template_list_command },
15
19
  Commands,
16
20
  TemplateCommand,
@@ -51,6 +55,10 @@ fn main() -> io::Result<()> {
51
55
  handle_build_command(config, entry, output, watch);
52
56
  }
53
57
 
58
+ Commands::Play { entry, output, watch, repeat } => {
59
+ handle_play_command(config, entry, output, watch, repeat);
60
+ }
61
+
54
62
  _ => {}
55
63
  }
56
64
 
@@ -1,5 +1,6 @@
1
- use crossterm::style::{ Attribute, Color, ResetColor, SetAttribute, SetForegroundColor };
2
- use std::{ fmt::Write };
1
+ #[cfg(feature = "cli")]
2
+ use crossterm::style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor};
3
+ use std::fmt::Write;
3
4
 
4
5
  #[derive(Debug, Clone, PartialEq)]
5
6
  pub enum LogLevel {
@@ -12,24 +13,43 @@ pub enum LogLevel {
12
13
  }
13
14
 
14
15
  #[derive(Debug, Clone)]
15
- pub struct Logger {}
16
+ pub struct Logger;
16
17
 
17
18
  impl Logger {
18
19
  pub fn new() -> Self {
19
- Logger {}
20
+ Logger
20
21
  }
21
22
 
23
+ // --- log_message ---
24
+
25
+ #[cfg(feature = "cli")]
22
26
  pub fn log_message(&self, level: LogLevel, message: &str) {
23
27
  let formatted_status = self.format_status(level);
24
28
  println!("🦊 {} {} {}", self.language_signature(), formatted_status, message);
25
29
  }
26
30
 
31
+ #[cfg(not(feature = "cli"))]
32
+ pub fn log_message(&self, _level: LogLevel, _message: &str) {
33
+ // no-op for WASM
34
+ }
35
+
36
+ // --- log_error_with_stacktrace ---
37
+
38
+ #[cfg(feature = "cli")]
27
39
  pub fn log_error_with_stacktrace(&self, message: &str, stacktrace: &str) {
28
40
  let formatted_status = self.format_status(LogLevel::Error);
29
41
  println!("🦊 {} {} {}", self.language_signature(), formatted_status, message);
30
42
  println!(" ↳ {}", stacktrace);
31
43
  }
32
44
 
45
+ #[cfg(not(feature = "cli"))]
46
+ pub fn log_error_with_stacktrace(&self, _message: &str, _stacktrace: &str) {
47
+ // no-op for WASM
48
+ }
49
+
50
+ // --- language_signature ---
51
+
52
+ #[cfg(feature = "cli")]
33
53
  fn language_signature(&self) -> String {
34
54
  let mut s = String::new();
35
55
 
@@ -43,12 +63,19 @@ impl Logger {
43
63
 
44
64
  write!(&mut s, "{}", SetForegroundColor(Color::Grey)).unwrap();
45
65
  s.push(']');
46
-
47
66
  write!(&mut s, "{}", ResetColor).unwrap();
48
67
 
49
68
  s
50
69
  }
51
70
 
71
+ #[cfg(not(feature = "cli"))]
72
+ fn language_signature(&self) -> String {
73
+ "[Devalang]".to_string()
74
+ }
75
+
76
+ // --- format_status ---
77
+
78
+ #[cfg(feature = "cli")]
52
79
  fn format_status(&self, level: LogLevel) -> String {
53
80
  let mut s = String::new();
54
81
 
@@ -76,9 +103,21 @@ impl Logger {
76
103
  s.push_str(status);
77
104
  write!(&mut s, "{}", SetAttribute(Attribute::Reset)).unwrap();
78
105
  s.push(']');
79
-
80
106
  write!(&mut s, "{}", ResetColor).unwrap();
81
107
 
82
108
  s
83
109
  }
110
+
111
+ #[cfg(not(feature = "cli"))]
112
+ fn format_status(&self, level: LogLevel) -> String {
113
+ match level {
114
+ LogLevel::Success => "[SUCCESS]",
115
+ LogLevel::Error => "[ERROR]",
116
+ LogLevel::Info => "[INFO]",
117
+ LogLevel::Warning => "[WARNING]",
118
+ LogLevel::Watcher => "[WATCHER]",
119
+ LogLevel::Debug => "[DEBUG]",
120
+ }
121
+ .to_string()
122
+ }
84
123
  }
@@ -1,6 +1,8 @@
1
+ #[cfg(feature = "cli")]
1
2
  use indicatif::{ ProgressBar, ProgressStyle };
2
3
  use std::{ time::Duration };
3
4
 
5
+ #[cfg(feature = "cli")]
4
6
  pub fn with_spinner<T, F>(start_msg: &str, f: F) -> T where F: FnOnce() -> T {
5
7
  let spinner = ProgressBar::new_spinner();
6
8
  spinner.set_style(
@@ -1,6 +1,8 @@
1
1
  use notify::{ Watcher, RecursiveMode, Config, RecommendedWatcher };
2
2
  use std::sync::mpsc::channel;
3
3
 
4
+ use std::time::{ Duration, Instant };
5
+
4
6
  pub fn watch_directory<F>(entry: String, callback: F) -> notify::Result<()>
5
7
  where F: Fn() + Send + 'static
6
8
  {
@@ -9,13 +11,19 @@ pub fn watch_directory<F>(entry: String, callback: F) -> notify::Result<()>
9
11
  let mut watcher: RecommendedWatcher = Watcher::new(tx, Config::default())?;
10
12
  watcher.watch(&entry.as_ref(), RecursiveMode::Recursive)?;
11
13
 
14
+ let mut last_trigger = Instant::now();
15
+
12
16
  loop {
13
17
  match rx.recv() {
14
18
  Ok(_) => {
15
- callback();
19
+ let now = Instant::now();
20
+ if now.duration_since(last_trigger) > Duration::from_millis(200) {
21
+ callback();
22
+ last_trigger = now;
23
+ }
16
24
  }
17
25
  Err(e) => {
18
- eprintln!("Channel error : {:?}", e);
26
+ eprintln!("Channel error: {:?}", e);
19
27
  break;
20
28
  }
21
29
  }
@@ -1,4 +1,5 @@
1
1
  [defaults]
2
2
  entry = "./src"
3
3
  output = "./output"
4
- watch = false
4
+ watch = false
5
+ repeat = false