nu-engine 0.112.2

Nushell's evaluation engine
Documentation
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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
use nu_protocol::{
    ENV_VARIABLE_ID, IntoSpanned, RegId, Span, Spanned, Value,
    ast::{Assignment, Boolean, CellPath, Expr, Expression, Math, Operator, PathMember, Pattern},
    engine::StateWorkingSet,
    ir::{Instruction, Literal},
};
use nu_utils::IgnoreCaseExt;

use super::{BlockBuilder, CompileError, RedirectModes, compile_expression};

/// Compile a binary operation (arithmetic, comparison, boolean, or assignment).
///
/// # Arguments
/// * `working_set` - The state working set for symbol resolution
/// * `builder` - The IR block builder
/// * `lhs` - Left-hand side expression
/// * `op` - The operator with span information
/// * `rhs` - Right-hand side expression
/// * `span` - Span for the entire binary operation
/// * `in_reg` - Optional input register for `$in` variable access in subexpressions
/// * `out_reg` - Output register for the result
#[allow(clippy::too_many_arguments)]
pub(crate) fn compile_binary_op(
    working_set: &StateWorkingSet,
    builder: &mut BlockBuilder,
    lhs: &Expression,
    op: Spanned<Operator>,
    rhs: &Expression,
    span: Span,
    in_reg: Option<RegId>,
    out_reg: RegId,
) -> Result<(), CompileError> {
    if let Operator::Assignment(assign_op) = op.item {
        if let Some(decomposed_op) = decompose_assignment(assign_op) {
            // Compiling an assignment that uses a binary op with the existing value
            compile_binary_op(
                working_set,
                builder,
                lhs,
                decomposed_op.into_spanned(op.span),
                rhs,
                span,
                in_reg,
                out_reg,
            )?;
        } else {
            // Compiling a plain assignment, where the current left-hand side value doesn't matter
            compile_expression(
                working_set,
                builder,
                rhs,
                RedirectModes::value(rhs.span),
                None,
                out_reg,
            )?;
        }

        compile_assignment(working_set, builder, lhs, op.span, out_reg)?;

        // Load out_reg with Nothing, as that's the result of an assignment
        builder.load_literal(out_reg, Literal::Nothing.into_spanned(op.span))
    } else {
        // Not an assignment: just do the binary op
        let lhs_reg = out_reg;

        // Pass in_reg to lhs if it uses $in
        let lhs_in_reg = if lhs.has_in_variable(working_set) {
            in_reg
        } else {
            None
        };

        compile_expression(
            working_set,
            builder,
            lhs,
            RedirectModes::value(lhs.span),
            lhs_in_reg,
            lhs_reg,
        )?;

        match op.item {
            // `and` / `or` are short-circuiting, use `match` to avoid running the RHS if LHS is
            // the correct value. Be careful to support and/or on non-boolean values
            Operator::Boolean(bool_op @ Boolean::And)
            | Operator::Boolean(bool_op @ Boolean::Or) => {
                // `and` short-circuits on false, and `or` short-circuits on true.
                let short_circuit_value = match bool_op {
                    Boolean::And => false,
                    Boolean::Or => true,
                    Boolean::Xor => unreachable!(),
                };

                // Before match against lhs_reg, it's important to collect it first to get a concrete value if there is a subexpression.
                builder.push(Instruction::Collect { src_dst: lhs_reg }.into_spanned(lhs.span))?;
                // Short-circuit to return `lhs_reg`. `match` op does not consume `lhs_reg`.
                let short_circuit_label = builder.label(None);
                builder.r#match(
                    Pattern::Value(Value::bool(short_circuit_value, op.span)),
                    lhs_reg,
                    short_circuit_label,
                    op.span,
                )?;

                // If the match failed then this was not the short-circuit value, so we have to run
                // the RHS expression
                let rhs_reg = builder.next_register()?;

                // Pass in_reg to rhs if it uses $in
                let rhs_in_reg = if rhs.has_in_variable(working_set) {
                    in_reg
                } else {
                    None
                };

                compile_expression(
                    working_set,
                    builder,
                    rhs,
                    RedirectModes::value(rhs.span),
                    rhs_in_reg,
                    rhs_reg,
                )?;

                // It may seem intuitive that we can just return RHS here, but we do have to
                // actually execute the binary-op in case this is not a boolean
                builder.push(
                    Instruction::BinaryOp {
                        lhs_dst: lhs_reg,
                        op: Operator::Boolean(bool_op),
                        rhs: rhs_reg,
                    }
                    .into_spanned(op.span),
                )?;

                // In either the short-circuit case or other case, the result is in lhs_reg =
                // out_reg
                builder.set_label(short_circuit_label, builder.here())?;
            }
            _ => {
                let rhs_reg = builder.next_register()?;

                // Pass in_reg to rhs if it uses $in
                let rhs_in_reg = if rhs.has_in_variable(working_set) {
                    in_reg
                } else {
                    None
                };

                compile_expression(
                    working_set,
                    builder,
                    rhs,
                    RedirectModes::value(rhs.span),
                    rhs_in_reg,
                    rhs_reg,
                )?;

                builder.push(
                    Instruction::BinaryOp {
                        lhs_dst: lhs_reg,
                        op: op.item,
                        rhs: rhs_reg,
                    }
                    .into_spanned(op.span),
                )?;
            }
        }

        if lhs_reg != out_reg {
            builder.push(
                Instruction::Move {
                    dst: out_reg,
                    src: lhs_reg,
                }
                .into_spanned(op.span),
            )?;
        }

        builder.push(Instruction::Span { src_dst: out_reg }.into_spanned(span))?;

        Ok(())
    }
}

