neovm-core 0.0.2

Core runtime structures for NeoVM
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
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
//! Common test utilities for neovm-core.
//!
//! Provides shared helpers used across all test modules.

use crate::emacs_core::error::map_flow;
use crate::emacs_core::load::{
    apply_ldefs_boot_autoloads_for_names, bootstrap_load_path_entries,
    create_runtime_startup_evaluator_cached, find_file_in_load_path, get_load_path, load_file,
};
use crate::emacs_core::value::Value;
use crate::emacs_core::{Context, format_eval_result};
use std::path::PathBuf;

/// Initialize the tracing subscriber for test output.
///
/// Thin wrapper around [`crate::logging::init_for_tests`] kept for the
/// many existing call sites in this crate's test files. Tests never
/// write to a log file regardless of `NEOMACS_LOG_TO_FILE` — output is
/// always routed through the test harness's writer.
///
/// # Usage
/// Call at the start of any test that needs tracing:
/// ```rust,ignore
/// #[test]
/// fn my_test() {
///     crate::test_utils::init_test_tracing();
///     // ... test code ...
/// }
/// ```
pub fn init_test_tracing() {
    crate::logging::init_for_tests();
}

/// Load a small GNU Lisp runtime that is sufficient for tests that need
/// `byte-run`, backquote expansion, and the basic `subr.el` support layer,
/// without paying for full `loadup.el` startup.
pub fn load_minimal_gnu_backquote_runtime(eval: &mut Context) {
    eval.set_lexical_binding(true);
    let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    let project_root = manifest.parent().expect("project root");
    let lisp_dir = project_root.join("lisp");
    eval.set_variable(
        "load-path",
        Value::list(bootstrap_load_path_entries(&lisp_dir)),
    );
    let load_path = get_load_path(&eval.obarray());
    for name in &[
        "emacs-lisp/debug-early",
        "emacs-lisp/byte-run",
        "emacs-lisp/backquote",
        "subr",
    ] {
        let path = find_file_in_load_path(name, &load_path)
            .unwrap_or_else(|| panic!("cannot find {name}"));
        load_file(eval, &path).unwrap_or_else(|err| panic!("load {name}: {err:?}"));
    }
}

/// Load GNU `macroexp.el` after the early `subr.el` layer, mirroring the
/// loadup phase before later Lisp files such as `simple.el` are evaluated.
pub fn load_gnu_macroexp_runtime(eval: &mut Context) {
    if eval.obarray().symbol_function("macroexp-progn").is_some() {
        return;
    }
    let load_path = get_load_path(&eval.obarray());
    for name in &["emacs-lisp/macroexp", "emacs-lisp/pcase"] {
        let path = find_file_in_load_path(name, &load_path)
            .unwrap_or_else(|| panic!("cannot find {name}"));
        load_file(eval, &path).unwrap_or_else(|err| panic!("load {name}: {err:?}"));
    }
}

