├── src ├── util │ ├── mod.rs │ ├── file.rs │ └── scripts_compiler.rs ├── lib.rs ├── parser │ ├── mod.rs │ ├── RathenaScriptLang.tokens │ └── RathenaScriptLangLexer.tokens └── lang │ ├── mod.rs │ ├── noop_hasher.rs │ ├── stack_trace.rs │ ├── stack.rs │ ├── call_frame.rs │ ├── error.rs │ ├── array.rs │ ├── class.rs │ ├── type_checker.rs │ ├── chunk.rs │ └── value.rs ├── .gitignore ├── bin ├── antlr-rust.jar └── antlr4.9.4-rust.jar ├── Cargo.toml ├── .github └── workflows │ └── rust.yml ├── tests ├── script_test.rs ├── return_test.rs ├── mutlithead_test.rs ├── stdlib_test.rs ├── native_test.rs ├── operator_test.rs ├── performance_test.rs ├── small_script_benchmark.rs ├── global_variables_test.rs ├── label_test.rs ├── loop_test.rs ├── common │ └── mod.rs ├── function_test.rs ├── condition_test.rs └── class_test.rs ├── native_functions_list.txt ├── README.md └── RathenaScriptLang.g4 /src/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod file; 2 | pub mod scripts_compiler; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea 3 | .project 4 | .stfolder 5 | .stignore -------------------------------------------------------------------------------- /bin/antlr-rust.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmeylan/rathena-script-lang-interpreter/HEAD/bin/antlr-rust.jar -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(try_blocks)] 2 | extern crate core; 3 | 4 | pub mod lang; 5 | pub mod parser; 6 | pub mod util; -------------------------------------------------------------------------------- /bin/antlr4.9.4-rust.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmeylan/rathena-script-lang-interpreter/HEAD/bin/antlr4.9.4-rust.jar -------------------------------------------------------------------------------- /src/parser/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::all)] 2 | #![allow(unused_braces)] 3 | #![allow(unused_parens)] 4 | 5 | pub mod rathenascriptlanglexer; 6 | pub mod rathenascriptlanglistener; 7 | pub mod rathenascriptlangvisitor; 8 | pub mod rathenascriptlangparser; -------------------------------------------------------------------------------- /src/lang/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod compiler; 2 | pub mod class; 3 | pub mod vm; 4 | pub mod noop_hasher; 5 | pub mod value; 6 | pub mod stack; 7 | pub mod call_frame; 8 | pub mod thread; 9 | pub mod chunk; 10 | pub mod stack_trace; 11 | pub mod error; 12 | mod native; 13 | pub mod array; 14 | mod type_checker; -------------------------------------------------------------------------------- /src/lang/noop_hasher.rs: -------------------------------------------------------------------------------- 1 | use std::hash::{BuildHasher, Hasher}; 2 | 3 | #[derive(Clone, Default)] 4 | pub struct NoopHasher { 5 | hash: u64 6 | } 7 | 8 | impl BuildHasher for NoopHasher { 9 | type Hasher = NoopHasher; 10 | 11 | fn build_hasher(&self) -> Self::Hasher { 12 | NoopHasher::default() 13 | } 14 | } 15 | 16 | impl Hasher for NoopHasher { 17 | fn finish(&self) -> u64 { 18 | self.hash 19 | } 20 | 21 | fn write(&mut self, bytes: &[u8]) { 22 | // Key is supposed to be hash already. we don't want to hash the hash. 23 | self.hash = u64::from_ne_bytes(bytes.try_into().unwrap()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/util/file.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::{env, io}; 3 | use std::io::BufRead; 4 | use std::path::{Path, PathBuf}; 5 | 6 | pub fn read_lines(filename: impl AsRef + Clone) -> Result>, String> { 7 | let file = File::open(filename.clone()) 8 | .map_err(|_| format!("Can't find file at {:?}", absolute_path(filename)))?; 9 | Ok(io::BufReader::new(file).lines()) 10 | } 11 | 12 | pub fn absolute_path(path: impl AsRef) -> io::Result { 13 | let path = path.as_ref(); 14 | 15 | let absolute_path = if path.is_absolute() { 16 | path.to_path_buf() 17 | } else { 18 | env::current_dir()?.join(path) 19 | }; 20 | 21 | Ok(absolute_path) 22 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rathena_script_lang_interpreter" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | 7 | [lib] 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | #antlr-rust = "0.2.2" 12 | antlr-rust = { git = "https://github.com/nmeylan/antlr4rust", rev = "7d91d3e"} 13 | serde = {version = "1", features = ["derive", "rc"]} 14 | bitcode = { version = "0.5.1", features = ["serde", "derive"] } 15 | fastrand = "2.0.1" 16 | # 17 | #[patch."https://github.com/nmeylan/antlr4rust"] 18 | #antlr-rust = {path = "C:\\dev\\antlr4rust"} 19 | #antlr-rust = {path = "/home/nmeylan/dev/perso/antlr4rust"} 20 | 21 | 22 | [dev-dependencies] 23 | criterion = "0.4.0" 24 | 25 | [[bench]] 26 | name = "class_vs_repl" 27 | path = "tests/small_script_benchmark.rs" 28 | harness = false -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/cache@v3 19 | with: 20 | path: | 21 | ~/.cargo/bin/ 22 | ~/.cargo/registry/index/ 23 | ~/.cargo/registry/cache/ 24 | ~/.cargo/git/db/ 25 | target/ 26 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 27 | - name: Install latest nightly 28 | uses: actions-rs/toolchain@v1 29 | with: 30 | toolchain: nightly 31 | override: true 32 | - name: Build 33 | run: cargo build --release --verbose 34 | - name: Run tests 35 | run: cargo test --release --verbose 36 | -------------------------------------------------------------------------------- /src/lang/stack_trace.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter}; 2 | use crate::lang::compiler::CompilationDetail; 3 | 4 | #[derive(Clone, Debug)] 5 | pub struct StackTrace { 6 | pub file_name: String, 7 | pub callframe_name: String, 8 | pub class_name: String, 9 | pub line_number: usize, 10 | } 11 | 12 | impl StackTrace { 13 | pub fn from_compilation_detail(compilation_detail: &CompilationDetail, callframe_name: String, class_name: String) -> Self{ 14 | Self { 15 | file_name: compilation_detail.file_name.clone(), 16 | callframe_name, 17 | class_name, 18 | line_number: compilation_detail.start_line, 19 | } 20 | } 21 | } 22 | 23 | impl Display for StackTrace { 24 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 25 | write!(f, "{}\n\tat {}({}:{})", self.callframe_name, self.file_name, self.class_name,self.line_number) 26 | } 27 | } -------------------------------------------------------------------------------- /tests/script_test.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::{Arc, Mutex}; 3 | use rathena_script_lang_interpreter::lang::compiler; 4 | use rathena_script_lang_interpreter::lang::vm::{DebugFlag, Vm}; 5 | use crate::common::{compile_script, Event, VmHook}; 6 | 7 | mod common; 8 | 9 | #[test] 10 | fn simple_script() { 11 | // Given 12 | let events = Arc::new(Mutex::new(HashMap::::new())); 13 | let classes = compile_script(r#" 14 | vm_dump_var("a", "hello"); 15 | "#, compiler::DebugFlag::None.value()).unwrap(); 16 | let events_clone = events.clone(); 17 | let vm = crate::common::setup_vm(DebugFlag::None.value()); 18 | // When 19 | let vm_hook = VmHook::new( Box::new(move |e| { events_clone.lock().unwrap().insert(e.name.clone(), e); })); 20 | Vm::repl(vm, &classes[1], Box::new(&vm_hook), vec![]).unwrap(); 21 | // Then 22 | assert_eq!(String::from("hello"), events.lock().unwrap().get("a").unwrap().value.string_value().unwrap().clone()); 23 | } -------------------------------------------------------------------------------- /tests/return_test.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::collections::HashMap; 3 | 4 | use std::sync::{Arc, Mutex}; 5 | use rathena_script_lang_interpreter::lang::compiler; 6 | 7 | 8 | use rathena_script_lang_interpreter::lang::vm::{DebugFlag, Vm}; 9 | use crate::common::{compile_script, Event, VmHook}; 10 | 11 | mod common; 12 | 13 | #[test] 14 | fn return_stop_script() { 15 | // Given 16 | let events = Arc::new(Mutex::new(HashMap::::new())); 17 | let classes = compile_script(r#" 18 | .@a$ = "hello world"; 19 | return; 20 | vm_dump_var("a", .@a$); 21 | "#, compiler::DebugFlag::None.value()).unwrap(); 22 | let events_clone = events.clone(); 23 | let vm = crate::common::setup_vm(DebugFlag::None.value()); 24 | // When 25 | let vm_hook = VmHook::new( Box::new(move |e| { events_clone.lock().unwrap().insert(e.name.clone(), e); })); 26 | Vm::bootstrap(vm.clone(), classes, Box::new(&vm_hook)); 27 | Vm::execute_main_script(vm, Box::new(&vm_hook), vec![]).unwrap(); 28 | // Then 29 | assert_eq!(true, events.lock().unwrap().get("a").is_none()); 30 | } -------------------------------------------------------------------------------- /tests/mutlithead_test.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::collections::HashMap; 3 | 4 | use std::sync::{Arc, Mutex}; 5 | use std::thread; 6 | use rathena_script_lang_interpreter::lang::compiler; 7 | 8 | 9 | use rathena_script_lang_interpreter::lang::vm::{DebugFlag, Vm}; 10 | use crate::common::{compile_script, Event, VmHook}; 11 | 12 | mod common; 13 | 14 | #[test] 15 | fn multithread_support_test() { 16 | // Given 17 | let events = Arc::new(Mutex::new(HashMap::::new())); 18 | let classes = compile_script(r#" 19 | .@a$ = "hello world"; 20 | vm_dump_var("a", .@a$); 21 | "#, compiler::DebugFlag::None.value()).unwrap(); 22 | let events_clone = events.clone(); 23 | let vm = crate::common::setup_vm(DebugFlag::None.value()); 24 | // When 25 | let vm_hook = VmHook::new( Box::new(move |e| { events_clone.lock().unwrap().insert(e.name.clone(), e); })); 26 | Vm::bootstrap(vm.clone(), classes, Box::new(&vm_hook)); 27 | thread::spawn(move || { 28 | Vm::execute_main_script(vm, Box::new(&vm_hook), vec![]).unwrap(); 29 | }).join().unwrap(); 30 | 31 | // Then 32 | assert_eq!(true, events.lock().unwrap().get("a").is_some()); 33 | } -------------------------------------------------------------------------------- /native_functions_list.txt: -------------------------------------------------------------------------------- 1 | // list native function like this: 2 | // native_function_name, min arguments, max arguments, <, know return type> 3 | println, 1, 1 4 | vm_dump_var, 2, 2 5 | vm_dump_locals, 0, 0 6 | setglobalvariable,3,3 7 | getglobalvariable,2,2 8 | loadconstant,1,1 9 | mes,1,255 10 | close,0,0 11 | next,0,0 12 | select,1,255,Number 13 | getlook,1,2,Number 14 | setlook,2,3 15 | getbattleflag,1,1,Number 16 | message,2,2 17 | strcharinfo,1,1,String 18 | input,1,3 19 | sprintf,1,255 20 | menu,2,256 21 | nativeacceptingarrayref,1,1 22 | // Start: found in items script 23 | skill,2,3 24 | bonus,1,2 25 | bonus2,3,3 26 | bonus3,4,4 27 | bonus4,5,5 28 | bonus5,6,6 29 | // not implemented yet, but here to make compilation pass 30 | readparam,1,2 31 | autobonus,3,5 32 | autobonus2,3,5 33 | autobonus3,3,5 34 | getrefine,0,0 35 | getskilllv,1,1 36 | getequiprefinerycnt,1,2 37 | vip_status,1,2 38 | gettime,1,1 39 | isequipped,1,256 40 | getequipid,1,2 41 | getiteminfo,1,2 42 | sc_start,3,6 43 | sc_start2,4,7 44 | sc_start4,6,9 45 | sc_end,1,2 46 | itemheal,2,3 47 | getrandgroupitem,1,5 48 | monster,6,9 49 | percentheal,2,3 50 | produce,1,1 51 | pet,1,1 52 | guildgetexp,1,1 53 | getpartnerid,0,1 54 | getitem,2,3 55 | cooking,1,1 56 | skilleffect,2,2 57 | heal,2,3 58 | countitem,1,2 59 | delitem,2,3 60 | // End: found in items script -------------------------------------------------------------------------------- /src/parser/RathenaScriptLang.tokens: -------------------------------------------------------------------------------- 1 | T__0=1 2 | T__1=2 3 | T__2=3 4 | T__3=4 5 | T__4=5 6 | T__5=6 7 | T__6=7 8 | T__7=8 9 | T__8=9 10 | LeftParen=10 11 | RightParen=11 12 | LeftBrace=12 13 | RightBrace=13 14 | LeftBracket=14 15 | RightBracket=15 16 | Comma=16 17 | At=17 18 | Colon=18 19 | SemiColon=19 20 | Percent=20 21 | Star=21 22 | Tilde=22 23 | QuestionMark=23 24 | Quote=24 25 | DoubleQuote=25 26 | Underscore=26 27 | BitOr=27 28 | OrOp=28 29 | BitAnd=29 30 | AndOp=30 31 | Slash=31 32 | SlashStar=32 33 | StarSlash=33 34 | DoubleSlash=34 35 | Sharp=35 36 | DoubleSharp=36 37 | Minus=37 38 | DecrementOp=38 39 | Plus=39 40 | IncrementOp=40 41 | Dot=41 42 | DotAt=42 43 | Dollar=43 44 | DollarAt=44 45 | Bang=45 46 | BangEqual=46 47 | Equal=47 48 | DoubleEqual=48 49 | LeftCaret=49 50 | DoubleLeftCaret=50 51 | LeftCaretEqual=51 52 | RightCaret=52 53 | DoubleRightCaret=53 54 | RightCaretEqual=54 55 | PlusEqual=55 56 | MinusEqual=56 57 | MultiplyEqual=57 58 | DivideEqual=58 59 | PercentEqual=59 60 | If=60 61 | Else=61 62 | End=62 63 | Set=63 64 | For=64 65 | While=65 66 | Do=66 67 | Goto=67 68 | Return=68 69 | Switch=69 70 | Case=70 71 | Default=71 72 | Function=72 73 | Break=73 74 | Callfunc=74 75 | Callsub=75 76 | Eof=76 77 | Setarray=77 78 | Copyarray=78 79 | True=79 80 | False=80 81 | Menu=81 82 | Close=82 83 | Close2=83 84 | Next=84 85 | Script=85 86 | Declare=86 87 | Identifier=87 88 | Label=88 89 | String=89 90 | Number=90 91 | Whitespace=91 92 | Newline=92 93 | BlockComment=93 94 | LineComment=94 95 | '^'=1 96 | '<<='=2 97 | '>>='=3 98 | '&='=4 99 | '^='=5 100 | '|='=6 101 | 'continue'=7 102 | 'duplicate'=8 103 | 'shop'=9 104 | '('=10 105 | ')'=11 106 | '{'=12 107 | '}'=13 108 | '['=14 109 | ']'=15 110 | ','=16 111 | '@'=17 112 | ':'=18 113 | ';'=19 114 | '%'=20 115 | '*'=21 116 | '~'=22 117 | '?'=23 118 | '\''=24 119 | '"'=25 120 | '_'=26 121 | '|'=27 122 | '||'=28 123 | '&'=29 124 | '&&'=30 125 | '/'=31 126 | '/*'=32 127 | '*/'=33 128 | '//'=34 129 | '#'=35 130 | '##'=36 131 | '-'=37 132 | '--'=38 133 | '+'=39 134 | '++'=40 135 | '.'=41 136 | '.@'=42 137 | '$'=43 138 | '$@'=44 139 | '!'=45 140 | '!='=46 141 | '='=47 142 | '=='=48 143 | '<'=49 144 | '<<'=50 145 | '<='=51 146 | '>'=52 147 | '>>'=53 148 | '>='=54 149 | '+='=55 150 | '-='=56 151 | '*='=57 152 | '/='=58 153 | '%='=59 154 | 'if'=60 155 | 'else'=61 156 | 'end'=62 157 | 'set'=63 158 | 'for'=64 159 | 'while'=65 160 | 'do'=66 161 | 'goto'=67 162 | 'return'=68 163 | 'switch'=69 164 | 'case'=70 165 | 'default:'=71 166 | 'function'=72 167 | 'break'=73 168 | 'callfunc'=74 169 | 'callsub'=75 170 | 'eof'=76 171 | 'setarray'=77 172 | 'copyarray'=78 173 | 'true'=79 174 | 'false'=80 175 | 'menu'=81 176 | 'close'=82 177 | 'close2'=83 178 | 'next'=84 179 | 'script'=85 180 | 'declare'=86 181 | -------------------------------------------------------------------------------- /src/parser/RathenaScriptLangLexer.tokens: -------------------------------------------------------------------------------- 1 | T__0=1 2 | T__1=2 3 | T__2=3 4 | T__3=4 5 | T__4=5 6 | T__5=6 7 | T__6=7 8 | T__7=8 9 | T__8=9 10 | LeftParen=10 11 | RightParen=11 12 | LeftBrace=12 13 | RightBrace=13 14 | LeftBracket=14 15 | RightBracket=15 16 | Comma=16 17 | At=17 18 | Colon=18 19 | SemiColon=19 20 | Percent=20 21 | Star=21 22 | Tilde=22 23 | QuestionMark=23 24 | Quote=24 25 | DoubleQuote=25 26 | Underscore=26 27 | BitOr=27 28 | OrOp=28 29 | BitAnd=29 30 | AndOp=30 31 | Slash=31 32 | SlashStar=32 33 | StarSlash=33 34 | DoubleSlash=34 35 | Sharp=35 36 | DoubleSharp=36 37 | Minus=37 38 | DecrementOp=38 39 | Plus=39 40 | IncrementOp=40 41 | Dot=41 42 | DotAt=42 43 | Dollar=43 44 | DollarAt=44 45 | Bang=45 46 | BangEqual=46 47 | Equal=47 48 | DoubleEqual=48 49 | LeftCaret=49 50 | DoubleLeftCaret=50 51 | LeftCaretEqual=51 52 | RightCaret=52 53 | DoubleRightCaret=53 54 | RightCaretEqual=54 55 | PlusEqual=55 56 | MinusEqual=56 57 | MultiplyEqual=57 58 | DivideEqual=58 59 | PercentEqual=59 60 | If=60 61 | Else=61 62 | End=62 63 | Set=63 64 | For=64 65 | While=65 66 | Do=66 67 | Goto=67 68 | Return=68 69 | Switch=69 70 | Case=70 71 | Default=71 72 | Function=72 73 | Break=73 74 | Callfunc=74 75 | Callsub=75 76 | Eof=76 77 | Setarray=77 78 | Copyarray=78 79 | True=79 80 | False=80 81 | Menu=81 82 | Close=82 83 | Close2=83 84 | Next=84 85 | Script=85 86 | Declare=86 87 | Identifier=87 88 | Label=88 89 | String=89 90 | Number=90 91 | Whitespace=91 92 | Newline=92 93 | BlockComment=93 94 | LineComment=94 95 | '^'=1 96 | '<<='=2 97 | '>>='=3 98 | '&='=4 99 | '^='=5 100 | '|='=6 101 | 'continue'=7 102 | 'duplicate'=8 103 | 'shop'=9 104 | '('=10 105 | ')'=11 106 | '{'=12 107 | '}'=13 108 | '['=14 109 | ']'=15 110 | ','=16 111 | '@'=17 112 | ':'=18 113 | ';'=19 114 | '%'=20 115 | '*'=21 116 | '~'=22 117 | '?'=23 118 | '\''=24 119 | '"'=25 120 | '_'=26 121 | '|'=27 122 | '||'=28 123 | '&'=29 124 | '&&'=30 125 | '/'=31 126 | '/*'=32 127 | '*/'=33 128 | '//'=34 129 | '#'=35 130 | '##'=36 131 | '-'=37 132 | '--'=38 133 | '+'=39 134 | '++'=40 135 | '.'=41 136 | '.@'=42 137 | '$'=43 138 | '$@'=44 139 | '!'=45 140 | '!='=46 141 | '='=47 142 | '=='=48 143 | '<'=49 144 | '<<'=50 145 | '<='=51 146 | '>'=52 147 | '>>'=53 148 | '>='=54 149 | '+='=55 150 | '-='=56 151 | '*='=57 152 | '/='=58 153 | '%='=59 154 | 'if'=60 155 | 'else'=61 156 | 'end'=62 157 | 'set'=63 158 | 'for'=64 159 | 'while'=65 160 | 'do'=66 161 | 'goto'=67 162 | 'return'=68 163 | 'switch'=69 164 | 'case'=70 165 | 'default:'=71 166 | 'function'=72 167 | 'break'=73 168 | 'callfunc'=74 169 | 'callsub'=75 170 | 'eof'=76 171 | 'setarray'=77 172 | 'copyarray'=78 173 | 'true'=79 174 | 'false'=80 175 | 'menu'=81 176 | 'close'=82 177 | 'close2'=83 178 | 'next'=84 179 | 'script'=85 180 | 'declare'=86 181 | -------------------------------------------------------------------------------- /tests/stdlib_test.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::collections::HashMap; 3 | 4 | use std::sync::{Arc, Mutex}; 5 | use rathena_script_lang_interpreter::lang::compiler; 6 | 7 | 8 | use rathena_script_lang_interpreter::lang::vm::{DebugFlag, Vm}; 9 | use crate::common::{compile_script, Event, VmHook}; 10 | 11 | mod common; 12 | 13 | #[test] 14 | fn pow() { 15 | // Given 16 | let events = Arc::new(Mutex::new(HashMap::::new())); 17 | let classes = compile_script(r#" 18 | .@two = 2; 19 | vm_dump_var("nine", pow(3, .@two)); 20 | "#, compiler::DebugFlag::None.value()).unwrap(); 21 | let events_clone = events.clone(); 22 | let vm = crate::common::setup_vm(DebugFlag::None.value()); 23 | // When 24 | let vm_hook = VmHook::new( Box::new(move |e| { events_clone.lock().unwrap().insert(e.name.clone(), e); })); 25 | Vm::bootstrap(vm.clone(), classes, Box::new(&vm_hook)); 26 | Vm::execute_main_script(vm, Box::new(&vm_hook), vec![]).unwrap(); 27 | // Then 28 | assert_eq!(9, events.lock().unwrap().get("nine").unwrap().value.number_value().unwrap()); 29 | } 30 | #[test] 31 | fn pow_with_wrong_type() { 32 | // Given 33 | let events = Arc::new(Mutex::new(HashMap::::new())); 34 | let classes = compile_script(r#" 35 | .@two = 2; 36 | vm_dump_var("nine", pow("3", .@two)); 37 | "#, compiler::DebugFlag::None.value()).unwrap(); 38 | let events_clone = events; 39 | let vm = crate::common::setup_vm(DebugFlag::None.value()); 40 | // When 41 | let vm_hook = VmHook::new( Box::new(move |e| { events_clone.lock().unwrap().insert(e.name.clone(), e); })); 42 | Vm::bootstrap(vm.clone(), classes, Box::new(&vm_hook)); 43 | let runtime_error = Vm::execute_main_script(vm, Box::new(&vm_hook), vec![]).err().unwrap(); 44 | // Then 45 | assert_eq!(r#"Pow first argument should be a number. Value is string not a number. 46 | _MainScript 4:24. 47 | l4 vm_dump_var("nine", pow("3", .@two)); 48 | ^^^^^^^^^^^^^^^ 49 | 50 | 0: _main 51 | at _MainScript(_MainScript:4)"#, runtime_error.to_string().trim()); 52 | } 53 | 54 | #[test] 55 | fn rand() { 56 | // Given 57 | let events = Arc::new(Mutex::new(HashMap::::new())); 58 | let classes = compile_script(r#" 59 | vm_dump_var("randomv", rand(1, 9)); 60 | "#, compiler::DebugFlag::None.value()).unwrap(); 61 | let events_clone = events.clone(); 62 | let vm = crate::common::setup_vm(DebugFlag::None.value()); 63 | // When 64 | let vm_hook = VmHook::new( Box::new(move |e| { events_clone.lock().unwrap().insert(e.name.clone(), e); })); 65 | Vm::bootstrap(vm.clone(), classes, Box::new(&vm_hook)); 66 | Vm::execute_main_script(vm, Box::new(&vm_hook), vec![]).unwrap(); 67 | // Then 68 | assert_eq!(true, events.lock().unwrap().get("randomv").unwrap().value.number_value().is_ok()); 69 | } -------------------------------------------------------------------------------- /src/lang/stack.rs: -------------------------------------------------------------------------------- 1 | use std::cell::{Ref, RefCell}; 2 | use std::fmt::{Display, Formatter}; 3 | use crate::lang::error::{TemporaryRuntimeError}; 4 | use crate::lang::value::Scope; 5 | 6 | #[derive(Debug, Clone)] 7 | #[allow(dead_code)] 8 | pub enum StackEntry { 9 | ConstantPoolReference(u64), 10 | LocalVariableReference(u64), 11 | NativeReference(u64), 12 | FunctionReference(u64), 13 | InstanceVariableReference(u64), 14 | StaticVariableReference(u64), 15 | VariableReference((Scope, u64, u64)), // Scope, owner reference, entry reference 16 | HeapReference((u64, u64)), // owner reference, entry reference 17 | ArrayHeapReference((u64, u64, usize)), // owner reference, entry reference, index 18 | } 19 | 20 | #[derive(Debug)] 21 | pub struct Stack { 22 | values: RefCell>, 23 | } 24 | 25 | impl Stack { 26 | pub fn new(capacity: usize) -> Self { 27 | Self { 28 | values: RefCell::new(Vec::with_capacity(capacity)), 29 | } 30 | } 31 | 32 | pub fn push(&self, value: StackEntry) { 33 | self.values.borrow_mut().push(value) 34 | } 35 | 36 | pub fn pop(&self) -> Result { 37 | self.values.borrow_mut() 38 | .pop() 39 | .ok_or_else(|| TemporaryRuntimeError::new_string("Tried to pop an empty stack.".to_string())) 40 | } 41 | 42 | pub fn peek(&self, index: usize) -> Result { 43 | self.values.borrow() 44 | .get(index).cloned() 45 | .ok_or_else(|| TemporaryRuntimeError::new_string(format!("Tried to access index \"{}\" is out of bounds \"{}\"", index, self.len() - 1))) 46 | } 47 | 48 | pub fn peek_last(&self) -> Result { 49 | let index = self.len() - 1; 50 | self.values.borrow() 51 | .get(index).cloned() 52 | .ok_or_else(|| TemporaryRuntimeError::new_string(format!("Tried to access index \"{}\" is out of bounds \"{}\"", index, self.len() - 1))) 53 | } 54 | pub fn contents(&self) -> Ref> { 55 | self.values.borrow() 56 | } 57 | 58 | pub fn truncate(&self, index: usize) { 59 | self.values.borrow_mut().truncate(index); 60 | } 61 | 62 | pub fn len(&self) -> usize { 63 | self.values.borrow().len() 64 | } 65 | 66 | pub fn is_empty(&self) -> bool { 67 | self.values.borrow().is_empty() 68 | } 69 | } 70 | 71 | impl Display for Stack { 72 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 73 | if self.contents().is_empty() { 74 | f.write_str(" \n")?; 75 | } else { 76 | for (i, val) in self.contents().iter().enumerate() { 77 | f.write_str(&format!(" [{}] {:?}\n", i, val))?; 78 | } 79 | } 80 | Ok(()) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/lang/call_frame.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use std::collections::hash_map::Iter; 4 | use std::hash::{Hash, Hasher}; 5 | use crate::lang::noop_hasher::NoopHasher; 6 | use std::io::{Stdout, Write}; 7 | use std::sync::Arc; 8 | use std::time::SystemTime; 9 | use crate::lang::chunk::{OpCode}; 10 | use crate::lang::class::Function; 11 | use crate::lang::value::Variable; 12 | use crate::lang::vm::{DebugFlag, Hashcode, Vm}; 13 | 14 | #[derive(Debug)] 15 | pub struct CallFrame { 16 | debug_flag: u16, 17 | reference: Option, 18 | pub op_codes: Vec, 19 | pub stack_pointer: usize, 20 | pub arguments_count: usize, 21 | pub name: String, 22 | pub locals: HashMap, 23 | current_op_code: usize, 24 | } 25 | 26 | impl CallFrame { 27 | pub fn new(function: Function, stack_pointer: usize, arguments_count: usize, debug_flag: u16) -> Self { 28 | let mut call_frame = Self { 29 | debug_flag, 30 | reference: None, 31 | op_codes: function.code, 32 | stack_pointer, 33 | current_op_code: 0, 34 | name: function.name, 35 | locals: function.locals, 36 | arguments_count, 37 | }; 38 | call_frame.reference = Some(fastrand::u64(..)); 39 | call_frame 40 | } 41 | 42 | 43 | pub fn get_local(&self, reference: u64) -> Option<&Variable> { 44 | self.locals.get(&reference) 45 | } 46 | 47 | pub fn locals(&self) -> Iter<'_, u64, Variable> { 48 | self.locals.iter() 49 | } 50 | 51 | pub fn dump(&self, out: &mut Stdout, vm: Arc) { 52 | if self.debug_flag & DebugFlag::OpCode.value() == DebugFlag::OpCode.value() { 53 | writeln!(out, "========= Callframe({}) OpCode =========", self.name).unwrap(); 54 | for (index, op_code) in self.op_codes.iter().enumerate() { 55 | writeln!(out, "[{}] {:?}", index, op_code).unwrap(); 56 | } 57 | writeln!(out).unwrap(); 58 | } 59 | self.dump_locals(out, vm); 60 | } 61 | 62 | pub fn dump_locals(&self, out: &mut Stdout, vm: Arc) { 63 | if self.debug_flag & DebugFlag::LocalsVariable.value() == DebugFlag::LocalsVariable.value() { 64 | writeln!(out, "======== Callframe({}) Locals =========", self.name).unwrap(); 65 | for (reference, local) in self.locals.iter() { 66 | let value_ref = &local.value_ref; 67 | writeln!(out, "({}) name: {}, scope: {:?}, type: {}, value: {}", reference, 68 | local.name, local.scope, value_ref.value_type.display_type(), 69 | if let Some(value_reference) = value_ref.reference() { 70 | if vm.get_from_constant_pool(value_reference).is_some() { 71 | vm.get_from_constant_pool(value_reference).unwrap().value().display_value() 72 | } else { "".to_string() } 73 | } else {"".to_string()} 74 | ).unwrap(); 75 | } 76 | } 77 | } 78 | } 79 | 80 | impl Hash for CallFrame { 81 | fn hash(&self, state: &mut H) { 82 | self.name.hash(state); 83 | self.op_codes.hash(state); 84 | self.locals.len().hash(state); 85 | } 86 | } 87 | 88 | impl Hashcode for CallFrame { 89 | fn hash_code(&self) -> u64 { 90 | self.reference.unwrap() 91 | } 92 | } -------------------------------------------------------------------------------- /src/lang/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter}; 2 | use crate::lang::compiler::CompilationDetail; 3 | use crate::lang::stack_trace::StackTrace; 4 | 5 | 6 | #[derive(Debug)] 7 | #[allow(dead_code)] 8 | pub struct CompilationError { 9 | pub error_type: CompilationErrorType, 10 | pub message: String, 11 | pub details: CompilationDetail, 12 | } 13 | 14 | 15 | #[derive(Debug)] 16 | pub enum CompilationErrorType { 17 | Generic, 18 | UndefinedVariable, 19 | UndefinedFunction, 20 | FunctionAlreadyDefined, 21 | ClassAlreadyDefined, 22 | NativeAlreadyDefined, 23 | Type, 24 | LabelNotInMain, 25 | UndefinedLabel, 26 | NativeArgumentCount, 27 | } 28 | 29 | impl CompilationError { 30 | pub fn message(&self) -> String { 31 | format!("{}", self) 32 | } 33 | } 34 | 35 | impl Display for CompilationError { 36 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 37 | writeln!(f, "{}", self.message).unwrap(); 38 | write!(f, "{}", self.details) 39 | } 40 | } 41 | 42 | 43 | #[derive(Debug)] 44 | pub enum RuntimeErrorType { 45 | Internal, 46 | Execution, 47 | } 48 | 49 | #[derive(Debug)] 50 | pub struct RuntimeError { 51 | pub message: String, 52 | pub source: CompilationDetail, 53 | pub error_type: RuntimeErrorType, 54 | pub stack_traces: Vec, 55 | } 56 | 57 | #[derive(Debug, Clone)] 58 | pub struct TemporaryRuntimeError { 59 | pub message: String, 60 | } 61 | 62 | impl TemporaryRuntimeError { 63 | pub fn new(message: &str) -> Self { 64 | Self { 65 | message: message.to_string() 66 | } 67 | } 68 | pub fn new_string(message: String) -> Self { 69 | Self { 70 | message 71 | } 72 | } 73 | } 74 | 75 | impl RuntimeError { 76 | pub fn new(source: CompilationDetail, stack_traces: Vec, message: &str) -> Self { 77 | Self { 78 | source, 79 | message: message.to_string(), 80 | stack_traces, 81 | error_type: RuntimeErrorType::Execution, 82 | } 83 | } 84 | pub fn new_with_type(source: CompilationDetail, stack_traces: Vec, error_type: RuntimeErrorType, message: &str) -> Self { 85 | Self { 86 | source, 87 | message: message.to_string(), 88 | stack_traces, 89 | error_type, 90 | } 91 | } 92 | pub fn new_string(source: CompilationDetail, stack_traces: Vec, message: String) -> Self { 93 | Self { 94 | source, 95 | message, 96 | stack_traces, 97 | error_type: RuntimeErrorType::Execution, 98 | } 99 | } 100 | pub fn from_temporary(source: CompilationDetail, stack_traces: Vec, temporary: TemporaryRuntimeError) -> Self { 101 | Self { 102 | message: temporary.message, 103 | source, 104 | error_type: RuntimeErrorType::Execution, 105 | stack_traces, 106 | } 107 | } 108 | pub fn from_temporary_and_message(source: CompilationDetail, stack_traces: Vec, temporary: TemporaryRuntimeError, message: &str) -> Self { 109 | Self { 110 | message: format!("{}. {}", message, temporary.message), 111 | source, 112 | error_type: RuntimeErrorType::Execution, 113 | stack_traces, 114 | } 115 | } 116 | pub fn message(&self) -> String { 117 | format!("{}", self) 118 | } 119 | } 120 | 121 | impl Display for RuntimeError { 122 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 123 | writeln!(f, "{}", self.message).unwrap(); 124 | writeln!(f, "{}", self.source).unwrap(); 125 | for (i, trace) in self.stack_traces.iter().enumerate() { 126 | writeln!(f, "{}: {}", i, trace).unwrap(); 127 | } 128 | write!(f, "") 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /tests/native_test.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::collections::HashMap; 3 | 4 | use std::sync::{Arc, Mutex}; 5 | use std::sync::atomic::{AtomicI32}; 6 | use std::sync::atomic::Ordering::Relaxed; 7 | use rathena_script_lang_interpreter::lang::compiler; 8 | use rathena_script_lang_interpreter::lang::value::Value; 9 | 10 | 11 | use rathena_script_lang_interpreter::lang::vm::{DebugFlag, Vm}; 12 | use crate::common::{compile_script, Event, VmHook}; 13 | 14 | mod common; 15 | 16 | #[test] 17 | fn menu_test() { 18 | // Given 19 | let events = Arc::new(Mutex::new(HashMap::::new())); 20 | let script = compile_script(r#" 21 | menu "A", A, "B", B, "C", -, "D", D, "E:F", E; 22 | vm_dump_var("selected_menu", "C"); 23 | end; 24 | A: 25 | vm_dump_var("selected_menu", "A"); 26 | end; 27 | B: 28 | vm_dump_var("selected_menu", "B"); 29 | end; 30 | D: 31 | vm_dump_var("selected_menu", "D"); 32 | end; 33 | E: 34 | vm_dump_var("selected_menu", "E"); 35 | end; 36 | "#, compiler::DebugFlag::None.value()).unwrap(); 37 | let events_clone = events.clone(); 38 | let vm = crate::common::setup_vm(DebugFlag::None.value()); 39 | // When 40 | let i = AtomicI32::new(1); 41 | // each time we will call the script, "menu" will select next option, start with 1 then 2, etc... 42 | let vm_hook = VmHook::new_with_custom_handler( 43 | Box::new(move |e| { events_clone.lock().unwrap().insert(e.name.clone(), e); }), 44 | Box::new(move |native_name, thread| { 45 | if native_name.eq("menu") { 46 | let i1 = i.load(Relaxed); 47 | thread.push_constant_on_stack(Value::new_number(i1)); 48 | i.store(i1 + 1, Relaxed); 49 | return false; 50 | } 51 | true 52 | }), 53 | ); 54 | Vm::bootstrap(vm.clone(), script, Box::new(&vm_hook)); 55 | // Then 56 | Vm::execute_main_script(vm.clone(), Box::new(&vm_hook), vec![]).unwrap(); 57 | assert_eq!(String::from("A"), events.lock().unwrap().get("selected_menu").unwrap().value.string_value().unwrap().clone()); 58 | Vm::execute_main_script(vm.clone(), Box::new(&vm_hook), vec![]).unwrap(); 59 | assert_eq!(String::from("B"), events.lock().unwrap().get("selected_menu").unwrap().value.string_value().unwrap().clone()); 60 | Vm::execute_main_script(vm.clone(), Box::new(&vm_hook), vec![]).unwrap(); 61 | assert_eq!(String::from("C"), events.lock().unwrap().get("selected_menu").unwrap().value.string_value().unwrap().clone()); 62 | Vm::execute_main_script(vm.clone(), Box::new(&vm_hook), vec![]).unwrap(); 63 | assert_eq!(String::from("D"), events.lock().unwrap().get("selected_menu").unwrap().value.string_value().unwrap().clone()); 64 | Vm::execute_main_script(vm.clone(), Box::new(&vm_hook), vec![]).unwrap(); 65 | assert_eq!(String::from("E"), events.lock().unwrap().get("selected_menu").unwrap().value.string_value().unwrap().clone()); 66 | Vm::execute_main_script(vm, Box::new(&vm_hook), vec![]).unwrap(); 67 | assert_eq!(String::from("E"), events.lock().unwrap().get("selected_menu").unwrap().value.string_value().unwrap().clone()); 68 | } 69 | 70 | 71 | #[test] 72 | fn implode_test() { 73 | // Given 74 | let events = Arc::new(Mutex::new(HashMap::::new())); 75 | let script = compile_script(r#" 76 | setarray .@a$[1], "Hello", "World", "this", "is implode function"; 77 | vm_dump_var("a", implode(.@a$, " ")); 78 | "#, compiler::DebugFlag::None.value()).unwrap(); 79 | let events_clone = events.clone(); 80 | let vm = crate::common::setup_vm(DebugFlag::None.value()); 81 | // When 82 | let vm_hook = VmHook::new( Box::new(move |e| { events_clone.lock().unwrap().insert(e.name.clone(), e); })); 83 | Vm::bootstrap(vm.clone(), script, Box::new(&vm_hook)); 84 | Vm::execute_main_script(vm, Box::new(&vm_hook), vec![]).unwrap(); 85 | // Then 86 | assert_eq!(String::from("Hello World this is implode function"), events.lock().unwrap().get("a").unwrap().value.string_value().unwrap().clone()); 87 | } -------------------------------------------------------------------------------- /tests/operator_test.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use std::collections::HashMap; 4 | use std::sync::{Arc, Mutex}; 5 | use rathena_script_lang_interpreter::lang::compiler; 6 | use rathena_script_lang_interpreter::lang::vm::{DebugFlag, Vm}; 7 | use crate::common::{compile_script, Event, VmHook}; 8 | 9 | 10 | #[test] 11 | fn bit_and() { 12 | // Given 13 | let events = Arc::new(Mutex::new(HashMap::::new())); 14 | let script = compile_script(r#" 15 | .@a = 1 & 255; 16 | .@b = 16 & 128; 17 | .@c = 16 & 255; 18 | .@d = 16 & 255 & 128; 19 | vm_dump_var("a", .@a); 20 | vm_dump_var("b", .@b); 21 | vm_dump_var("c", .@c); 22 | vm_dump_var("d", .@d); 23 | "#, compiler::DebugFlag::None.value()).unwrap(); 24 | let events_clone = events.clone(); 25 | let vm = crate::common::setup_vm(DebugFlag::None.value()); 26 | // When 27 | let vm_hook = VmHook::new( Box::new(move |e| { events_clone.lock().unwrap().insert(e.name.clone(), e); })); 28 | Vm::bootstrap(vm.clone(), script, Box::new(&vm_hook)); 29 | Vm::execute_main_script(vm, Box::new(&vm_hook), vec![]).unwrap(); 30 | // Then 31 | assert_eq!(1, events.lock().unwrap().get("a").unwrap().value.number_value().unwrap().clone()); 32 | assert_eq!(0, events.lock().unwrap().get("b").unwrap().value.number_value().unwrap().clone()); 33 | assert_eq!(16, events.lock().unwrap().get("c").unwrap().value.number_value().unwrap().clone()); 34 | assert_eq!(0, events.lock().unwrap().get("d").unwrap().value.number_value().unwrap().clone()); 35 | } 36 | #[test] 37 | fn bit_or() { 38 | // Given 39 | let events = Arc::new(Mutex::new(HashMap::::new())); 40 | let script = compile_script(r#" 41 | .@a = 1 | 255; 42 | .@b = 16 | 128; 43 | .@c = 16 | 255; 44 | .@d = 16 | 32 | 128 ; 45 | vm_dump_var("a", .@a); 46 | vm_dump_var("b", .@b); 47 | vm_dump_var("c", .@c); 48 | vm_dump_var("d", .@d); 49 | "#, compiler::DebugFlag::None.value()).unwrap(); 50 | let events_clone = events.clone(); 51 | let vm = crate::common::setup_vm(DebugFlag::None.value()); 52 | // When 53 | let vm_hook = VmHook::new( Box::new(move |e| { events_clone.lock().unwrap().insert(e.name.clone(), e); })); 54 | Vm::bootstrap(vm.clone(), script, Box::new(&vm_hook)); 55 | Vm::execute_main_script(vm, Box::new(&vm_hook), vec![]).unwrap(); 56 | // Then 57 | assert_eq!(255, events.lock().unwrap().get("a").unwrap().value.number_value().unwrap().clone()); 58 | assert_eq!(144, events.lock().unwrap().get("b").unwrap().value.number_value().unwrap().clone()); 59 | assert_eq!(255, events.lock().unwrap().get("c").unwrap().value.number_value().unwrap().clone()); 60 | assert_eq!(176, events.lock().unwrap().get("d").unwrap().value.number_value().unwrap().clone()); 61 | } 62 | 63 | #[test] 64 | fn bitwise_not() { 65 | // Given 66 | let events = Arc::new(Mutex::new(HashMap::::new())); 67 | let script = compile_script(r#" 68 | .@a = ~2; 69 | .@b = ~16; 70 | .@c = ~769; 71 | vm_dump_var("a", .@a & 1); 72 | vm_dump_var("b", .@b & 16); 73 | vm_dump_var("c", .@b & 32); 74 | vm_dump_var("d", .@c & 32); 75 | vm_dump_var("e", .@c & 512); 76 | "#, compiler::DebugFlag::None.value()).unwrap(); 77 | let events_clone = events.clone(); 78 | let vm = crate::common::setup_vm(DebugFlag::None.value()); 79 | // When 80 | let vm_hook = VmHook::new( Box::new(move |e| { events_clone.lock().unwrap().insert(e.name.clone(), e); })); 81 | Vm::bootstrap(vm.clone(), script, Box::new(&vm_hook)); 82 | Vm::execute_main_script(vm, Box::new(&vm_hook), vec![]).unwrap(); 83 | // Then 84 | assert_eq!(1, events.lock().unwrap().get("a").unwrap().value.number_value().unwrap().clone()); 85 | assert_eq!(0, events.lock().unwrap().get("b").unwrap().value.number_value().unwrap().clone()); 86 | assert_eq!(32, events.lock().unwrap().get("c").unwrap().value.number_value().unwrap().clone()); 87 | assert_eq!(32, events.lock().unwrap().get("d").unwrap().value.number_value().unwrap().clone()); 88 | assert_eq!(0, events.lock().unwrap().get("e").unwrap().value.number_value().unwrap().clone()); 89 | } -------------------------------------------------------------------------------- /src/lang/array.rs: -------------------------------------------------------------------------------- 1 | use std::hash::{Hash, Hasher}; 2 | use std::sync::{Arc, RwLock}; 3 | use crate::lang::error::TemporaryRuntimeError; 4 | use crate::lang::value::{Scope, ValueType}; 5 | 6 | #[derive(Debug)] 7 | pub struct Array { 8 | pub(crate) reference: u64, 9 | pub(crate) values: RwLock>>, 10 | pub(crate) value_type: ValueType, 11 | pub(crate) name: String, 12 | pub(crate) scope: Scope, 13 | } 14 | 15 | impl Clone for Array { 16 | fn clone(&self) -> Self { 17 | Self { 18 | reference: self.reference, 19 | values: RwLock::new(self.values.read().unwrap().clone()), 20 | value_type: self.value_type.clone(), 21 | name: self.name.clone(), 22 | scope: self.scope.clone() 23 | } 24 | } 25 | } 26 | 27 | impl Array { 28 | pub fn new(reference: u64, value_type: ValueType, scope: Scope, name: String) -> Self { 29 | Self { 30 | reference, 31 | values: RwLock::new(vec![]), 32 | value_type, 33 | name, 34 | scope 35 | } 36 | } 37 | 38 | pub fn is_scope_global(&self) -> bool { 39 | self.scope.is_global() 40 | } 41 | 42 | pub fn value_type(&self) -> ValueType { 43 | self.value_type.clone() 44 | } 45 | 46 | pub fn assign(&self, index: usize, constant_pool_reference: Option, callback: Option) 47 | where F: Fn(Self) 48 | { 49 | let len = self.values.read().unwrap().len(); 50 | if index >= len { 51 | for _ in len..index + 1 { 52 | self.values.write().unwrap().push(None); 53 | } 54 | } 55 | self.values.write().unwrap()[index] = constant_pool_reference; 56 | if let Some(callback) = callback { 57 | callback(self.clone()); 58 | } 59 | } 60 | 61 | pub fn get(&self, index: usize) -> Result, TemporaryRuntimeError> { 62 | let len = self.values.read().unwrap().len(); 63 | if index >= len { 64 | return Err(TemporaryRuntimeError::new_string(format!("Array index out of bounds: index {}, length {}", index, len))); 65 | } 66 | Ok(*self.values.read().unwrap().get(index).unwrap()) 67 | } 68 | 69 | pub fn remove(&self, index: usize, count: usize, callback: Option) 70 | where F: Fn(Self, usize, usize) 71 | { 72 | let len = self.len(); 73 | let end = count.min(len); 74 | self.values.write().unwrap().drain(index..end); 75 | if let Some(callback) = callback { 76 | callback(self.clone(), index, end); 77 | } 78 | } 79 | 80 | pub fn index_of(&self, reference: u64) -> isize { 81 | self.values.read().unwrap().iter().position(|entry_ref| entry_ref.is_some() && entry_ref.unwrap() == reference) 82 | .map_or(-1, |index| index as isize) 83 | } 84 | 85 | pub fn copyarray(&self, source_array: Arc, destination_array_start_index: usize, source_array_index: usize, count: usize, callback: Option) -> Result<(), TemporaryRuntimeError> 86 | where F: Fn(Self) 87 | { 88 | let mut destination_array_index = destination_array_start_index; 89 | for index in source_array_index..(source_array_index + count) { 90 | let value = source_array.get(index)?; 91 | if let Some(value) = value { 92 | self.assign::(destination_array_index, Some(value), None); 93 | destination_array_index += 1; 94 | } else { 95 | break; 96 | } 97 | } 98 | if let Some(callback) = callback { 99 | callback(self.clone()); 100 | } 101 | Ok(()) 102 | } 103 | 104 | pub fn len(&self) -> usize { 105 | self.values.read().unwrap().len() 106 | } 107 | 108 | pub fn is_empty(&self) -> bool { 109 | self.values.read().unwrap().is_empty() 110 | } 111 | 112 | pub fn assign_multiple(&self, start_index: usize, size: usize, value_reference: u64, callback: Option) 113 | where F: Fn(Self) 114 | { 115 | for i in start_index..(start_index + size) { 116 | self.assign::(i, Some(value_reference), None); 117 | } 118 | if let Some(callback) = callback { 119 | callback(self.clone()); 120 | } 121 | } 122 | } 123 | 124 | 125 | impl Hash for Array { 126 | fn hash(&self, state: &mut H) { 127 | self.reference.hash(state); 128 | } 129 | } -------------------------------------------------------------------------------- /src/lang/class.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fmt::{Display, Formatter}; 3 | use std::hash::{Hash, Hasher}; 4 | use std::mem; 5 | 6 | use std::sync::{RwLock}; 7 | use std::time::SystemTime; 8 | use crate::lang::chunk::{Chunk, OpCode}; 9 | use crate::lang::compiler::CompilationDetail; 10 | use crate::lang::noop_hasher::NoopHasher; 11 | use crate::lang::value::{Variable}; 12 | use crate::lang::vm::{Hashcode, Vm}; 13 | 14 | #[derive(Debug)] 15 | pub struct Class { 16 | pub(crate) reference: u64, 17 | pub(crate) name: String, 18 | pub(crate) functions_pool: HashMap, 19 | pub(crate) sources: HashMap, NoopHasher>, // Key is function reference 20 | pub(crate) instances_references: RwLock, 21 | pub(crate) static_variables: RwLock>, 22 | pub(crate) instance_variables: HashMap, // Only instance variables definition 23 | } 24 | 25 | impl Class { 26 | pub fn new(name: String, reference: u64, functions_pool: HashMap, sources: HashMap, NoopHasher>, static_variables: HashMap, 27 | instance_variables: HashMap) -> Self { 28 | 29 | Self { 30 | reference, 31 | name, 32 | functions_pool, 33 | sources, 34 | instances_references: RwLock::new(0), 35 | static_variables: RwLock::new(static_variables), 36 | instance_variables 37 | } 38 | } 39 | 40 | pub fn new_instance(&self) -> Instance { 41 | *self.instances_references.write().unwrap() += 1; 42 | Instance { 43 | reference: Vm::calculate_hash(&self.instances_references.read().unwrap().clone()) & SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_nanos() as u64, 44 | class_name: self.name.clone(), 45 | variables: RwLock::new(self.instance_variables.clone()) 46 | } 47 | } 48 | pub fn get_variable(&self, reference: u64) -> Option { 49 | self.static_variables.read().unwrap().get(&reference).cloned() 50 | } 51 | pub fn insert_variable(&self, reference: u64, variable: Variable) { 52 | self.static_variables.write().unwrap().insert(reference, variable); 53 | } 54 | } 55 | 56 | impl Hash for Class { 57 | fn hash(&self, state: &mut H) { 58 | self.name.hash(state); 59 | self.sources.len().hash(state); 60 | self.functions_pool.len().hash(state); 61 | } 62 | } 63 | 64 | #[derive(Debug)] 65 | pub struct Instance { 66 | pub reference: u64, 67 | pub class_name: String, 68 | pub variables: RwLock>, 69 | } 70 | 71 | impl Hash for Instance { 72 | fn hash(&self, state: &mut H) { 73 | self.reference.hash(state) 74 | } 75 | } 76 | 77 | impl Instance { 78 | pub fn insert_variable(&self, reference: u64, variable: Variable) { 79 | let mut variable_guard = self.variables.write().unwrap(); 80 | variable_guard.insert(reference, variable); 81 | } 82 | } 83 | 84 | #[derive(Debug, Clone)] 85 | pub struct Function { 86 | pub name: String, 87 | pub code: Vec, 88 | pub locals: HashMap, 89 | } 90 | 91 | impl PartialEq for Function { 92 | fn eq(&self, other: &Self) -> bool { 93 | self.name == other.name 94 | } 95 | } 96 | 97 | impl Hash for Function { 98 | fn hash(&self, state: &mut H) { 99 | state.write(self.name.as_bytes()); 100 | } 101 | } 102 | 103 | impl Function { 104 | pub fn new(name: String) -> Self { 105 | Self { 106 | name, 107 | code: Default::default(), 108 | locals: Default::default() 109 | } 110 | } 111 | 112 | pub fn from_chunk(name: String, chunk: Chunk) -> Self { 113 | Self { 114 | name, 115 | code: mem::take(&mut chunk.op_codes.borrow_mut()), 116 | locals: mem::take(&mut chunk.locals.borrow_mut()) 117 | } 118 | } 119 | } 120 | 121 | impl Display for Function { 122 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 123 | write!(f, "function {}()", self.name) 124 | } 125 | } 126 | 127 | impl Hashcode for Class { 128 | fn hash_code(&self) -> u64 { 129 | self.reference 130 | } 131 | } 132 | impl Hashcode for Instance { 133 | fn hash_code(&self) -> u64 { 134 | self.reference 135 | } 136 | } -------------------------------------------------------------------------------- /tests/performance_test.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | extern crate test; 3 | 4 | use std::collections::HashMap; 5 | use std::fs::File; 6 | use std::io::{BufReader, Read}; 7 | use std::path::Path; 8 | use std::sync::{Arc, Mutex}; 9 | use std::time::Instant; 10 | use antlr_rust::common_token_stream::CommonTokenStream; 11 | use antlr_rust::InputStream; 12 | use rathena_script_lang_interpreter::lang::compiler; 13 | use rathena_script_lang_interpreter::lang::compiler::{Compiler}; 14 | use rathena_script_lang_interpreter::lang::vm::{DebugFlag, Vm}; 15 | use rathena_script_lang_interpreter::parser::rathenascriptlanglexer::RathenaScriptLangLexer; 16 | use rathena_script_lang_interpreter::parser::rathenascriptlangparser::RathenaScriptLangParser; 17 | use rathena_script_lang_interpreter::parser::rathenascriptlangvisitor::RathenaScriptLangVisitor; 18 | use crate::common::{compile_script, Event, VmHook}; 19 | 20 | 21 | mod common; 22 | #[test] 23 | fn parser_compiler_performance() { 24 | // Given 25 | let file = File::open(Path::new("tests/fixtures/warper.txt")).unwrap(); 26 | let mut reader = BufReader::new(file); 27 | let mut file_content = String::new(); 28 | reader.read_to_string(&mut file_content).unwrap(); 29 | // When 30 | let start_process = Instant::now(); 31 | let mut start = Instant::now(); 32 | let file_content_clone = file_content.clone(); 33 | let lexer = RathenaScriptLangLexer::new(InputStream::new(file_content_clone.as_str())); 34 | println!("Lexer tooks {}ms", start.elapsed().as_millis() as f32 ); 35 | start = Instant::now(); 36 | let token_stream = CommonTokenStream::new(lexer); 37 | let mut parser = RathenaScriptLangParser::new(token_stream); 38 | println!("Parser tooks {}ms", start.elapsed().as_millis() as f32 ); 39 | start = Instant::now(); 40 | let tree = parser.compilationUnit(); 41 | println!("Tree building tooks {}ms", start.elapsed().as_millis() as f32 ); 42 | start = Instant::now(); 43 | let mut compiler = Compiler::new("warper.txt".to_string(), file_content, "native_functions_list.txt", compiler::DebugFlag::None.value()); 44 | compiler.visit_compilationUnit(tree.as_ref().unwrap()); 45 | println!("Compilation took {}ms", start.elapsed().as_millis() as f32 ); 46 | // Then 47 | assert_eq!(true, 2 > start_process.elapsed().as_secs()); 48 | } 49 | 50 | 51 | #[cfg(test)] 52 | mod benchmark { 53 | use std::collections::HashMap; 54 | use std::sync::{Arc, Mutex}; 55 | use rathena_script_lang_interpreter::lang::compiler; 56 | use rathena_script_lang_interpreter::lang::value::Value; 57 | use rathena_script_lang_interpreter::lang::vm::{DebugFlag, Vm}; 58 | use crate::common::{compile_script, Event, GlobalVariableEntry, VmHook}; 59 | 60 | #[bench] 61 | fn profile_item_script(bencher: &mut test::Bencher) { 62 | let events = Arc::new(Mutex::new(HashMap::::new())); 63 | let classes = compile_script(r#" 64 | if (!isequipped(4172,4257,4230,4272)) 65 | bonus3 bAutoSpell,"RG_INTIMIDATE",1,20; 66 | if (BaseClass == 12) 67 | bonus bFlee,20; 68 | bonus2 bAddEffWhenHit,Eff_Stun,300+600*(readparam(bDex)>=77); 69 | bonus3 bAutoSpell,"RG_STRIPWEAPON",1,50; 70 | bonus2 bHPDrainRate,50,8; 71 | bonus2 bSPDrainRate,10,4; 72 | bonus2 bHPLossRate,10,5000; 73 | bonus bInt,1; 74 | if (BaseClass == 13) { 75 | bonus bInt,1; 76 | bonus bMdef,1; 77 | } 78 | bonus bFlee,3; 79 | bonus bHit,10; 80 | if (BaseClass == 13) { 81 | bonus2 bCriticalAddRace,RC_Undead,9; 82 | bonus2 bCriticalAddRace,RC_Demon,9; 83 | } 84 | "#, compiler::DebugFlag::None.value()).unwrap(); 85 | let events_clone = events.clone(); 86 | let vm = crate::common::setup_vm(DebugFlag::None.value()); 87 | // When 88 | let vm_hook = VmHook::new_with_behavior_on_missing_native( Box::new(move |e| { events_clone.lock().unwrap().insert(e.name.clone(), e); }), false); 89 | vm_hook.global_variable_store.lock().unwrap().push(GlobalVariableEntry { name: "BaseClass".to_string(), value: Value::Number(Some(13)), scope: "char_permanent".to_string(), index: None }); 90 | vm_hook.global_variable_store.lock().unwrap().push(GlobalVariableEntry { name: "Eff_Stun".to_string(), value: Value::Number(Some(13)), scope: "char_permanent".to_string(), index: None }); 91 | vm_hook.global_variable_store.lock().unwrap().push(GlobalVariableEntry { name: "RC_Undead".to_string(), value: Value::Number(Some(13)), scope: "char_permanent".to_string(), index: None }); 92 | vm_hook.global_variable_store.lock().unwrap().push(GlobalVariableEntry { name: "RC_Demon".to_string(), value: Value::Number(Some(13)), scope: "char_permanent".to_string(), index: None }); 93 | vm_hook.global_variable_store.lock().unwrap().push(GlobalVariableEntry { name: "bDex".to_string(), value: Value::Number(Some(13)), scope: "char_permanent".to_string(), index: None }); 94 | bencher.iter(|| { 95 | // Then 96 | Vm::repl(vm.clone(), &classes[1], Box::new(&vm_hook), vec![]).unwrap(); 97 | }); 98 | } 99 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![build](https://github.com/nmeylan/rathena-script-lang-interpreter/actions/workflows/rust.yml/badge.svg) 2 | 3 | **WIP** [Check remaining todos](https://github.com/nmeylan/rathena-script-lang-interpreter/issues/1) or [code samples](https://github.com/nmeylan/rathena-script-lang-interpreter/tree/master/tests) 4 | 5 | # rAthena script language interpreter 6 | A grammar, parser, compiler and VM implementation for rAthena script language. 7 | 8 | Inspired by [https://github.com/Doddler/Scripting-Language-Guide](https://github.com/Doddler/Scripting-Language-Guide) and [https://craftinginterpreters.com/](https://craftinginterpreters.com/). 9 | 10 | VM architecture inspired by [jvm](https://docs.oracle.com/javase/specs/jvms/se11/html/index.html) 11 | 12 | This crate offers a compiler and VM to be embedded in game server. 13 | 14 | # Generate Parser 15 | `java -jar bin/antlr4.9.4-rust.jar RathenaScriptLang.g4 -Dlanguage=Rust -visitor -o src/parser` 16 | 17 | # Grammar 18 | Grammar is defined in [RathenaScriptLang.g4](https://github.com/nmeylan/rathena-script-lang-interpreter/blob/master/RathenaScriptLang.g4) file. Parser is generated using `antlr`. 19 | 20 | # Integration with a server 21 | Currently only integretation for a rust server implementation is targeted. A C/C++ API can be provided to integrate it in [herculesWS](https://github.com/HerculesWS/Hercules) and [rathena](https://github.com/rathena/rathena) 22 | 23 | Vm should be run inside mmo server. NPC script can be pre-compiled or compiled at VM startup, using this crate. 24 | 25 | Rathena script command related to game, almost all commands listed [here](https://github.com/rathena/rathena/blob/master/doc/script_commands.txt#L1012), have to be implemented in server side. 26 | 27 | Compiler considers those command as "native" functions. VM require to implement a native functions handler, which has to be implemented by the server. 28 | 29 | [Example of integration](https://github.com/nmeylan/rust-ro/blob/094aa0c6641da6461ef905a17a8ca245bb0388cd/server/src/server/script/mod.rs#L178) 30 | 31 | # Features 32 | In addition of providing a compiler and vm to interpret rathena script lang, this implementation provides: 33 | - Nice compilation errors for easier code writing 34 | ``` 35 | Variable ".@c" is declared as a Number but is assigned with a String. 36 | test_script 12:4. 37 | l12 .@c = str(); 38 | ^^^ 39 | ``` 40 | - Nice runtime errors for easier debugging 41 | ``` 42 | Can't call getarg(1) which is greater than number of arguments provided: 1. Maximum index is 0. Consider calling getarg with a default value: getarg(1, DEFAULT_VALUE) 43 | test_script 5:15. 44 | l5 .@a$ = getarg(1) + " world"; 45 | ^^^^^^^^^ 46 | 47 | 0: _main 48 | at /home/ragnarok/dev/npc/warps.txt(Warper:3) 49 | 1: my_func 50 | at /home/ragnarok/dev/npc/warps.txt(Warper:5) 51 | ``` 52 | - AOT Compilation for faster server startup 53 | - AOT Compilation type checking for variable assignment 54 | - AOT Compilation argument count checking for native functions 55 | 56 | # Server implementation notes 57 | Server should implement natives functions (commands starting after [point 2](https://github.com/rathena/rathena/blob/master/doc/script_commands.txt#L2344)) in order to make scripts working. 58 | 59 | In addition of native functions listed in link above, server should implement some extra native functions, for example to support **char**, **account** and **server** variable scopes. 60 | 61 | [more details here]() 62 | 63 | # Limitation and workaround 64 | 65 | ## Lack of reference/value consistency in the language 66 | Some methods return reference or value dependending on the context, forcing to implement some "hack" to support all use case. 67 | 68 | Here an example with `getelementofarray` but there are plenty of example 69 | ``` 70 | .@a$[0] = "hello"; // This is a valid array of string declaration 71 | 72 | getelementofarray(.@a$, 0) + " world"; // getelementofarray() returns a value 73 | setarray getelementofarray(.@a$, 0), "bye"; // getelementofarray() returns a reference so we can assign "bye" to array element 0 74 | ``` 75 | 76 | ## Global variable array 77 | - `getglobalarrayref`: put reference of a global variable array on the stack 78 | 79 | # Architecture 80 | ## Language specificity 81 | Althought Rathena script lang is not an oriented object programming language, it has notion of: 82 | - Global script(or npc) state, which are alive while server is alive 83 | - Global script(or npc) state, which are alive for the duration of the script execution 84 | 85 | To ease the implementation of the VM we will call: 86 | - Script/NPC -> Class 87 | - Script/NPC instance -> Instance 88 | 89 | ## VM Memory layout 90 | ```mermaid 91 | flowchart LR 92 | subgraph S1[VM] 93 | direction TB 94 | A1["ConstantPool
  • String (#quot;hello world#quot;)
  • Number(1)
"] 95 | A2[NativeMethodPool
  • print
  • callfunc
  • ...
] 96 | A3[Heap
  • Arrays
  • Instances
] 97 | A4[ClassPool
Classes] 98 | A5[InstancePool*
Instances] 99 | A3 --> A1 100 | A3 --> A5 101 | end 102 | 103 | subgraph S3[Callframe] 104 | direction TB 105 | C1[Operand Stack] 106 | C2["Local variable pool
  • String pointer
  • Number pointer
"] 107 | C2 --> A1 108 | end 109 | 110 | subgraph S2[Thread] 111 | direction TB 112 | B1[Stack
  • References
] 113 | B1 --> A1 114 | B1 --> A2 115 | B1 --> A3 116 | B1 --> C2 117 | end 118 | ``` 119 | ## VM lifecycle 120 | ```mermaid 121 | graph TD 122 | Server[RagnarokServer] --> Boot --> Load[Load bytecode] --> VM; 123 | Compiler --> ReadTxt[Parse source] --> AST[Generate AST] --> Visit[Visit ast nodes] --> Chunk[Generate chunks] --> ByteCode[Write bytes code] --> Bytecode; 124 | VM[VM] --> Startup; 125 | Server --> Player[Player interact with NPC] --> Execute; 126 | Execute[Execute script] --> VM[VM] --> Instantiate[Instantiate script] --> Read2[Read byte code] --> Run[Execute program] --> I2[Execute op code]; 127 | Startup[Bootstrap] --> Loading --> Linking --> Initialization 128 | 129 | 130 | ``` 131 | 132 | ## Garbage collection (design in progress) 133 | All "data" pool store references to constant pool. Only constant pool store values. 134 | 135 | TBD 136 | -------------------------------------------------------------------------------- /tests/small_script_benchmark.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::{Arc, Mutex}; 3 | use criterion::{Criterion, criterion_group, criterion_main}; 4 | use rathena_script_lang_interpreter::lang::compiler; 5 | use rathena_script_lang_interpreter::lang::value::Value; 6 | use rathena_script_lang_interpreter::lang::vm::{DebugFlag, Vm}; 7 | use crate::common::{compile_script, Event, GlobalVariableEntry, VmHook}; 8 | 9 | mod common; 10 | 11 | pub fn criterion_benchmark(c: &mut Criterion) { 12 | let vm = crate::common::setup_vm(DebugFlag::None.value()); 13 | let events = Arc::new(Mutex::new(HashMap::::new())); 14 | let vm_hook = VmHook::new_with_behavior_on_missing_native( Box::new(move |e| { events.lock().unwrap().insert(e.name.clone(), e); }), false); 15 | vm_hook.global_variable_store.lock().unwrap().push(GlobalVariableEntry { name: "BaseClass".to_string(), value: Value::Number(Some(13)), scope: "char_permanent".to_string(), index: None }); 16 | vm_hook.global_variable_store.lock().unwrap().push(GlobalVariableEntry { name: "Eff_Stun".to_string(), value: Value::Number(Some(13)), scope: "char_permanent".to_string(), index: None }); 17 | vm_hook.global_variable_store.lock().unwrap().push(GlobalVariableEntry { name: "RC_Undead".to_string(), value: Value::Number(Some(13)), scope: "char_permanent".to_string(), index: None }); 18 | vm_hook.global_variable_store.lock().unwrap().push(GlobalVariableEntry { name: "RC_Demon".to_string(), value: Value::Number(Some(13)), scope: "char_permanent".to_string(), index: None }); 19 | vm_hook.global_variable_store.lock().unwrap().push(GlobalVariableEntry { name: "bDex".to_string(), value: Value::Number(Some(13)), scope: "char_permanent".to_string(), index: None }); 20 | 21 | let classes = compile_script(r#" 22 | if (!isequipped(4172,4257,4230,4272)) 23 | bonus3 bAutoSpell,"RG_INTIMIDATE",1,20; 24 | if (BaseClass == 12) 25 | bonus bFlee,20; 26 | bonus2 bAddEffWhenHit,Eff_Stun,300+600*(readparam(bDex)>=77); 27 | bonus3 bAutoSpell,"RG_STRIPWEAPON",1,50; 28 | bonus2 bHPDrainRate,50,8; 29 | bonus2 bSPDrainRate,10,4; 30 | bonus2 bHPLossRate,10,5000; 31 | bonus bInt,1; 32 | if (BaseClass == 13) { 33 | bonus bInt,1; 34 | bonus bMdef,1; 35 | } 36 | bonus bFlee,3; 37 | bonus bHit,10; 38 | if (BaseClass == 13) { 39 | bonus2 bCriticalAddRace,RC_Undead,9; 40 | bonus2 bCriticalAddRace,RC_Demon,9; 41 | } 42 | "#, compiler::DebugFlag::None.value()).unwrap(); 43 | Vm::bootstrap(vm.clone(), classes, Box::new(&vm_hook)); 44 | c.bench_function("small_script_execute_main_script", |b| b.iter(|| { 45 | Vm::execute_main_script(vm.clone(), Box::new(&vm_hook), vec![]).unwrap(); 46 | })); 47 | 48 | 49 | let vm = crate::common::setup_vm(DebugFlag::None.value()); 50 | let events = Arc::new(Mutex::new(HashMap::::new())); 51 | let vm_hook = VmHook::new_with_behavior_on_missing_native( Box::new(move |e| { events.lock().unwrap().insert(e.name.clone(), e); }), false); 52 | vm_hook.global_variable_store.lock().unwrap().push(GlobalVariableEntry { name: "BaseClass".to_string(), value: Value::Number(Some(13)), scope: "char_permanent".to_string(), index: None }); 53 | vm_hook.global_variable_store.lock().unwrap().push(GlobalVariableEntry { name: "Eff_Stun".to_string(), value: Value::Number(Some(13)), scope: "char_permanent".to_string(), index: None }); 54 | vm_hook.global_variable_store.lock().unwrap().push(GlobalVariableEntry { name: "RC_Undead".to_string(), value: Value::Number(Some(13)), scope: "char_permanent".to_string(), index: None }); 55 | vm_hook.global_variable_store.lock().unwrap().push(GlobalVariableEntry { name: "RC_Demon".to_string(), value: Value::Number(Some(13)), scope: "char_permanent".to_string(), index: None }); 56 | vm_hook.global_variable_store.lock().unwrap().push(GlobalVariableEntry { name: "bDex".to_string(), value: Value::Number(Some(13)), scope: "char_permanent".to_string(), index: None }); 57 | 58 | let classes = compile_script(r#" 59 | if (!isequipped(4172,4257,4230,4272)) 60 | bonus3 bAutoSpell,"RG_INTIMIDATE",1,20; 61 | if (BaseClass == 12) 62 | bonus bFlee,20; 63 | bonus2 bAddEffWhenHit,Eff_Stun,300+600*(readparam(bDex)>=77); 64 | bonus3 bAutoSpell,"RG_STRIPWEAPON",1,50; 65 | bonus2 bHPDrainRate,50,8; 66 | bonus2 bSPDrainRate,10,4; 67 | bonus2 bHPLossRate,10,5000; 68 | bonus bInt,1; 69 | if (BaseClass == 13) { 70 | bonus bInt,1; 71 | bonus bMdef,1; 72 | } 73 | bonus bFlee,3; 74 | bonus bHit,10; 75 | if (BaseClass == 13) { 76 | bonus2 bCriticalAddRace,RC_Undead,9; 77 | bonus2 bCriticalAddRace,RC_Demon,9; 78 | } 79 | "#, compiler::DebugFlag::None.value()).unwrap(); 80 | c.bench_function("repl", |b| b.iter(|| { 81 | Vm::repl(vm.clone(), &classes[1], Box::new(&vm_hook), vec![]).unwrap(); 82 | })); 83 | 84 | let vm = crate::common::setup_vm(DebugFlag::None.value()); 85 | let events = Arc::new(Mutex::new(HashMap::::new())); 86 | let vm_hook = VmHook::new_with_behavior_on_missing_native( Box::new(move |e| { events.lock().unwrap().insert(e.name.clone(), e); }), false); 87 | vm_hook.global_variable_store.lock().unwrap().push(GlobalVariableEntry { name: "BaseClass".to_string(), value: Value::Number(Some(13)), scope: "char_permanent".to_string(), index: None }); 88 | vm_hook.global_variable_store.lock().unwrap().push(GlobalVariableEntry { name: "Eff_Stun".to_string(), value: Value::Number(Some(13)), scope: "char_permanent".to_string(), index: None }); 89 | vm_hook.global_variable_store.lock().unwrap().push(GlobalVariableEntry { name: "RC_Undead".to_string(), value: Value::Number(Some(13)), scope: "char_permanent".to_string(), index: None }); 90 | vm_hook.global_variable_store.lock().unwrap().push(GlobalVariableEntry { name: "RC_Demon".to_string(), value: Value::Number(Some(13)), scope: "char_permanent".to_string(), index: None }); 91 | vm_hook.global_variable_store.lock().unwrap().push(GlobalVariableEntry { name: "bDex".to_string(), value: Value::Number(Some(13)), scope: "char_permanent".to_string(), index: None }); 92 | 93 | let classes = compile_script(r#" 94 | if (!isequipped(4172,4257,4230,4272)) 95 | bonus3 bAutoSpell,"RG_INTIMIDATE",1,20; 96 | if (BaseClass == 12) 97 | bonus bFlee,20; 98 | bonus2 bAddEffWhenHit,Eff_Stun,300+600*(readparam(bDex)>=77); 99 | bonus3 bAutoSpell,"RG_STRIPWEAPON",1,50; 100 | bonus2 bHPDrainRate,50,8; 101 | bonus2 bSPDrainRate,10,4; 102 | bonus2 bHPLossRate,10,5000; 103 | bonus bInt,1; 104 | if (BaseClass == 13) { 105 | bonus bInt,1; 106 | bonus bMdef,1; 107 | } 108 | bonus bFlee,3; 109 | bonus bHit,10; 110 | if (BaseClass == 13) { 111 | bonus2 bCriticalAddRace,RC_Undead,9; 112 | bonus2 bCriticalAddRace,RC_Demon,9; 113 | } 114 | "#, compiler::DebugFlag::None.value()).unwrap(); 115 | 116 | Vm::bootstrap_without_init(vm.clone(), classes); 117 | c.bench_function("repl with cache", |b| b.iter(|| { 118 | Vm::repl_on_registered_class(vm.clone(), "_MainScript", Box::new(&vm_hook), vec![]).unwrap(); 119 | })); 120 | } 121 | 122 | criterion_group!(benches, criterion_benchmark); 123 | criterion_main!(benches); -------------------------------------------------------------------------------- /src/util/scripts_compiler.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::default::Default; 3 | use std::fs::File; 4 | use std::io::{BufReader, Read}; 5 | use std::mem; 6 | use std::path::Path; 7 | 8 | use antlr_rust::common_token_stream::CommonTokenStream; 9 | use antlr_rust::InputStream; 10 | use antlr_rust::parser_rule_context::ParserRuleContext; 11 | use antlr_rust::token::Token; 12 | use antlr_rust::tree::{ParseTree, ParseTreeVisitor, Tree}; 13 | 14 | use crate::lang::value::Value; 15 | use crate::lang::chunk::ClassFile; 16 | use crate::lang::compiler::{Compiler, parse_number}; 17 | use crate::lang::error::CompilationError; 18 | use crate::parser::rathenascriptlanglexer::RathenaScriptLangLexer; 19 | use crate::parser::rathenascriptlangparser::{NpcInitializationContext, NpcInitializationContextAttrs, NpcShopDiscountContextAttrs, NpcShopItemContextAttrs, NpcShopPriceContextAttrs, RathenaScriptLangParser, RathenaScriptLangParserContextType, ScriptDirContextAttrs, ScriptInitializationContext, ScriptInitializationContextAttrs, ScriptSpriteContextAttrs, ScriptXPosContextAttrs, ScriptYPosContextAttrs}; 20 | use crate::parser::rathenascriptlangvisitor::RathenaScriptLangVisitor; 21 | 22 | pub struct Script { 23 | pub name: String, 24 | pub x_pos: usize, 25 | pub y_pos: usize, 26 | pub dir: usize, 27 | pub map: String, 28 | pub sprite: String, 29 | pub x_size: usize, 30 | pub y_size: usize, 31 | pub class_name: String, 32 | pub constructor_args: Vec, 33 | pub class_reference: u64, 34 | } 35 | 36 | pub struct ScriptVisitor { 37 | scripts: Vec