duckscript/
runner.rs

1//! # runner
2//!
3//! The main entry point which enables running scripts.
4//!
5
6#[cfg(test)]
7#[path = "./runner_test.rs"]
8mod runner_test;
9
10use crate::expansion::{self, ExpandedValue};
11use crate::parser;
12use crate::types::command::{CommandInvocationContext, CommandResult, Commands, GoToValue};
13use crate::types::env::Env;
14use crate::types::error::ScriptError;
15use crate::types::instruction::{
16    Instruction, InstructionMetaInfo, InstructionType, ScriptInstruction,
17};
18use crate::types::runtime::{Context, Runtime, StateValue};
19use std::collections::HashMap;
20use std::io::stdin;
21use std::sync::atomic::Ordering;
22
23#[derive(Debug)]
24enum EndReason {
25    ExitCalled,
26    ReachedEnd,
27    Crash(ScriptError),
28    Halted,
29}
30
31/// Executes the provided script with the given context
32pub fn run_script(text: &str, context: Context, env: Option<Env>) -> Result<Context, ScriptError> {
33    match parser::parse_text(text) {
34        Ok(instructions) => run(instructions, context, env),
35        Err(error) => Err(error),
36    }
37}
38
39/// Executes the provided script file with the given context
40pub fn run_script_file(
41    file: &str,
42    context: Context,
43    env: Option<Env>,
44) -> Result<Context, ScriptError> {
45    match parser::parse_file(file) {
46        Ok(instructions) => run(instructions, context, env),
47        Err(error) => Err(error),
48    }
49}
50
51/// Provides the REPL entry point
52pub fn repl(mut context: Context) -> Result<Context, ScriptError> {
53    let mut text = String::new();
54    let mut instructions = vec![];
55
56    loop {
57        text.clear();
58
59        match stdin().read_line(&mut text) {
60            Ok(_) => {
61                match parser::parse_text(&text) {
62                    Ok(mut new_instructions) => {
63                        // get start line
64                        let start = instructions.len();
65
66                        // add new instructions
67                        instructions.append(&mut new_instructions);
68                        let runtime = create_runtime(instructions.clone(), context, None);
69
70                        let (updated_context, end_reason) = run_instructions(runtime, start, true)?;
71
72                        context = updated_context;
73
74                        match end_reason {
75                            EndReason::ExitCalled => return Ok(context),
76                            EndReason::Crash(error) => println!("{}", &error.to_string()),
77                            _ => (),
78                        };
79                    }
80                    Err(error) => return Err(error),
81                }
82            }
83            Err(error) => {
84                return Err(ScriptError::Runtime(
85                    error.to_string(),
86                    Some(InstructionMetaInfo::new()),
87                ));
88            }
89        };
90    }
91}
92
93fn run(
94    instructions: Vec<Instruction>,
95    context: Context,
96    env: Option<Env>,
97) -> Result<Context, ScriptError> {
98    let runtime = create_runtime(instructions, context, env);
99
100    match run_instructions(runtime, 0, false) {
101        Ok((context, _)) => Ok(context),
102        Err(error) => Err(error),
103    }
104}
105
106fn create_runtime(instructions: Vec<Instruction>, context: Context, env: Option<Env>) -> Runtime {
107    let mut runtime = Runtime::new(context, env);
108
109    let mut line = 0;
110    for instruction in &instructions {
111        if let InstructionType::Script(ref value) = &instruction.instruction_type {
112            if let Some(ref label) = value.label {
113                runtime.label_to_line.insert(label.to_string(), line);
114            };
115        };
116
117        line += 1;
118    }
119
120    runtime.instructions = Some(instructions);
121
122    runtime
123}
124
125fn run_instructions(
126    mut runtime: Runtime,
127    start_at: usize,
128    repl_mode: bool,
129) -> Result<(Context, EndReason), ScriptError> {
130    let mut line = start_at;
131    let mut state = runtime.context.state.clone();
132
133    let instructions = match runtime.instructions {
134        Some(ref instructions) => instructions,
135        None => return Ok((runtime.context, EndReason::ReachedEnd)),
136    };
137
138    let mut end_reason = EndReason::ReachedEnd;
139    loop {
140        if runtime.env.halt.load(Ordering::SeqCst) {
141            end_reason = EndReason::Halted;
142            break;
143        }
144
145        let (instruction, meta_info) = if instructions.len() > line {
146            let instruction = instructions[line].clone();
147            let meta_info = instruction.meta_info.clone();
148            (instruction, meta_info)
149        } else {
150            break;
151        };
152
153        let (command_result, output_variable) = run_instruction(
154            &mut runtime.context.commands,
155            &mut runtime.context.variables,
156            &mut state,
157            instructions,
158            instruction,
159            line,
160            &mut runtime.env,
161        );
162
163        match command_result {
164            CommandResult::Exit(output) => {
165                update_output(
166                    &mut runtime.context.variables,
167                    output_variable,
168                    output.clone(),
169                );
170                end_reason = EndReason::ExitCalled;
171
172                if repl_mode {
173                    return Ok((runtime.context, end_reason));
174                }
175
176                if let Some(exit_code_str) = output {
177                    if let Ok(exit_code) = exit_code_str.parse::<i32>() {
178                        if exit_code != 0 {
179                            return Err(ScriptError::Runtime(
180                                format!("Exit with error code: {}", exit_code).to_string(),
181                                Some(meta_info.clone()),
182                            ));
183                        }
184                    }
185                }
186
187                break;
188            }
189            CommandResult::Error(error) => {
190                update_output(
191                    &mut runtime.context.variables,
192                    output_variable,
193                    Some("false".to_string()),
194                );
195
196                let post_error_line = line + 1;
197
198                if let Err(error) = run_on_error_instruction(
199                    &mut runtime.context.commands,
200                    &mut runtime.context.variables,
201                    &mut state,
202                    instructions,
203                    error,
204                    meta_info.clone(),
205                    &mut runtime.env,
206                ) {
207                    return Err(ScriptError::Runtime(error, Some(meta_info.clone())));
208                };
209
210                line = post_error_line;
211            }
212            CommandResult::Crash(error) => {
213                let script_error = ScriptError::Runtime(error, Some(meta_info));
214
215                if repl_mode {
216                    return Ok((runtime.context, EndReason::Crash(script_error)));
217                }
218
219                return Err(script_error);
220            }
221            CommandResult::Continue(output) => {
222                update_output(&mut runtime.context.variables, output_variable, output);
223
224                line += 1;
225            }
226            CommandResult::GoTo(output, goto_value) => {
227                update_output(&mut runtime.context.variables, output_variable, output);
228
229                match goto_value {
230                    GoToValue::Label(label) => match runtime.label_to_line.get(&label) {
231                        Some(value) => line = *value,
232                        None => {
233                            return Err(ScriptError::Runtime(
234                                format!("Label: {} not found.", label),
235                                Some(meta_info),
236                            ));
237                        }
238                    },
239                    GoToValue::Line(line_number) => line = line_number,
240                }
241            }
242        };
243    }
244
245    runtime.context.state = state;
246
247    Ok((runtime.context, end_reason))
248}
249
250fn update_output(
251    variables: &mut HashMap<String, String>,
252    output_variable: Option<String>,
253    output: Option<String>,
254) {
255    if output_variable.is_some() {
256        match output {
257            Some(value) => variables.insert(output_variable.unwrap(), value),
258            None => variables.remove(&output_variable.unwrap()),
259        };
260    }
261}
262
263fn run_on_error_instruction(
264    commands: &mut Commands,
265    variables: &mut HashMap<String, String>,
266    state: &mut HashMap<String, StateValue>,
267    instructions: &Vec<Instruction>,
268    error: String,
269    meta_info: InstructionMetaInfo,
270    env: &mut Env,
271) -> Result<(), String> {
272    if commands.exists("on_error") {
273        let mut script_instruction = ScriptInstruction::new();
274        script_instruction.command = Some("on_error".to_string());
275        script_instruction.arguments = Some(vec![
276            error,
277            meta_info.line.unwrap_or(0).to_string(),
278            meta_info.source.unwrap_or("".to_string()),
279        ]);
280        let instruction = Instruction {
281            meta_info: InstructionMetaInfo::new(),
282            instruction_type: InstructionType::Script(script_instruction),
283        };
284
285        let (command_result, output_variable) = run_instruction(
286            commands,
287            variables,
288            state,
289            instructions,
290            instruction,
291            0,
292            env,
293        );
294
295        match command_result {
296            CommandResult::Exit(output) => {
297                update_output(variables, output_variable, output);
298
299                Err("Exiting Script.".to_string())
300            }
301            CommandResult::Crash(error) => Err(error),
302            _ => Ok(()),
303        }
304    } else {
305        Ok(())
306    }
307}
308
309/// Enables to evaluate a single instruction and return its result.
310pub fn run_instruction(
311    commands: &mut Commands,
312    variables: &mut HashMap<String, String>,
313    state: &mut HashMap<String, StateValue>,
314    instructions: &Vec<Instruction>,
315    instruction: Instruction,
316    line: usize,
317    env: &mut Env,
318) -> (CommandResult, Option<String>) {
319    let mut output_variable = None;
320    let command_result = match instruction.instruction_type {
321        InstructionType::Empty => CommandResult::Continue(None),
322        InstructionType::PreProcess(_) => CommandResult::Continue(None),
323        InstructionType::Script(ref script_instruction) => {
324            output_variable = script_instruction.output.clone();
325
326            match script_instruction.command {
327                Some(ref command) => match commands.get_for_use(command) {
328                    Some(command_instance) => {
329                        let command_arguments = bind_command_arguments(
330                            variables,
331                            script_instruction,
332                            &instruction.meta_info,
333                        );
334
335                        let command_args = CommandInvocationContext {
336                            arguments: command_arguments,
337                            state,
338                            variables,
339                            output_variable: output_variable.clone(),
340                            instructions,
341                            commands,
342                            line,
343                            env,
344                        };
345                        command_instance.run(command_args)
346                    }
347                    None => CommandResult::Crash(format!("Command: {} not found.", &command)),
348                },
349                None => CommandResult::Continue(None),
350            }
351        }
352    };
353
354    (command_result, output_variable)
355}
356
357fn bind_command_arguments(
358    variables: &HashMap<String, String>,
359    instruction: &ScriptInstruction,
360    meta_info: &InstructionMetaInfo,
361) -> Vec<String> {
362    let mut arguments = vec![];
363
364    match instruction.arguments {
365        Some(ref arguments_ref) => {
366            for argument in arguments_ref {
367                match expansion::expand_by_wrapper(&argument, meta_info, variables) {
368                    ExpandedValue::Single(value) => arguments.push(value),
369                    ExpandedValue::Multi(values) => {
370                        for value in values {
371                            arguments.push(value)
372                        }
373                    }
374                    ExpandedValue::None => arguments.push("".to_string()),
375                }
376            }
377        }
378        None => (),
379    };
380
381    arguments
382}