/// Load the GNU `simple.el` undo auto-amalgamation surface needed by
/// primitives such as `delete-char` and `self-insert-command`.
///
/// GNU cmds.c calls `undo-auto-amalgamate` unconditionally; that function is
/// Lisp-defined by `simple.el` during loadup. Some focused unit tests do not
/// run loadup, so this evaluates only the real source forms that provide that
/// function and its helper variables instead of guarding the primitive. It does
/// not install `undo-auto--undoable-change`, which also requires `timer.el`.
pub fn load_gnu_undo_auto_runtime(eval: &mut Context) {
    if eval
        .obarray()
        .symbol_function("undo-auto-amalgamate")
        .is_some()
    {
        return;
    }
    if eval.obarray().symbol_function("defvar-local").is_none() {
        load_minimal_gnu_backquote_runtime(eval);
    }
    load_gnu_macroexp_runtime(eval);

    let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    let project_root = manifest.parent().expect("project root");
    let simple_path = project_root.join("lisp/simple.el");
    let simple_source =
        std::fs::read_to_string(&simple_path).unwrap_or_else(|err| panic!("read simple.el: {err}"));
    let limit_start = simple_source
        .find("(defvar amalgamating-undo-limit ")
        .expect("simple.el amalgamating-undo-limit form");
    let limit_form = crate::emacs_core::value_reader::read_one(&simple_source, limit_start)
        .expect("parse simple.el amalgamating-undo-limit")
        .map(|(form, _)| form)
        .expect("read simple.el amalgamating-undo-limit");

    let start = simple_source
        .find("(defvar-local undo-auto--last-boundary-cause ")
        .expect("simple.el undo auto section start");
    let end = simple_source[start..]
        .find("(defun undo-auto--undoable-change ")
        .map(|offset| start + offset)
        .expect("simple.el undo auto section end");
    let mut forms = vec![limit_form];
    forms.extend(
        crate::emacs_core::value_reader::read_all(&simple_source[start..end])
            .expect("parse simple.el undo auto section"),
    );

    let roots = eval.save_specpdl_roots();
    for form in &forms {
        eval.push_specpdl_root(*form);
    }
    for (index, form) in forms.into_iter().enumerate() {
        eval.eval_sub(form).map_err(map_flow).unwrap_or_else(|err| {
            panic!(
                "eval simple.el undo auto section form #{index} {}: {err}",
                crate::emacs_core::print::print_value(&form)
            )
        });
    }
    eval.restore_specpdl_roots(roots);
}

/// Load the real GNU `simple.el` special-mode surface required by help buffers.
pub fn load_gnu_special_mode_runtime(eval: &mut Context) {
    if eval.obarray().symbol_value("special-mode-map").is_some()
        && eval.obarray().symbol_function("special-mode").is_some()
    {
        return;
    }

    let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    let project_root = manifest.parent().expect("project root");
    let simple_path = project_root.join("lisp/simple.el");
    let simple_source =
        std::fs::read_to_string(&simple_path).unwrap_or_else(|err| panic!("read simple.el: {err}"));
    let start = simple_source
        .find("(defun fundamental-mode ()")
        .expect("simple.el fundamental-mode form");
    let end = simple_source[start..]
        .find(";; Making and deleting lines.")
        .map(|offset| start + offset)
        .expect("simple.el special-mode section end");
    let forms = crate::emacs_core::value_reader::read_all(&simple_source[start..end])
        .expect("parse simple.el special-mode section");

    let roots = eval.save_specpdl_roots();
    for form in &forms {
        eval.push_specpdl_root(*form);
    }
    for (index, form) in forms.into_iter().enumerate() {
        eval.eval_sub(form).map_err(map_flow).unwrap_or_else(|err| {
            panic!(
                "eval simple.el special-mode section form #{index} {}: {err}",
                crate::emacs_core::print::print_value(&form)
            )
        });
    }
    eval.restore_specpdl_roots(roots);
}

/// Load the real GNU display predicate used by help separators.
pub fn load_gnu_display_graphic_runtime(eval: &mut Context) {
    if eval
        .obarray()
        .symbol_function("display-graphic-p")
        .is_some()
    {
        return;
    }

    let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    let project_root = manifest.parent().expect("project root");
    let frame_path = project_root.join("lisp/frame.el");
    let frame_source =
        std::fs::read_to_string(&frame_path).unwrap_or_else(|err| panic!("read frame.el: {err}"));
    let framep_start = frame_source
        .find("(defun framep-on-display ")
        .expect("frame.el framep-on-display form");
    let framep_form = crate::emacs_core::value_reader::read_one(&frame_source, framep_start)
        .expect("parse frame.el framep-on-display")
        .map(|(form, _)| form)
        .expect("read frame.el framep-on-display");
    let display_start = frame_source
        .find("(defun display-graphic-p ")
        .expect("frame.el display-graphic-p form");
    let display_form = crate::emacs_core::value_reader::read_one(&frame_source, display_start)
        .expect("parse frame.el display-graphic-p")
        .map(|(form, _)| form)
        .expect("read frame.el display-graphic-p");
    let forms = vec![framep_form, display_form];

    let roots = eval.save_specpdl_roots();
    for form in &forms {
        eval.push_specpdl_root(*form);
    }
    for (index, form) in forms.into_iter().enumerate() {
        eval.eval_sub(form).map_err(map_flow).unwrap_or_else(|err| {
            panic!(
                "eval frame.el display predicate form #{index} {}: {err}",
                crate::emacs_core::print::print_value(&form)
            )
        });
    }
    eval.restore_specpdl_roots(roots);
}

