script/python/ffi_types/copr/
compile.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! compile script to code object
use rustpython_codegen::compile::compile_top;
use rustpython_compiler::{CompileOpts, Mode};
use rustpython_compiler_core::CodeObject;
use rustpython_parser::ast::{ArgData, Located, Location};
use rustpython_parser::{ast, parse, Mode as ParseMode};
use snafu::ResultExt;

use crate::fail_parse_error;
use crate::python::error::{PyCompileSnafu, PyParseSnafu, Result};
use crate::python::ffi_types::copr::parse::{ret_parse_error, DecoratorArgs};

fn create_located<T>(node: T, loc: Location) -> Located<T> {
    Located::new(loc, loc, node)
}

/// generate a call to the coprocessor function
/// with arguments given in decorator's `args` list
/// also set in location in source code to `loc`
fn gen_call(
    name: &str,
    deco_args: &DecoratorArgs,
    kwarg: &Option<String>,
    loc: &Location,
) -> ast::Stmt<()> {
    let mut loc = *loc;
    // adding a line to avoid confusing if any error occurs when calling the function
    // then the pretty print will point to the last line in code
    // instead of point to any of existing code written by user.
    loc.newline();
    let mut args: Vec<Located<ast::ExprKind>> = if let Some(arg_names) = &deco_args.arg_names {
        arg_names
            .iter()
            .map(|v| {
                let node = ast::ExprKind::Name {
                    id: v.clone(),
                    ctx: ast::ExprContext::Load,
                };
                create_located(node, loc)
            })
            .collect()
    } else {
        vec![]
    };

    if let Some(kwarg) = kwarg {
        let node = ast::ExprKind::Name {
            id: kwarg.clone(),
            ctx: ast::ExprContext::Load,
        };
        args.push(create_located(node, loc));
    }

    let func = ast::ExprKind::Call {
        func: Box::new(create_located(
            ast::ExprKind::Name {
                id: name.to_string(),
                ctx: ast::ExprContext::Load,
            },
            loc,
        )),
        args,
        keywords: Vec::new(),
    };
    let stmt = ast::StmtKind::Expr {
        value: Box::new(create_located(func, loc)),
    };
    create_located(stmt, loc)
}

/// stripe the decorator(`@xxxx`) and type annotation(for type checker is done in rust function), add one line in the ast for call function with given parameter, and compiler into `CodeObject`
///
/// The rationale is that rustpython's vm is not very efficient according to [official benchmark](https://rustpython.github.io/benchmarks),
/// So we should avoid running too much Python Bytecode, hence in this function we delete `@` decorator(instead of actually write a decorator in python)
/// And add a function call in the end and also
/// strip type annotation
pub fn compile_script(
    name: &str,
    deco_args: &DecoratorArgs,
    kwarg: &Option<String>,
    script: &str,
) -> Result<CodeObject> {
    // note that it's important to use `parser::Mode::Interactive` so the ast can be compile to return a result instead of return None in eval mode
    let mut top = parse(script, ParseMode::Interactive, "<embedded>").context(PyParseSnafu)?;
    // erase decorator
    if let ast::Mod::Interactive { body } = &mut top {
        let stmts = body;
        let mut loc = None;
        for stmt in stmts.iter_mut() {
            if let ast::StmtKind::FunctionDef {
                name: _,
                args,
                body: _,
                decorator_list,
                returns,
                type_comment: __main__,
            } = &mut stmt.node
            {
                // Rewrite kwargs in coprocessor, make it as a positional argument
                if !decorator_list.is_empty() {
                    if let Some(kwarg) = kwarg {
                        args.kwarg = None;
                        let node = ArgData {
                            arg: kwarg.clone(),
                            annotation: None,
                            type_comment: Some("kwargs".to_string()),
                        };
                        let kwarg = create_located(node, stmt.location);
                        args.args.push(kwarg);
                    }
                }

                *decorator_list = Vec::new();
                // strip type annotation
                // def a(b: int, c:int) -> int
                // will became
                // def a(b, c)
                *returns = None;
                for arg in &mut args.args {
                    arg.node.annotation = None;
                }
            } else if matches!(
                stmt.node,
                ast::StmtKind::Import { .. } | ast::StmtKind::ImportFrom { .. }
            ) {
                // import statements are allowed.
            } else {
                // already checked in parser
                unreachable!()
            }
            loc = Some(stmt.location);

            // This manually construct ast has no corresponding code
            // in the script, so just give it a location that don't exist in original script
            // (which doesn't matter because Location usually only used in pretty print errors)
        }
        // Append statement which calling coprocessor function.
        // It's safe to unwrap loc, it is always exists.
        stmts.push(gen_call(name, deco_args, kwarg, &loc.unwrap()));
    } else {
        return fail_parse_error!(format!("Expect statement in script, found: {top:?}"), None);
    }
    // use `compile::Mode::BlockExpr` so it return the result of statement
    compile_top(
        &top,
        "<embedded>".to_string(),
        Mode::BlockExpr,
        CompileOpts { optimize: 0 },
    )
    .context(PyCompileSnafu)
}