/// The equivalent plain operator to use for an assignment, if any
pub(crate) fn decompose_assignment(assignment: Assignment) -> Option<Operator> {
    match assignment {
        Assignment::Assign => None,
        Assignment::AddAssign => Some(Operator::Math(Math::Add)),
        Assignment::SubtractAssign => Some(Operator::Math(Math::Subtract)),
        Assignment::MultiplyAssign => Some(Operator::Math(Math::Multiply)),
        Assignment::DivideAssign => Some(Operator::Math(Math::Divide)),
        Assignment::ConcatenateAssign => Some(Operator::Math(Math::Concatenate)),
    }
}

/// Compile assignment of the value in a register to a left-hand expression
pub(crate) fn compile_assignment(
    working_set: &StateWorkingSet,
    builder: &mut BlockBuilder,
    lhs: &Expression,
    assignment_span: Span,
    rhs_reg: RegId,
) -> Result<(), CompileError> {
    match lhs.expr {
        Expr::Var(var_id) => {
            // Double check that the variable is supposed to be mutable
            if !working_set.get_variable(var_id).mutable {
                return Err(CompileError::AssignmentRequiresMutableVar { span: lhs.span });
            }

            builder.push(
                Instruction::StoreVariable {
                    var_id,
                    src: rhs_reg,
                }
                .into_spanned(assignment_span),
            )?;
            Ok(())
        }
        Expr::FullCellPath(ref path) => match (&path.head, &path.tail) {
            (
                Expression {
                    expr: Expr::Var(var_id),
                    ..
                },
                _,
            ) if *var_id == ENV_VARIABLE_ID => {
                // This will be an assignment to an environment variable.
                let Some(PathMember::String { val: key, .. }) = path.tail.first() else {
                    return Err(CompileError::CannotReplaceEnv { span: lhs.span });
                };

                // Some env vars can't be set by Nushell code.
                const AUTOMATIC_NAMES: &[&str] = &["PWD", "FILE_PWD", "CURRENT_FILE"];
                if AUTOMATIC_NAMES.iter().any(|name| key.eq_ignore_case(name)) {
                    return Err(CompileError::AutomaticEnvVarSetManually {
                        envvar_name: "PWD".into(),
                        span: lhs.span,
                    });
                }

                let key_data = builder.data(key)?;

                let val_reg = if path.tail.len() > 1 {
                    // Get the current value of the head and first tail of the path, from env
                    let head_reg = builder.next_register()?;

                    // We could use compile_load_env, but this shares the key data...
                    // Always use optional, because it doesn't matter if it's already there
                    builder.push(
                        Instruction::LoadEnvOpt {
                            dst: head_reg,
                            key: key_data,
                        }
                        .into_spanned(lhs.span),
                    )?;

                    // Default to empty record so we can do further upserts
                    let default_label = builder.label(None);
                    let upsert_label = builder.label(None);
                    builder.branch_if_empty(head_reg, default_label, assignment_span)?;
                    builder.jump(upsert_label, assignment_span)?;

                    builder.set_label(default_label, builder.here())?;
                    builder.load_literal(
                        head_reg,
                        Literal::Record { capacity: 0 }.into_spanned(lhs.span),
                    )?;

                    // Do the upsert on the current value to incorporate rhs
                    builder.set_label(upsert_label, builder.here())?;
                    compile_upsert_cell_path(
                        builder,
                        (&path.tail[1..]).into_spanned(lhs.span),
                        head_reg,
                        rhs_reg,
                        assignment_span,
                    )?;

                    head_reg
                } else {
                    // Path has only one tail, so we don't need the current value to do an upsert,
                    // just set it directly to rhs
                    rhs_reg
                };

                // Finally, store the modified env variable
                builder.push(
                    Instruction::StoreEnv {
                        key: key_data,
                        src: val_reg,
                    }
                    .into_spanned(assignment_span),
                )?;
                Ok(())
            }
            (_, tail) if tail.is_empty() => {
                // If the path tail is empty, we can really just treat this as if it were an
                // assignment to the head
                compile_assignment(working_set, builder, &path.head, assignment_span, rhs_reg)
            }
            _ => {
                // Just a normal assignment to some path
                let head_reg = builder.next_register()?;

                // Compile getting current value of the head expression
                compile_expression(
                    working_set,
                    builder,
                    &path.head,
                    RedirectModes::value(path.head.span),
                    None,
                    head_reg,
                )?;

                // Upsert the tail of the path into the old value of the head expression
                compile_upsert_cell_path(
                    builder,
                    path.tail.as_slice().into_spanned(lhs.span),
                    head_reg,
                    rhs_reg,
                    assignment_span,
                )?;

                // Now compile the assignment of the updated value to the head
                compile_assignment(working_set, builder, &path.head, assignment_span, head_reg)
            }
        },
        Expr::Garbage => Err(CompileError::Garbage { span: lhs.span }),
        _ => Err(CompileError::AssignmentRequiresVar { span: lhs.span }),
    }
}