/// Load GNU aliases from `window.el` for C-defined window primitives.
pub fn load_gnu_window_alias_runtime(eval: &mut Context) {
    if eval.obarray().symbol_function("window-width").is_some() {
        return;
    }

    let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    let project_root = manifest.parent().expect("project root");
    let window_path = project_root.join("lisp/window.el");
    let window_source =
        std::fs::read_to_string(&window_path).unwrap_or_else(|err| panic!("read window.el: {err}"));
    let start = window_source
        .find("(defalias 'window-height ")
        .expect("window.el window primitive alias block");
    let end = window_source[start..]
        .find("(defun window-full-height-p ")
        .map(|offset| start + offset)
        .expect("window.el window primitive alias block end");
    let forms = crate::emacs_core::value_reader::read_all(&window_source[start..end])
        .expect("parse window.el window primitive alias block");

    let roots = eval.save_specpdl_roots();
    for form in &forms {
        eval.push_specpdl_root(*form);
    }
    for (index, form) in forms.into_iter().enumerate() {
        eval.eval_sub(form).map_err(map_flow).unwrap_or_else(|err| {
            panic!(
                "eval window.el primitive alias form #{index} {}: {err}",
                crate::emacs_core::print::print_value(&form)
            )
        });
    }
    eval.restore_specpdl_roots(roots);
}

/// Load the real GNU separator-line helper used by help buffers.
pub fn load_gnu_separator_line_runtime(eval: &mut Context) {
    if eval
        .obarray()
        .symbol_function("make-separator-line")
        .is_some()
    {
        return;
    }

    let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    let project_root = manifest.parent().expect("project root");
    let simple_path = project_root.join("lisp/simple.el");
    let simple_source =
        std::fs::read_to_string(&simple_path).unwrap_or_else(|err| panic!("read simple.el: {err}"));
    let start = simple_source
        .find("(defface separator-line")
        .expect("simple.el separator-line face form");
    let end = simple_source[start..]
        .find("(defun delete-indentation ")
        .map(|offset| start + offset)
        .expect("simple.el make-separator-line section end");
    let forms = crate::emacs_core::value_reader::read_all(&simple_source[start..end])
        .expect("parse simple.el make-separator-line section");

    let roots = eval.save_specpdl_roots();
    for form in &forms {
        eval.push_specpdl_root(*form);
    }
    for (index, form) in forms.into_iter().enumerate() {
        eval.eval_sub(form).map_err(map_flow).unwrap_or_else(|err| {
            panic!(
                "eval simple.el make-separator-line section form #{index} {}: {err}",
                crate::emacs_core::print::print_value(&form)
            )
        });
    }
    eval.restore_specpdl_roots(roots);
}

