@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.
- package/Cargo.toml +45 -0
- package/LICENSE +21 -0
- package/README.md +161 -0
- package/docs/COMMANDS.md +31 -0
- package/docs/ROADMAP.md +27 -0
- package/docs/SYNTAX.md +148 -0
- package/docs/TODO.md +66 -0
- package/examples/exported.deva +7 -0
- package/examples/index.deva +9 -0
- package/examples/samples/kick-808.wav +0 -0
- package/out-tsc/bin/devalang.exe +0 -0
- package/out-tsc/bin/index.js +13 -0
- package/out-tsc/index.js +2 -0
- package/out-tsc/scripts/postbuild.js +11 -0
- package/out-tsc/scripts/version/bump.js +52 -0
- package/out-tsc/scripts/version/fetch.js +34 -0
- package/out-tsc/scripts/version/index.js +32 -0
- package/out-tsc/scripts/version/sync.js +32 -0
- package/package.json +43 -0
- package/project-version.json +6 -0
- package/rust/audio/mod.rs +1 -0
- package/rust/cli/build.rs +51 -0
- package/rust/cli/check.rs +124 -0
- package/rust/cli/mod.rs +3 -0
- package/rust/cli/new.rs +1 -0
- package/rust/core/builder/mod.rs +37 -0
- package/rust/core/debugger/mod.rs +57 -0
- package/rust/core/lexer/mod.rs +333 -0
- package/rust/core/mod.rs +6 -0
- package/rust/core/parser/at.rs +142 -0
- package/rust/core/parser/bank.rs +42 -0
- package/rust/core/parser/dot.rs +107 -0
- package/rust/core/parser/identifer.rs +91 -0
- package/rust/core/parser/loop_.rs +62 -0
- package/rust/core/parser/mod.rs +201 -0
- package/rust/core/parser/tempo.rs +42 -0
- package/rust/core/parser/variable.rs +129 -0
- package/rust/core/preprocessor/dependencies.rs +54 -0
- package/rust/core/preprocessor/mod.rs +26 -0
- package/rust/core/preprocessor/module.rs +70 -0
- package/rust/core/preprocessor/resolver/at.rs +24 -0
- package/rust/core/preprocessor/resolver/bank.rs +59 -0
- package/rust/core/preprocessor/resolver/loop_.rs +82 -0
- package/rust/core/preprocessor/resolver/mod.rs +113 -0
- package/rust/core/preprocessor/resolver/tempo.rs +70 -0
- package/rust/core/preprocessor/resolver/trigger.rs +176 -0
- package/rust/core/types/cli.rs +160 -0
- package/rust/core/types/mod.rs +7 -0
- package/rust/core/types/module.rs +41 -0
- package/rust/core/types/parser.rs +73 -0
- package/rust/core/types/statement.rs +105 -0
- package/rust/core/types/store.rs +116 -0
- package/rust/core/types/token.rs +83 -0
- package/rust/core/types/variable.rs +32 -0
- package/rust/lib.rs +1 -0
- package/rust/main.rs +49 -0
- package/rust/runner/executer.rs +44 -0
- package/rust/runner/mod.rs +1 -0
- package/rust/utils/loader.rs +19 -0
- package/rust/utils/logger.rs +49 -0
- package/rust/utils/mod.rs +5 -0
- package/rust/utils/path.rs +46 -0
- package/rust/utils/signature.rs +17 -0
- package/rust/utils/version.rs +15 -0
- package/tsconfig.json +113 -0
- package/typescript/bin/index.ts +14 -0
- package/typescript/index.ts +1 -0
- package/typescript/scripts/postbuild.ts +8 -0
- package/typescript/scripts/version/bump.ts +45 -0
- package/typescript/scripts/version/fetch.ts +23 -0
- package/typescript/scripts/version/index.ts +26 -0
- 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 @@
|
|
|
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
|
+
}
|
package/rust/cli/mod.rs
ADDED
package/rust/cli/new.rs
ADDED
|
@@ -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
|
+
}
|