/// Compile an upsert-cell-path instruction, with known literal members
pub(crate) fn compile_upsert_cell_path(
    builder: &mut BlockBuilder,
    members: Spanned<&[PathMember]>,
    src_dst: RegId,
    new_value: RegId,
    span: Span,
) -> Result<(), CompileError> {
    let path_reg = builder.literal(
        Literal::CellPath(
            CellPath {
                members: members.item.to_vec(),
            }
            .into(),
        )
        .into_spanned(members.span),
    )?;
    builder.push(
        Instruction::UpsertCellPath {
            src_dst,
            path: path_reg,
            new_value,
        }
        .into_spanned(span),
    )?;
    Ok(())
}

/// Compile the correct sequence to get an environment variable + follow a path on it
pub(crate) fn compile_load_env(
    builder: &mut BlockBuilder,
    span: Span,
    path: &[PathMember],
    out_reg: RegId,
) -> Result<(), CompileError> {
    match path {
        [] => builder.push(
            Instruction::LoadVariable {
                dst: out_reg,
                var_id: ENV_VARIABLE_ID,
            }
            .into_spanned(span),
        )?,
        [PathMember::Int { span, .. }, ..] => {
            return Err(CompileError::AccessEnvByInt { span: *span });
        }
        [
            PathMember::String {
                val: key, optional, ..
            },
            tail @ ..,
        ] => {
            let key = builder.data(key)?;

            builder.push(if *optional {
                Instruction::LoadEnvOpt { dst: out_reg, key }.into_spanned(span)
            } else {
                Instruction::LoadEnv { dst: out_reg, key }.into_spanned(span)
            })?;

            if !tail.is_empty() {
                let path = builder.literal(
                    Literal::CellPath(Box::new(CellPath {
                        members: tail.to_vec(),
                    }))
                    .into_spanned(span),
                )?;
                builder.push(
                    Instruction::FollowCellPath {
                        src_dst: out_reg,
                        path,
                    }
                    .into_spanned(span),
                )?;
            }
        }
    }
    Ok(())
}