/// Load the real GNU Emacs Lisp syntax table used by help-mode.
pub fn load_gnu_elisp_syntax_table_runtime(eval: &mut Context) {
    if eval
        .obarray()
        .symbol_value("emacs-lisp-mode-syntax-table")
        .is_some()
    {
        return;
    }

    let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    let project_root = manifest.parent().expect("project root");
    let elisp_mode_path = project_root.join("lisp/progmodes/elisp-mode.el");
    let elisp_mode_source = std::fs::read_to_string(&elisp_mode_path)
        .unwrap_or_else(|err| panic!("read elisp-mode.el: {err}"));
    let start = elisp_mode_source
        .find("(defvar emacs-lisp-mode-syntax-table")
        .expect("elisp-mode.el emacs-lisp-mode-syntax-table form");
    let form = crate::emacs_core::value_reader::read_one(&elisp_mode_source, start)
        .expect("parse elisp-mode.el emacs-lisp-mode-syntax-table")
        .map(|(form, _)| form)
        .expect("read elisp-mode.el emacs-lisp-mode-syntax-table");

    let roots = eval.save_specpdl_roots();
    eval.push_specpdl_root(form);
    eval.eval_sub(form).map_err(map_flow).unwrap_or_else(|err| {
        panic!(
            "eval elisp-mode.el emacs-lisp-mode-syntax-table {}: {err}",
            crate::emacs_core::print::print_value(&form)
        )
    });
    eval.restore_specpdl_roots(roots);
}

/// Load a small GNU Lisp runtime that is sufficient for `help.el`
/// semantics such as `substitute-command-keys`, without paying for
/// full `loadup.el` startup.
pub fn load_minimal_gnu_help_runtime(eval: &mut Context) {
    load_minimal_gnu_backquote_runtime(eval);
    let load_path = get_load_path(&eval.obarray());
    for name in &[
        "keymap",
        "widget",
        "custom",
        "cus-face",
        "faces",
        "bindings",
        "emacs-lisp/macroexp",
        "emacs-lisp/pcase",
        "emacs-lisp/gv",
    ] {
        let path = find_file_in_load_path(name, &load_path)
            .unwrap_or_else(|| panic!("cannot find {name}"));
        load_file(eval, &path).unwrap_or_else(|err| panic!("load {name}: {err:?}"));
    }
    apply_ldefs_boot_autoloads_for_names(
        eval,
        &[
            "define-derived-mode",
            "define-inline",
            "define-minor-mode",
            "help-fns-function-name",
            "regexp-opt",
            "rx",
        ],
    )
    .expect("ldefs-boot help runtime autoloads");
    for name in &[
        "emacs-lisp/cl-preloaded",
        "emacs-lisp/oclosure",
        "obarray",
        "abbrev",
        "emacs-lisp/cl-generic",
        "emacs-lisp/seq",
        "emacs-lisp/easy-mmode",
        "emacs-lisp/derived",
        "emacs-lisp/easymenu",
        "button",
        "help-macro",
    ] {
        let path = find_file_in_load_path(name, &load_path)
            .unwrap_or_else(|| panic!("cannot find {name}"));
        load_file(eval, &path).unwrap_or_else(|err| panic!("load {name}: {err:?}"));
    }
    load_gnu_special_mode_runtime(eval);
    load_gnu_display_graphic_runtime(eval);
    load_gnu_window_alias_runtime(eval);
    load_gnu_separator_line_runtime(eval);
    for name in &["progmodes/prog-mode", "emacs-lisp/lisp-mode", "tool-bar"] {
        let path = find_file_in_load_path(name, &load_path)
            .unwrap_or_else(|| panic!("cannot find {name}"));
        load_file(eval, &path).unwrap_or_else(|err| panic!("load {name}: {err:?}"));
    }
    load_gnu_elisp_syntax_table_runtime(eval);
    // Force the .el source — `find_file_in_load_path("help", ...)`
    // returns help.elc when both exist, but `read_to_string` then
    // mis-parses .elc binary data and emits `(nil . OFFSET)` doc
    // refs that downstream `defface` rejects. Passing "help.el"
    // explicitly bypasses the suffix preference loop.
    let help_path = find_file_in_load_path("help.el", &load_path).expect("cannot find help.el");
    let help_source =
        std::fs::read_to_string(&help_path).unwrap_or_else(|err| panic!("read help.el: {err}"));
    let help_forms =
        crate::emacs_core::value_reader::read_all(&help_source).expect("parse help.el");
    // Root every parsed form upfront. Without this, forms still
    // sitting in the `help_forms` Vec aren't visible to the GC and
    // can be reclaimed when an `eval_sub` of an earlier form
    // triggers a collection. Mirrors the rooting pattern in
    // `Context::eval_str_each` (eval.rs:6170-6183).
    let roots = eval.save_specpdl_roots();
    for form in &help_forms {
        eval.push_specpdl_root(*form);
    }
    let mut found_substitute_command_keys = false;
    let mut found_describe_map_fill_columns = false;
    for form in &help_forms {
        let is_substitute_command_keys = is_named_defun_value(form, "substitute-command-keys");
        let is_describe_map_fill_columns = is_named_defun_value(form, "describe-map--fill-columns");
        eval.eval_sub(*form)
            .map_err(map_flow)
            .unwrap_or_else(|err| panic!("eval help.el prefix: {err:?}"));
        if is_substitute_command_keys {
            found_substitute_command_keys = true;
        }
        if is_describe_map_fill_columns {
            found_describe_map_fill_columns = true;
            break;
        }
    }
    eval.restore_specpdl_roots(roots);
    assert!(
        found_substitute_command_keys,
        "help.el should define substitute-command-keys"
    );
    assert!(
        found_describe_map_fill_columns,
        "help.el should define describe-map--fill-columns"
    );
    load_gnu_undo_auto_runtime(eval);
}

