@devaloop/devalang 0.0.1-alpha.1

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 (72) hide show
  1. package/Cargo.toml +45 -0
  2. package/LICENSE +21 -0
  3. package/README.md +161 -0
  4. package/docs/COMMANDS.md +31 -0
  5. package/docs/ROADMAP.md +27 -0
  6. package/docs/SYNTAX.md +148 -0
  7. package/docs/TODO.md +66 -0
  8. package/examples/exported.deva +7 -0
  9. package/examples/index.deva +9 -0
  10. package/examples/samples/kick-808.wav +0 -0
  11. package/out-tsc/bin/devalang.exe +0 -0
  12. package/out-tsc/bin/index.js +13 -0
  13. package/out-tsc/index.js +2 -0
  14. package/out-tsc/scripts/postbuild.js +11 -0
  15. package/out-tsc/scripts/version/bump.js +52 -0
  16. package/out-tsc/scripts/version/fetch.js +34 -0
  17. package/out-tsc/scripts/version/index.js +32 -0
  18. package/out-tsc/scripts/version/sync.js +32 -0
  19. package/package.json +43 -0
  20. package/project-version.json +6 -0
  21. package/rust/audio/mod.rs +1 -0
  22. package/rust/cli/build.rs +51 -0
  23. package/rust/cli/check.rs +124 -0
  24. package/rust/cli/mod.rs +3 -0
  25. package/rust/cli/new.rs +1 -0
  26. package/rust/core/builder/mod.rs +37 -0
  27. package/rust/core/debugger/mod.rs +57 -0
  28. package/rust/core/lexer/mod.rs +333 -0
  29. package/rust/core/mod.rs +6 -0
  30. package/rust/core/parser/at.rs +142 -0
  31. package/rust/core/parser/bank.rs +42 -0
  32. package/rust/core/parser/dot.rs +107 -0
  33. package/rust/core/parser/identifer.rs +91 -0
  34. package/rust/core/parser/loop_.rs +62 -0
  35. package/rust/core/parser/mod.rs +201 -0
  36. package/rust/core/parser/tempo.rs +42 -0
  37. package/rust/core/parser/variable.rs +129 -0
  38. package/rust/core/preprocessor/dependencies.rs +54 -0
  39. package/rust/core/preprocessor/mod.rs +26 -0
  40. package/rust/core/preprocessor/module.rs +70 -0
  41. package/rust/core/preprocessor/resolver/at.rs +24 -0
  42. package/rust/core/preprocessor/resolver/bank.rs +59 -0
  43. package/rust/core/preprocessor/resolver/loop_.rs +82 -0
  44. package/rust/core/preprocessor/resolver/mod.rs +113 -0
  45. package/rust/core/preprocessor/resolver/tempo.rs +70 -0
  46. package/rust/core/preprocessor/resolver/trigger.rs +176 -0
  47. package/rust/core/types/cli.rs +160 -0
  48. package/rust/core/types/mod.rs +7 -0
  49. package/rust/core/types/module.rs +41 -0
  50. package/rust/core/types/parser.rs +73 -0
  51. package/rust/core/types/statement.rs +105 -0
  52. package/rust/core/types/store.rs +116 -0
  53. package/rust/core/types/token.rs +83 -0
  54. package/rust/core/types/variable.rs +32 -0
  55. package/rust/lib.rs +1 -0
  56. package/rust/main.rs +49 -0
  57. package/rust/runner/executer.rs +44 -0
  58. package/rust/runner/mod.rs +1 -0
  59. package/rust/utils/loader.rs +19 -0
  60. package/rust/utils/logger.rs +49 -0
  61. package/rust/utils/mod.rs +5 -0
  62. package/rust/utils/path.rs +46 -0
  63. package/rust/utils/signature.rs +17 -0
  64. package/rust/utils/version.rs +15 -0
  65. package/tsconfig.json +113 -0
  66. package/typescript/bin/index.ts +14 -0
  67. package/typescript/index.ts +1 -0
  68. package/typescript/scripts/postbuild.ts +8 -0
  69. package/typescript/scripts/version/bump.ts +45 -0
  70. package/typescript/scripts/version/fetch.ts +23 -0
  71. package/typescript/scripts/version/index.ts +26 -0
  72. package/typescript/scripts/version/sync.ts +24 -0
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.syncVersion = void 0;
16
+ const fs_1 = __importDefault(require("fs"));
17
+ const path_1 = __importDefault(require("path"));
18
+ const syncVersion = (projectVersionPath) => __awaiter(void 0, void 0, void 0, function* () {
19
+ const version = fs_1.default.readFileSync(projectVersionPath, "utf-8").trim();
20
+ const versionString = JSON.parse(version).version;
21
+ // Package.json
22
+ const pkgPath = path_1.default.join(__dirname, "..", "..", "..", "package.json");
23
+ const pkgJson = JSON.parse(fs_1.default.readFileSync(pkgPath, "utf-8"));
24
+ pkgJson.version = versionString;
25
+ fs_1.default.writeFileSync(pkgPath, JSON.stringify(pkgJson, null, 2));
26
+ // Cargo.toml
27
+ const cargoPath = path_1.default.join(__dirname, "..", "..", "..", "Cargo.toml");
28
+ const cargoToml = fs_1.default.readFileSync(cargoPath, "utf-8");
29
+ const updatedCargo = cargoToml.replace(/(version\s*=\s*")[^"]*(")/, `$1${versionString}$2`);
30
+ fs_1.default.writeFileSync(cargoPath, updatedCargo);
31
+ });
32
+ exports.syncVersion = syncVersion;
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@devaloop/devalang",
3
+ "private": false,
4
+ "version": "0.0.1-alpha.1",
5
+ "description": "Write music like code. Devalang is a domain-specific language (DSL) for sound designers and music hackers. Compose, automate, and control sound — in plain text.",
6
+ "main": "out-tsc/index.js",
7
+ "bin": {
8
+ "devalang": "./out-tsc/bin/index.js"
9
+ },
10
+ "scripts": {
11
+ "prepublish": "cargo build --release && npm run script:postbuild",
12
+ "rust:dev": "cargo run",
13
+ "rust:dev:build": "cargo run build --entry examples --output output",
14
+ "rust:dev:check": "cargo run check --entry examples --output output",
15
+ "script:postbuild": "tsc && node out-tsc/scripts/postbuild.js",
16
+ "script:version:bump": "tsc && node out-tsc/scripts/version/index.js"
17
+ },
18
+ "homepage": "https://devaloop.com",
19
+ "keywords": [
20
+ "devalang",
21
+ "music",
22
+ "sound",
23
+ "domain-specific language",
24
+ "dsl",
25
+ "programming language",
26
+ "sound design",
27
+ "music hacking",
28
+ "audio",
29
+ "synthesis",
30
+ "scripting",
31
+ "sound synthesis",
32
+ "music programming"
33
+ ],
34
+ "author": "Devaloop",
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/devaloop-labs/devalang.git"
39
+ },
40
+ "dependencies": {
41
+ "@types/node": "^24.0.3"
42
+ }
43
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": "0.0.1-alpha.1",
3
+ "channel": "alpha",
4
+ "lastCommit": "",
5
+ "build": 1
6
+ }
@@ -0,0 +1 @@
1
+ // TODO Audio engine
@@ -0,0 +1,51 @@
1
+ use std::{ thread, time::Duration };
2
+
3
+ use crate::{
4
+ core::{
5
+ builder::{ build_ast, write_ast_to_file },
6
+ debugger::Debugger,
7
+ preprocessor::module::load_all_modules,
8
+ },
9
+ runner::executer::execute_statements,
10
+ utils::{ loader::with_spinner, logger::log_message, path::{ find_entry_file, normalize_path } },
11
+ };
12
+
13
+ pub fn handle_build_command(entry: String, output: String) {
14
+ let entry_file = find_entry_file(&entry).unwrap_or_else(|| {
15
+ eprintln!("❌ index.deva not found in directory: {}", entry);
16
+ std::process::exit(1);
17
+ });
18
+
19
+ let spinner = with_spinner("Building...", || {
20
+ thread::sleep(Duration::from_millis(800));
21
+ });
22
+
23
+ let duration = std::time::Instant::now();
24
+
25
+ let normalized_entry_file = normalize_path(&entry_file);
26
+ let normalized_output_dir = normalize_path(&output);
27
+
28
+ let global_store = load_all_modules(&normalized_entry_file);
29
+
30
+ if let Some(module) = global_store.modules.get(&normalized_entry_file) {
31
+ let mut module_clone = module.clone();
32
+
33
+ let resolved_statements = execute_statements(&mut module_clone);
34
+
35
+ let ast = build_ast(&resolved_statements);
36
+
37
+ let ast_dir = format!("{}/json", normalized_output_dir.clone());
38
+ write_ast_to_file(&ast, &ast_dir);
39
+
40
+ let debugger = Debugger::new(&module_clone);
41
+ let debug_dir = format!("{}/debug/", normalized_output_dir.clone());
42
+ debugger.write_files(debug_dir.as_str(), resolved_statements);
43
+
44
+ let success_message = format!(
45
+ "Build completed successfully in {:.2?}. Output files written to: '{}'",
46
+ duration.elapsed(),
47
+ normalized_output_dir
48
+ );
49
+ log_message(&success_message, "SUCCESS");
50
+ }
51
+ }
@@ -0,0 +1,124 @@
1
+ use std::{ thread, time::Duration };
2
+
3
+ use crate::{
4
+ core::{
5
+ debugger::Debugger,
6
+ preprocessor::module::load_all_modules,
7
+ types::{
8
+ statement::{
9
+ Statement,
10
+ StatementIterator,
11
+ StatementKind,
12
+ StatementResolved,
13
+ StatementResolvedValue,
14
+ },
15
+ variable::VariableValue,
16
+ },
17
+ },
18
+ runner::executer::execute_statements,
19
+ utils::{ loader::with_spinner, logger::log_message, path::{ find_entry_file, normalize_path } },
20
+ };
21
+
22
+ pub fn handle_check_command(entry: String, output: String) -> () {
23
+ let entry_file = find_entry_file(&entry).unwrap_or_else(|| {
24
+ eprintln!("❌ index.deva not found in directory: {}", entry);
25
+ std::process::exit(1);
26
+ });
27
+
28
+ let spinner = with_spinner("Checking...", || {
29
+ thread::sleep(Duration::from_millis(800));
30
+ });
31
+
32
+ let duration = std::time::Instant::now();
33
+
34
+ let normalized_entry_file = normalize_path(&entry_file);
35
+ let normalized_output_dir = normalize_path(&output);
36
+
37
+ let global_store = load_all_modules(&normalized_entry_file);
38
+
39
+ if let Some(module) = global_store.modules.get(&normalized_entry_file) {
40
+ let mut module_clone = module.clone();
41
+
42
+ let resolved_statements = execute_statements(&mut module_clone);
43
+
44
+ let debugger = Debugger::new(&module_clone);
45
+ let debug_dir = format!("{}/debug/", normalized_output_dir.clone());
46
+ debugger.write_files(debug_dir.as_str(), resolved_statements.clone());
47
+
48
+ let has_errors = resolved_statements.iter().any(|stmt| {
49
+ match_error_recursively_resolved(&stmt.clone())
50
+ });
51
+
52
+ if has_errors {
53
+ let warning_message = format!(
54
+ "Check completed with errors in {:.2?}. Output files written to: '{}'",
55
+ duration.elapsed(),
56
+ normalized_output_dir
57
+ );
58
+
59
+ log_message(&warning_message, "WARNING");
60
+ } else {
61
+ let success_message = format!(
62
+ "Check completed successfully in {:.2?}. Output files written to: '{}'",
63
+ duration.elapsed(),
64
+ normalized_output_dir
65
+ );
66
+
67
+ log_message(&success_message, "SUCCESS");
68
+ }
69
+ }
70
+ }
71
+
72
+ fn match_error_recursively_resolved(stmt: &StatementResolved) -> bool {
73
+ match stmt.value.clone() {
74
+ // TODO Other statement value types here
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
+ }
99
+
100
+ false
101
+ }
102
+
103
+ fn match_error_recursively_resolved_value(value: &StatementResolvedValue) -> bool {
104
+ match value {
105
+ StatementResolvedValue::Map(map) => {
106
+ for (_, v) in map {
107
+ if match_error_recursively_resolved_value(v) {
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
+ }
122
+
123
+ false
124
+ }
@@ -0,0 +1,3 @@
1
+ pub mod check;
2
+ pub mod new;
3
+ pub mod build;
@@ -0,0 +1 @@
1
+ // TODO Implement the new command to create a new project with a template
@@ -0,0 +1,37 @@
1
+ use crate::core::types::statement::{ StatementResolved };
2
+ use std::fs::File;
3
+ use std::io::Write;
4
+
5
+ pub fn build_ast(statements: &Vec<StatementResolved>) -> String {
6
+ let mut ast_string = String::new();
7
+
8
+ serde_json
9
+ ::to_string_pretty(statements)
10
+ .map(|json| {
11
+ ast_string.push_str(&json);
12
+ })
13
+ .unwrap_or_else(|err| {
14
+ eprintln!("Error serializing AST: {}", err);
15
+ std::process::exit(1);
16
+ });
17
+
18
+ ast_string
19
+ }
20
+
21
+ pub fn write_ast_to_file(ast: &str, file_path: &str) {
22
+ clear_json_directory(&file_path);
23
+ create_json_directory(&file_path);
24
+
25
+ let file_path = format!("{}/ast.json", file_path);
26
+
27
+ let mut file = File::create(&file_path).expect("Unable to create AST file");
28
+ file.write_all(ast.as_bytes()).expect("Unable to write AST to file");
29
+ }
30
+
31
+ fn clear_json_directory(path: &str) {
32
+ std::fs::remove_dir_all(path);
33
+ }
34
+
35
+ fn create_json_directory(path: &str) {
36
+ std::fs::create_dir_all(path);
37
+ }
@@ -0,0 +1,57 @@
1
+ use crate::core::types::{ module::Module, statement::{ StatementResolved } };
2
+
3
+ pub struct Debugger {
4
+ pub module: Module,
5
+ }
6
+
7
+ impl Debugger {
8
+ pub fn new(module: &Module) -> Self {
9
+ Debugger {
10
+ module: module.clone(),
11
+ }
12
+ }
13
+
14
+ pub fn write_files(&self, output_dir: &str, resolved_statements: Vec<StatementResolved>) {
15
+ const LEXER_FILENAME: &str = "debug_lexer.log";
16
+ const STATEMENTS_FILENAME: &str = "debug_statements.log";
17
+
18
+ let lexer_path = format!("{}{}", output_dir, LEXER_FILENAME);
19
+ let statements_path = format!("{}{}", output_dir, STATEMENTS_FILENAME);
20
+
21
+ // Collect debug information
22
+ let tokens = self.module.tokens
23
+ .iter()
24
+ .map(|token| format!("{:?}", token))
25
+ .collect::<Vec<String>>();
26
+ let statements = resolved_statements
27
+ .iter()
28
+ .map(|stmt| format!("{:?}", stmt))
29
+ .collect::<Vec<String>>();
30
+
31
+ // Ensure the debug directory exists and is cleared
32
+ clear_debug_directory(output_dir);
33
+ create_debug_directory(output_dir);
34
+
35
+ // Writing files
36
+ write_tokens_debug_to_file(&tokens, &lexer_path);
37
+ write_statements_debug_to_file(&statements, &statements_path);
38
+ }
39
+ }
40
+
41
+ fn clear_debug_directory(path: &str) {
42
+ std::fs::remove_dir_all(path);
43
+ }
44
+
45
+ fn create_debug_directory(path: &str) {
46
+ std::fs::create_dir_all(path);
47
+ }
48
+
49
+ fn write_statements_debug_to_file(statements: &Vec<String>, path: &str) {
50
+ let content = statements.join("\n");
51
+ std::fs::write(path, content).expect("Unable to write statements to file");
52
+ }
53
+
54
+ fn write_tokens_debug_to_file(tokens: &Vec<String>, path: &str) {
55
+ let content = tokens.join("\n");
56
+ std::fs::write(path, content).expect("Unable to write tokens to file");
57
+ }
@@ -0,0 +1,333 @@
1
+ use crate::core::types::token::{ Token, TokenKind };
2
+
3
+ pub fn lex(input: String) -> Vec<Token> {
4
+ let mut tokens = Vec::new();
5
+
6
+ let mut line = 1;
7
+ let mut column = 1;
8
+
9
+ let mut indent_stack: Vec<usize> = vec![0];
10
+ let mut current_indent = 0;
11
+ let mut at_line_start = true;
12
+
13
+ let mut chars = input.chars().peekable();
14
+
15
+ while let Some(_) = chars.peek() {
16
+ if at_line_start {
17
+ current_indent = 0;
18
+
19
+ while let Some(&c) = chars.peek() {
20
+ if c == ' ' {
21
+ current_indent += 1;
22
+ chars.next();
23
+ column += 1;
24
+ } else {
25
+ break;
26
+ }
27
+ }
28
+
29
+ let last_indent = *indent_stack.last().unwrap();
30
+ if current_indent > last_indent {
31
+ indent_stack.push(current_indent);
32
+ tokens.push(Token {
33
+ kind: TokenKind::Indent,
34
+ lexeme: String::new(),
35
+ line,
36
+ column,
37
+ indent: current_indent,
38
+ });
39
+ } else {
40
+ while current_indent < *indent_stack.last().unwrap() {
41
+ indent_stack.pop();
42
+ tokens.push(Token {
43
+ kind: TokenKind::Dedent,
44
+ lexeme: String::new(),
45
+ line,
46
+ column,
47
+ indent: current_indent,
48
+ });
49
+ }
50
+ }
51
+
52
+ at_line_start = false;
53
+ }
54
+
55
+ let Some(ch) = chars.next() else {
56
+ break;
57
+ };
58
+
59
+ if ch == '\n' {
60
+ tokens.push(Token {
61
+ kind: TokenKind::Newline,
62
+ lexeme: ch.to_string(),
63
+ line,
64
+ column,
65
+ indent: current_indent,
66
+ });
67
+
68
+ line += 1;
69
+ column = 1;
70
+ at_line_start = true;
71
+
72
+ continue;
73
+ }
74
+
75
+ if ch == ' ' || ch == '\t' {
76
+ column += if ch == '\t' { 4 } else { 1 };
77
+ continue;
78
+ }
79
+
80
+ if ch == '#' {
81
+ let mut comment = String::new();
82
+ while let Some(&c) = chars.peek() {
83
+ if c == '\n' {
84
+ break;
85
+ }
86
+ comment.push(c);
87
+ chars.next();
88
+ column += 1;
89
+ }
90
+ tokens.push(Token {
91
+ kind: TokenKind::Comment(comment.trim().to_string()),
92
+ lexeme: ch.to_string(),
93
+ line,
94
+ column,
95
+ indent: current_indent,
96
+ });
97
+ continue;
98
+ }
99
+
100
+ if ch == ':' {
101
+ tokens.push(Token {
102
+ kind: TokenKind::Colon,
103
+ lexeme: ch.to_string(),
104
+ line,
105
+ column,
106
+ indent: current_indent,
107
+ });
108
+ column += 1;
109
+ continue;
110
+ }
111
+
112
+ if ch == '=' {
113
+ if let Some('=') = chars.peek() {
114
+ chars.next();
115
+ tokens.push(Token {
116
+ kind: TokenKind::DoubleEquals,
117
+ lexeme: ch.to_string(),
118
+ line,
119
+ column,
120
+ indent: current_indent,
121
+ });
122
+ column += 2;
123
+ } else {
124
+ tokens.push(Token {
125
+ kind: TokenKind::Equals,
126
+ lexeme: ch.to_string(),
127
+ line,
128
+ column,
129
+ indent: current_indent,
130
+ });
131
+ column += 1;
132
+ }
133
+ continue;
134
+ }
135
+
136
+ if ch == '[' {
137
+ tokens.push(Token {
138
+ kind: TokenKind::LBracket,
139
+ lexeme: ch.to_string(),
140
+ line,
141
+ column,
142
+ indent: current_indent,
143
+ });
144
+ column += 1;
145
+ continue;
146
+ }
147
+
148
+ if ch == ']' {
149
+ tokens.push(Token {
150
+ kind: TokenKind::RBracket,
151
+ lexeme: ch.to_string(),
152
+ line,
153
+ column,
154
+ indent: current_indent,
155
+ });
156
+ column += 1;
157
+ continue;
158
+ }
159
+
160
+ if ch == '{' {
161
+ tokens.push(Token {
162
+ kind: TokenKind::LBrace,
163
+ lexeme: ch.to_string(),
164
+ line,
165
+ column,
166
+ indent: current_indent,
167
+ });
168
+ column += 1;
169
+ continue;
170
+ }
171
+
172
+ if ch == '}' {
173
+ tokens.push(Token {
174
+ kind: TokenKind::RBrace,
175
+ lexeme: ch.to_string(),
176
+ line,
177
+ column,
178
+ indent: current_indent,
179
+ });
180
+ column += 1;
181
+ continue;
182
+ }
183
+
184
+ if ch == '"' {
185
+ let mut string_content = String::new();
186
+ column += 1; // skip the opening quote
187
+
188
+ while let Some(next_ch) = chars.next() {
189
+ column += 1;
190
+ if next_ch == '"' {
191
+ break; // closing quote reached
192
+ } else {
193
+ string_content.push(next_ch);
194
+ }
195
+ }
196
+
197
+ tokens.push(Token {
198
+ kind: TokenKind::String,
199
+ lexeme: string_content,
200
+ line,
201
+ column,
202
+ indent: current_indent,
203
+ });
204
+
205
+ continue;
206
+ }
207
+
208
+ if ch == '\'' {
209
+ let mut string_content = String::new();
210
+ column += 1;
211
+
212
+ while let Some(next_ch) = chars.next() {
213
+ column += 1;
214
+ if next_ch == '\'' {
215
+ break;
216
+ } else {
217
+ string_content.push(next_ch);
218
+ }
219
+ }
220
+
221
+ tokens.push(Token {
222
+ kind: TokenKind::String,
223
+ lexeme: string_content,
224
+ line,
225
+ column,
226
+ indent: current_indent,
227
+ });
228
+
229
+ continue;
230
+ }
231
+
232
+ if ch == '.' {
233
+ tokens.push(Token {
234
+ kind: TokenKind::Dot,
235
+ lexeme: ch.to_string(),
236
+ line,
237
+ column,
238
+ indent: current_indent,
239
+ });
240
+ column += 1;
241
+ continue;
242
+ }
243
+
244
+ if ch == '@' {
245
+ tokens.push(Token {
246
+ kind: TokenKind::At,
247
+ lexeme: ch.to_string(),
248
+ line,
249
+ column,
250
+ indent: current_indent,
251
+ });
252
+ column += 1;
253
+ continue;
254
+ }
255
+
256
+ if ch.is_ascii_digit() {
257
+ let mut number = ch.to_string();
258
+ while let Some(&c) = chars.peek() {
259
+ if c.is_ascii_digit() {
260
+ number.push(c);
261
+ chars.next();
262
+ column += 1;
263
+ } else {
264
+ break;
265
+ }
266
+ }
267
+
268
+ // let value = number.parse::<f32>().unwrap();
269
+ tokens.push(Token {
270
+ kind: TokenKind::Number,
271
+ lexeme: number,
272
+ line,
273
+ column,
274
+ indent: current_indent,
275
+ });
276
+ continue;
277
+ }
278
+
279
+ if ch.is_ascii_alphabetic() {
280
+ let mut ident = ch.to_string();
281
+ while let Some(&c) = chars.peek() {
282
+ if c.is_ascii_alphanumeric() || c == '_' {
283
+ ident.push(c);
284
+ chars.next();
285
+ column += 1;
286
+ } else {
287
+ break;
288
+ }
289
+ }
290
+
291
+ let kind = match ident.as_str() {
292
+ "bank" => TokenKind::Bank,
293
+ "bpm" => TokenKind::Tempo,
294
+ "loop" => TokenKind::Loop,
295
+ _ => TokenKind::Identifier,
296
+ };
297
+
298
+ tokens.push(Token {
299
+ kind,
300
+ lexeme: ident,
301
+ line,
302
+ column,
303
+ indent: current_indent,
304
+ });
305
+ continue;
306
+ }
307
+
308
+ // Skip unknown char
309
+ column += 1;
310
+ }
311
+
312
+ while indent_stack.len() > 1 {
313
+ indent_stack.pop();
314
+ current_indent = *indent_stack.last().unwrap();
315
+ tokens.push(Token {
316
+ kind: TokenKind::Dedent,
317
+ lexeme: String::new(),
318
+ line,
319
+ column,
320
+ indent: current_indent,
321
+ });
322
+ }
323
+
324
+ tokens.push(Token {
325
+ kind: TokenKind::EOF,
326
+ lexeme: String::new(),
327
+ line: line + 1, // EOF is considered to be on the next line
328
+ column: 0, // EOF has no column
329
+ indent: 0, // EOF has no indent
330
+ });
331
+
332
+ tokens
333
+ }