fn is_named_defun_value(form: &Value, name: &str) -> bool {
    if !form.is_cons() {
        return false;
    }
    let car = form.cons_car();
    if !car.is_symbol_named("defun") {
        return false;
    }
    let cdr = form.cons_cdr();
    if !cdr.is_cons() {
        return false;
    }
    cdr.cons_car().is_symbol_named(name)
}

/// Create a bare evaluator with GNU `ldefs-boot.el` autoload cells restored
/// for the named symbols and a bootstrap-compatible `load-path`.
pub fn eval_with_ldefs_boot_autoloads(names: &[&str]) -> Context {
    let mut eval = Context::new();
    let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    let project_root = manifest.parent().expect("project root");
    let lisp_dir = project_root.join("lisp");
    eval.set_variable(
        "load-path",
        Value::list(bootstrap_load_path_entries(&lisp_dir)),
    );
    for name in names {
        eval.obarray_mut().fmakunbound(name);
    }
    apply_ldefs_boot_autoloads_for_names(&mut eval, names).expect("ldefs-boot autoload restore");
    eval
}

/// Create a cached runtime-startup evaluator for tests that need the full
/// GNU bootstrap surface.
pub fn runtime_startup_context() -> Context {
    create_runtime_startup_evaluator_cached().expect("bootstrap")
}

/// Evaluate FORMS in a cached runtime-startup evaluator and return formatted
/// results, matching the common bootstrap test pattern.
pub fn runtime_startup_eval_all(src: &str) -> Vec<String> {
    let mut eval = runtime_startup_context();
    let forms = crate::emacs_core::value_reader::read_all(src).expect("parse");
    // Root parsed forms across the eval loop. Heap literals like bignums,
    // strings, and cons cells are otherwise invisible to the GC until the
    // evaluator reaches them, which can corrupt bootstrap tests that call
    // into bytecode and trigger collection mid-eval.
    let roots = eval.save_specpdl_roots();
    for form in &forms {
        eval.push_specpdl_root(*form);
    }
    let results = forms
        .into_iter()
        .map(|form| {
            let result = eval.eval_form(form);
            format_eval_result(&result)
        })
        .collect();
    eval.restore_specpdl_roots(roots);
    results
}

/// Evaluate the first form from SRC in a cached runtime-startup evaluator and
/// return the formatted result.
pub fn runtime_startup_eval_one(src: &str) -> String {
    let mut eval = runtime_startup_context();
    let result = eval.eval_str(src);
    format_eval_result(&result)
}