Skip to main content

fidget_jit/
lib.rs

1//! Compilation down to native machine code
2//!
3//! Users are unlikely to use anything in this module other than [`JitFunction`],
4//! which is a [`Function`] that uses JIT evaluation.
5//!
6//! ```
7//! use fidget_core::{
8//!     context::Tree,
9//!     shape::EzShape,
10//! };
11//! use fidget_jit::JitShape;
12//!
13//! let tree = Tree::x() + Tree::y();
14//! let shape = JitShape::from(tree);
15//!
16//! // Generate machine code to execute the tape
17//! let tape = shape.ez_point_tape();
18//! let mut eval = JitShape::new_point_eval();
19//!
20//! // This calls directly into that machine code!
21//! let (r, _trace) = eval.eval(&tape, 0.1, 0.3, 0.0)?;
22//! assert_eq!(r, 0.1 + 0.3);
23//! # Ok::<(), Box<dyn std::error::Error>>(())
24//! ```
25
26use crate::mmap::{Mmap, MmapWriter};
27use fidget_core::{
28    compiler::RegOp,
29    context::{BadNode, Context, Node},
30    eval::{
31        BulkEvalError, BulkEvaluator, BulkOutput, Function, MathFunction, Tape,
32        TracingEvalError, TracingEvaluator,
33    },
34    render::{RenderHints, TileSizes},
35    types::{Grad, Interval},
36    var::VarMap,
37    vm::{BadTrace, Choice, GenericVmFunction, VmData, VmTrace, VmWorkspace},
38};
39
40use dynasmrt::{
41    AssemblyOffset, DynamicLabel, DynasmApi, DynasmError, DynasmLabelApi,
42    TargetKind, components::PatchLoc, dynasm,
43};
44use std::sync::Arc;
45
46mod mmap;
47mod permit;
48pub(crate) use permit::WritePermit;
49
50// Evaluators
51mod float_slice;
52mod grad_slice;
53mod interval;
54mod point;
55
56#[cfg(not(any(
57    target_os = "linux",
58    target_os = "macos",
59    target_os = "windows"
60)))]
61compile_error!(
62    "The `jit` module only builds on Linux, macOS, and Windows; \
63    please disable the `jit` feature"
64);
65
66#[cfg(target_arch = "aarch64")]
67mod aarch64;
68#[cfg(target_arch = "aarch64")]
69use aarch64 as arch;
70
71#[cfg(target_arch = "x86_64")]
72mod x86_64;
73#[cfg(target_arch = "x86_64")]
74use x86_64 as arch;
75
76/// Number of registers available when executing natively
77const REGISTER_LIMIT: usize = arch::REGISTER_LIMIT;
78
79/// Offset before the first useable register
80const OFFSET: u8 = arch::OFFSET;
81
82/// Register written to by `CopyImm`
83///
84/// It is the responsibility of functions to avoid writing to `IMM_REG` in cases
85/// where it could be one of their arguments (i.e. all functions of 2 or more
86/// arguments).
87const IMM_REG: u8 = arch::IMM_REG;
88
89/// Converts from a tape-local register to a hardware register
90///
91/// Tape-local registers are in the range `0..REGISTER_LIMIT`, while ARM
92/// registers have an offset (based on calling convention).
93///
94/// This uses `wrapping_add` to support immediates, which are loaded into a
95/// register below [`OFFSET`] (which is "negative" from the perspective of this
96/// function).
97fn reg(r: u8) -> u8 {
98    let out = r.wrapping_add(OFFSET);
99    assert!(out < 32);
100    out
101}
102
103const CHOICE_LEFT: u32 = Choice::Left as u32;
104const CHOICE_RIGHT: u32 = Choice::Right as u32;
105const CHOICE_BOTH: u32 = Choice::Both as u32;
106
107/// Trait for generating machine assembly
108trait Assembler {
109    /// Data type used during evaluation.
110    ///
111    /// This should be a `repr(C)` type, so it can be passed around directly.
112    type Data;
113
114    /// Initializes the assembler with the given slot count
115    ///
116    /// This will likely construct a function prelude and reserve space on the
117    /// stack for slot spills.
118    fn init(m: Mmap, slot_count: usize) -> Self;
119
120    /// Returns an approximate bytes per clause value, used for preallocation
121    fn bytes_per_clause() -> usize {
122        8 // probably wrong!
123    }
124
125    /// Builds a load from memory to a register
126    fn build_load(&mut self, dst_reg: u8, src_mem: u32);
127
128    /// Builds a store from a register to a memory location
129    fn build_store(&mut self, dst_mem: u32, src_reg: u8);
130
131    /// Copies the given input to `out_reg`
132    fn build_input(&mut self, out_reg: u8, src_arg: u32);
133
134    /// Writes the argument register to the output
135    fn build_output(&mut self, arg_reg: u8, out_index: u32);
136
137    /// Copies a register
138    fn build_copy(&mut self, out_reg: u8, lhs_reg: u8);
139
140    /// Unary negation
141    fn build_neg(&mut self, out_reg: u8, lhs_reg: u8);
142
143    /// Absolute value
144    fn build_abs(&mut self, out_reg: u8, lhs_reg: u8);
145
146    /// Reciprocal (1 / `lhs_reg`)
147    fn build_recip(&mut self, out_reg: u8, lhs_reg: u8);
148
149    /// Square root
150    fn build_sqrt(&mut self, out_reg: u8, lhs_reg: u8);
151
152    /// Sine
153    fn build_sin(&mut self, out_reg: u8, lhs_reg: u8);
154
155    /// Cosine
156    fn build_cos(&mut self, out_reg: u8, lhs_reg: u8);
157
158    /// Tangent
159    fn build_tan(&mut self, out_reg: u8, lhs_reg: u8);
160
161    /// Arcsine
162    fn build_asin(&mut self, out_reg: u8, lhs_reg: u8);
163
164    /// Arccosine
165    fn build_acos(&mut self, out_reg: u8, lhs_reg: u8);
166
167    /// Arctangent
168    fn build_atan(&mut self, out_reg: u8, lhs_reg: u8);
169
170    /// Exponent
171    fn build_exp(&mut self, out_reg: u8, lhs_reg: u8);
172
173    /// Natural log
174    fn build_ln(&mut self, out_reg: u8, lhs_reg: u8);
175
176    /// Less than
177    fn build_compare(&mut self, out_reg: u8, lhs_reg: u8, rhs_reg: u8);
178
179    /// Square
180    ///
181    /// This has a default implementation, but can be overloaded for efficiency;
182    /// for example, in interval arithmetic, we benefit from knowing that both
183    /// values are the same.
184    fn build_square(&mut self, out_reg: u8, lhs_reg: u8) {
185        self.build_mul(out_reg, lhs_reg, lhs_reg)
186    }
187
188    /// Arithmetic floor
189    fn build_floor(&mut self, out_reg: u8, lhs_reg: u8);
190
191    /// Arithmetic ceiling
192    fn build_ceil(&mut self, out_reg: u8, lhs_reg: u8);
193
194    /// Rounding
195    fn build_round(&mut self, out_reg: u8, lhs_reg: u8);
196
197    /// Logical not
198    fn build_not(&mut self, out_reg: u8, lhs_reg: u8);
199
200    /// Logical and (short-circuiting)
201    fn build_and(&mut self, out_reg: u8, lhs_reg: u8, rhs_reg: u8);
202
203    /// Logical or (short-circuiting)
204    fn build_or(&mut self, out_reg: u8, lhs_reg: u8, rhs_reg: u8);
205
206    /// Addition
207    fn build_add(&mut self, out_reg: u8, lhs_reg: u8, rhs_reg: u8);
208
209    /// Subtraction
210    fn build_sub(&mut self, out_reg: u8, lhs_reg: u8, rhs_reg: u8);
211
212    /// Multiplication
213    fn build_mul(&mut self, out_reg: u8, lhs_reg: u8, rhs_reg: u8);
214
215    /// Division
216    fn build_div(&mut self, out_reg: u8, lhs_reg: u8, rhs_reg: u8);
217
218    /// Four-quadrant arctangent
219    fn build_atan2(&mut self, out_reg: u8, lhs_reg: u8, rhs_reg: u8);
220
221    /// Maximum of two values
222    ///
223    /// In a tracing evaluator, this function must also write to the `choices`
224    /// array and may set `simplify` if one branch is always taken.
225    fn build_max(&mut self, out_reg: u8, lhs_reg: u8, rhs_reg: u8);
226
227    /// Minimum of two values
228    ///
229    /// In a tracing evaluator, this function must also write to the `choices`
230    /// array and may set `simplify` if one branch is always taken.
231    fn build_min(&mut self, out_reg: u8, lhs_reg: u8, rhs_reg: u8);
232
233    /// Modulo of two values (least non-negative remainder)
234    fn build_mod(&mut self, out_reg: u8, lhs_reg: u8, rhs_reg: u8);
235
236    // Special-case functions for immediates.  In some cases, you can be more
237    // efficient if you know that an argument is an immediate (for example, both
238    // values in the interval will be the same, and it will have no gradients).
239
240    /// Builds a addition (immediate + register)
241    ///
242    /// This has a default implementation, but can be overloaded for efficiency
243    fn build_add_imm(&mut self, out_reg: u8, lhs_reg: u8, imm: f32) {
244        let imm = self.load_imm(imm);
245        self.build_add(out_reg, lhs_reg, imm);
246    }
247    /// Builds a subtraction (immediate − register)
248    ///
249    /// This has a default implementation, but can be overloaded for efficiency
250    fn build_sub_imm_reg(&mut self, out_reg: u8, arg: u8, imm: f32) {
251        let imm = self.load_imm(imm);
252        self.build_sub(out_reg, imm, arg);
253    }
254    /// Builds a subtraction (register − immediate)
255    ///
256    /// This has a default implementation, but can be overloaded for efficiency
257    fn build_sub_reg_imm(&mut self, out_reg: u8, arg: u8, imm: f32) {
258        let imm = self.load_imm(imm);
259        self.build_sub(out_reg, arg, imm);
260    }
261    /// Builds a multiplication (register × immediate)
262    ///
263    /// This has a default implementation, but can be overloaded for efficiency
264    fn build_mul_imm(&mut self, out_reg: u8, lhs_reg: u8, imm: f32) {
265        let imm = self.load_imm(imm);
266        self.build_mul(out_reg, lhs_reg, imm);
267    }
268
269    /// Loads an immediate into a register, returning that register
270    fn load_imm(&mut self, imm: f32) -> u8;
271
272    /// Finalize the assembly code, returning a memory-mapped region
273    fn finalize(self) -> Result<Mmap, DynasmError>;
274}
275
276/// Trait defining SIMD width
277pub trait SimdSize {
278    /// Number of elements processed in a single iteration
279    ///
280    /// This value is used when checking array sizes, as we want to be sure to
281    /// pass the JIT code an appropriately sized array.
282    const SIMD_SIZE: usize;
283}
284
285/////////////////////////////////////////////////////////////////////////////////////////
286
287pub(crate) struct AssemblerData<T> {
288    ops: MmapAssembler,
289
290    /// Current offset of the stack pointer, in bytes
291    mem_offset: usize,
292
293    /// Set to true if we have saved certain callee-saved registers
294    ///
295    /// These registers are only modified in function calls, so normally we
296    /// don't save them.
297    saved_callee_regs: bool,
298
299    _p: std::marker::PhantomData<*const T>,
300}
301
302impl<T> AssemblerData<T> {
303    fn new(mmap: Mmap) -> Self {
304        Self {
305            ops: MmapAssembler::from(mmap),
306            mem_offset: 0,
307            saved_callee_regs: false,
308            _p: std::marker::PhantomData,
309        }
310    }
311
312    fn prepare_stack(&mut self, slot_count: usize, stack_size: usize) {
313        // We always use the stack, if only to store callee-saved registers
314        let mem = slot_count.saturating_sub(REGISTER_LIMIT)
315            * std::mem::size_of::<T>()
316            + stack_size;
317
318        // Round up to the nearest multiple of 16 bytes, for alignment
319        self.mem_offset = mem.next_multiple_of(16);
320        self.push_stack();
321    }
322
323    fn stack_pos(&self, slot: u32) -> u32 {
324        assert!(slot >= REGISTER_LIMIT as u32);
325        (slot - REGISTER_LIMIT as u32) * std::mem::size_of::<T>() as u32
326    }
327}
328
329#[cfg(target_arch = "x86_64")]
330impl<T> AssemblerData<T> {
331    fn push_stack(&mut self) {
332        dynasm!(self.ops
333            ; sub rsp, self.mem_offset as i32
334        );
335    }
336
337    fn finalize(mut self) -> Result<Mmap, DynasmError> {
338        dynasm!(self.ops
339            ; add rsp, self.mem_offset as i32
340            ; pop rbp
341            ; vzeroupper
342            ; ret
343        );
344        self.ops.finalize()
345    }
346}
347
348#[cfg(target_arch = "aarch64")]
349#[allow(clippy::unnecessary_cast)] // dynasm-rs#106
350impl<T> AssemblerData<T> {
351    fn push_stack(&mut self) {
352        if self.mem_offset < 4096 {
353            dynasm!(self.ops
354                ; sub sp, sp, self.mem_offset as u32
355            );
356        } else if self.mem_offset < 65536 {
357            dynasm!(self.ops
358                ; mov w28, self.mem_offset as u32
359                ; sub sp, sp, w28
360            );
361        } else {
362            panic!("invalid mem offset: {} is too large", self.mem_offset);
363        }
364    }
365
366    fn finalize(mut self) -> Result<Mmap, DynasmError> {
367        // Fix up the stack
368        if self.mem_offset < 4096 {
369            dynasm!(self.ops
370                ; add sp, sp, self.mem_offset as u32
371            );
372        } else if self.mem_offset < 65536 {
373            dynasm!(self.ops
374                ; mov w9, self.mem_offset as u32
375                ; add sp, sp, w9
376            );
377        } else {
378            panic!("invalid mem offset: {}", self.mem_offset);
379        }
380
381        dynasm!(self.ops
382            ; ret
383        );
384        self.ops.finalize()
385    }
386}
387
388////////////////////////////////////////////////////////////////////////////////
389
390#[cfg(target_arch = "x86_64")]
391type Relocation = dynasmrt::x64::X64Relocation;
392
393#[cfg(target_arch = "aarch64")]
394type Relocation = dynasmrt::aarch64::Aarch64Relocation;
395
396struct MmapAssembler {
397    mmap: MmapWriter,
398
399    global_labels: [Option<AssemblyOffset>; 26],
400    local_labels: [Option<AssemblyOffset>; 26],
401
402    global_relocs: arrayvec::ArrayVec<(PatchLoc<Relocation>, u8), 2>,
403    local_relocs: arrayvec::ArrayVec<(PatchLoc<Relocation>, u8), 8>,
404}
405
406impl Extend<u8> for MmapAssembler {
407    fn extend<T>(&mut self, iter: T)
408    where
409        T: IntoIterator<Item = u8>,
410    {
411        for c in iter.into_iter() {
412            self.push(c);
413        }
414    }
415}
416
417impl<'a> Extend<&'a u8> for MmapAssembler {
418    fn extend<T>(&mut self, iter: T)
419    where
420        T: IntoIterator<Item = &'a u8>,
421    {
422        for c in iter.into_iter() {
423            self.push(*c);
424        }
425    }
426}
427
428impl DynasmApi for MmapAssembler {
429    #[inline(always)]
430    fn offset(&self) -> AssemblyOffset {
431        AssemblyOffset(self.mmap.len())
432    }
433
434    #[inline(always)]
435    fn push(&mut self, byte: u8) {
436        self.mmap.push(byte);
437    }
438
439    #[inline(always)]
440    fn align(&mut self, alignment: usize, with: u8) {
441        let offset = self.offset().0 % alignment;
442        if offset != 0 {
443            for _ in offset..alignment {
444                self.push(with);
445            }
446        }
447    }
448
449    #[inline(always)]
450    fn push_u32(&mut self, value: u32) {
451        for b in value.to_le_bytes() {
452            self.mmap.push(b);
453        }
454    }
455}
456
457/// This is a very limited implementation of the labels API.  Compared to the
458/// standard labels API, it has the following limitations:
459///
460/// - Labels must be a single character
461/// - Local labels must be committed before they're reused, using `commit_local`
462/// - Only 8 local jumps are available at any given time; this is reset when
463///   `commit_local` is called.  (if this becomes problematic, it can be
464///   increased by tweaking the size of `local_relocs: ArrayVec<..., 8>`.
465///
466/// In exchange for these limitations, it allocates no memory at runtime, and all
467/// label lookups are done in constant time.
468///
469/// However, it still has overhead compared to computing the jumps by hand;
470/// this overhead was roughly 5% in one unscientific test.
471impl DynasmLabelApi for MmapAssembler {
472    type Relocation = Relocation;
473
474    fn local_label(&mut self, name: &'static str) {
475        if name.len() != 1 {
476            panic!("local label must be a single character");
477        }
478        let c = name.as_bytes()[0].wrapping_sub(b'A');
479        if c >= 26 {
480            panic!("Invalid label {name}, must be A-Z");
481        }
482        if self.local_labels[c as usize].is_some() {
483            panic!("duplicate local label {name}");
484        }
485
486        self.local_labels[c as usize] = Some(self.offset());
487    }
488    fn global_label(&mut self, name: &'static str) {
489        if name.len() != 1 {
490            panic!("local label must be a single character");
491        }
492        let c = name.as_bytes()[0].wrapping_sub(b'A');
493        if c >= 26 {
494            panic!("Invalid label {name}, must be A-Z");
495        }
496        if self.global_labels[c as usize].is_some() {
497            panic!("duplicate global label {name}");
498        }
499
500        self.global_labels[c as usize] = Some(self.offset());
501    }
502    fn dynamic_label(&mut self, _id: DynamicLabel) {
503        panic!("dynamic labels are not supported");
504    }
505    fn global_relocation(
506        &mut self,
507        name: &'static str,
508        target_offset: isize,
509        field_offset: u8,
510        ref_offset: u8,
511        kind: Relocation,
512    ) {
513        let location = self.offset();
514        if name.len() != 1 {
515            panic!("local label must be a single character");
516        }
517        let c = name.as_bytes()[0].wrapping_sub(b'A');
518        if c >= 26 {
519            panic!("Invalid label {name}, must be A-Z");
520        }
521        self.global_relocs.push((
522            PatchLoc::new(
523                location,
524                target_offset,
525                field_offset,
526                ref_offset,
527                kind,
528            ),
529            c,
530        ));
531    }
532    fn dynamic_relocation(
533        &mut self,
534        _id: DynamicLabel,
535        _target_offset: isize,
536        _field_offset: u8,
537        _ref_offset: u8,
538        _kind: Relocation,
539    ) {
540        panic!("dynamic relocations are not supported");
541    }
542    fn forward_relocation(
543        &mut self,
544        name: &'static str,
545        target_offset: isize,
546        field_offset: u8,
547        ref_offset: u8,
548        kind: Relocation,
549    ) {
550        if name.len() != 1 {
551            panic!("local label must be a single character");
552        }
553        let c = name.as_bytes()[0].wrapping_sub(b'A');
554        if c >= 26 {
555            panic!("Invalid label {name}, must be A-Z");
556        }
557        if self.local_labels[c as usize].is_some() {
558            panic!("invalid forward relocation: {name} already exists!");
559        }
560        let location = self.offset();
561        self.local_relocs.push((
562            PatchLoc::new(
563                location,
564                target_offset,
565                field_offset,
566                ref_offset,
567                kind,
568            ),
569            c,
570        ));
571    }
572    fn backward_relocation(
573        &mut self,
574        name: &'static str,
575        target_offset: isize,
576        field_offset: u8,
577        ref_offset: u8,
578        kind: Relocation,
579    ) {
580        if name.len() != 1 {
581            panic!("local label must be a single character");
582        }
583        let c = name.as_bytes()[0].wrapping_sub(b'A');
584        if c >= 26 {
585            panic!("Invalid label {name}, must be A-Z");
586        }
587        if self.local_labels[c as usize].is_none() {
588            panic!("invalid backward relocation: {name} does not exist");
589        }
590        let location = self.offset();
591        self.local_relocs.push((
592            PatchLoc::new(
593                location,
594                target_offset,
595                field_offset,
596                ref_offset,
597                kind,
598            ),
599            c,
600        ));
601    }
602    fn value_relocation(
603        &mut self,
604        _target: usize,
605        _field_offset: u8,
606        _ref_offset: u8,
607        _kind: Relocation,
608    ) {
609        panic!("bare relocations not implemented");
610    }
611}
612
613impl MmapAssembler {
614    /// Applies all local relocations, clearing the `local_relocs` array
615    ///
616    /// This should be called after any function which uses local labels.
617    fn commit_local(&mut self) -> Result<(), DynasmError> {
618        let baseaddr = self.mmap.as_ptr() as usize;
619
620        for (loc, label) in self.local_relocs.take() {
621            let target =
622                self.local_labels[label as usize].expect("invalid local label");
623            let buf = &mut self.mmap.as_mut_slice()[loc.range(0)];
624            if loc.patch(buf, baseaddr, target.0).is_err() {
625                return Err(DynasmError::ImpossibleRelocation(
626                    TargetKind::Local("oh no"),
627                ));
628            }
629        }
630        self.local_labels = [None; 26];
631        Ok(())
632    }
633
634    fn finalize(mut self) -> Result<Mmap, DynasmError> {
635        self.commit_local()?;
636
637        let baseaddr = self.mmap.as_ptr() as usize;
638        for (loc, label) in self.global_relocs.take() {
639            let target =
640                self.global_labels.get(label as usize).unwrap().unwrap();
641            let buf = &mut self.mmap.as_mut_slice()[loc.range(0)];
642            if loc.patch(buf, baseaddr, target.0).is_err() {
643                return Err(DynasmError::ImpossibleRelocation(
644                    TargetKind::Global("oh no"),
645                ));
646            }
647        }
648
649        Ok(self.mmap.finalize())
650    }
651}
652
653impl From<Mmap> for MmapAssembler {
654    fn from(mmap: Mmap) -> Self {
655        Self {
656            mmap: MmapWriter::from(mmap),
657            global_labels: [None; 26],
658            local_labels: [None; 26],
659            global_relocs: Default::default(),
660            local_relocs: Default::default(),
661        }
662    }
663}
664
665/////////////////////////////////////////////////////////////////////////////////////////
666
667fn build_asm_fn_with_storage<A: Assembler>(
668    t: &VmData<REGISTER_LIMIT>,
669    mut s: Mmap,
670) -> Mmap {
671    let size_estimate = t.len() * A::bytes_per_clause();
672    if size_estimate > 2 * s.capacity() {
673        s = Mmap::new(size_estimate).expect("failed to build mmap")
674    }
675
676    let mut asm = A::init(s, t.slot_count());
677
678    for op in t.iter_asm() {
679        match op {
680            RegOp::Load(reg, mem) => {
681                asm.build_load(reg, mem);
682            }
683            RegOp::Store(reg, mem) => {
684                asm.build_store(mem, reg);
685            }
686            RegOp::Input(out, i) => {
687                asm.build_input(out, i);
688            }
689            RegOp::Output(arg, i) => {
690                asm.build_output(arg, i);
691            }
692            RegOp::NegReg(out, arg) => {
693                asm.build_neg(out, arg);
694            }
695            RegOp::AbsReg(out, arg) => {
696                asm.build_abs(out, arg);
697            }
698            RegOp::RecipReg(out, arg) => {
699                asm.build_recip(out, arg);
700            }
701            RegOp::SqrtReg(out, arg) => {
702                asm.build_sqrt(out, arg);
703            }
704            RegOp::SinReg(out, arg) => {
705                asm.build_sin(out, arg);
706            }
707            RegOp::CosReg(out, arg) => {
708                asm.build_cos(out, arg);
709            }
710            RegOp::TanReg(out, arg) => {
711                asm.build_tan(out, arg);
712            }
713            RegOp::AsinReg(out, arg) => {
714                asm.build_asin(out, arg);
715            }
716            RegOp::AcosReg(out, arg) => {
717                asm.build_acos(out, arg);
718            }
719            RegOp::AtanReg(out, arg) => {
720                asm.build_atan(out, arg);
721            }
722            RegOp::ExpReg(out, arg) => {
723                asm.build_exp(out, arg);
724            }
725            RegOp::LnReg(out, arg) => {
726                asm.build_ln(out, arg);
727            }
728            RegOp::CopyReg(out, arg) => {
729                asm.build_copy(out, arg);
730            }
731            RegOp::SquareReg(out, arg) => {
732                asm.build_square(out, arg);
733            }
734            RegOp::FloorReg(out, arg) => {
735                asm.build_floor(out, arg);
736            }
737            RegOp::CeilReg(out, arg) => {
738                asm.build_ceil(out, arg);
739            }
740            RegOp::RoundReg(out, arg) => {
741                asm.build_round(out, arg);
742            }
743            RegOp::NotReg(out, arg) => {
744                asm.build_not(out, arg);
745            }
746            RegOp::AddRegReg(out, lhs, rhs) => {
747                asm.build_add(out, lhs, rhs);
748            }
749            RegOp::MulRegReg(out, lhs, rhs) => {
750                asm.build_mul(out, lhs, rhs);
751            }
752            RegOp::DivRegReg(out, lhs, rhs) => {
753                asm.build_div(out, lhs, rhs);
754            }
755            RegOp::AtanRegReg(out, lhs, rhs) => {
756                asm.build_atan2(out, lhs, rhs);
757            }
758            RegOp::SubRegReg(out, lhs, rhs) => {
759                asm.build_sub(out, lhs, rhs);
760            }
761            RegOp::MinRegReg(out, lhs, rhs) => {
762                asm.build_min(out, lhs, rhs);
763            }
764            RegOp::MaxRegReg(out, lhs, rhs) => {
765                asm.build_max(out, lhs, rhs);
766            }
767            RegOp::AddRegImm(out, arg, imm) => {
768                asm.build_add_imm(out, arg, imm);
769            }
770            RegOp::MulRegImm(out, arg, imm) => {
771                asm.build_mul_imm(out, arg, imm);
772            }
773            RegOp::DivRegImm(out, arg, imm) => {
774                let reg = asm.load_imm(imm);
775                asm.build_div(out, arg, reg);
776            }
777            RegOp::DivImmReg(out, arg, imm) => {
778                let reg = asm.load_imm(imm);
779                asm.build_div(out, reg, arg);
780            }
781            RegOp::AtanRegImm(out, arg, imm) => {
782                let reg = asm.load_imm(imm);
783                asm.build_atan2(out, arg, reg);
784            }
785            RegOp::AtanImmReg(out, arg, imm) => {
786                let reg = asm.load_imm(imm);
787                asm.build_atan2(out, reg, arg);
788            }
789            RegOp::SubImmReg(out, arg, imm) => {
790                asm.build_sub_imm_reg(out, arg, imm);
791            }
792            RegOp::SubRegImm(out, arg, imm) => {
793                asm.build_sub_reg_imm(out, arg, imm);
794            }
795            RegOp::MinRegImm(out, arg, imm) => {
796                let reg = asm.load_imm(imm);
797                asm.build_min(out, arg, reg);
798            }
799            RegOp::MaxRegImm(out, arg, imm) => {
800                let reg = asm.load_imm(imm);
801                asm.build_max(out, arg, reg);
802            }
803            RegOp::ModRegReg(out, lhs, rhs) => {
804                asm.build_mod(out, lhs, rhs);
805            }
806            RegOp::ModRegImm(out, arg, imm) => {
807                let reg = asm.load_imm(imm);
808                asm.build_mod(out, arg, reg);
809            }
810            RegOp::ModImmReg(out, arg, imm) => {
811                let reg = asm.load_imm(imm);
812                asm.build_mod(out, reg, arg);
813            }
814            RegOp::AndRegReg(out, lhs, rhs) => {
815                asm.build_and(out, lhs, rhs);
816            }
817            RegOp::AndRegImm(out, arg, imm) => {
818                let reg = asm.load_imm(imm);
819                asm.build_and(out, arg, reg);
820            }
821            RegOp::OrRegReg(out, lhs, rhs) => {
822                asm.build_or(out, lhs, rhs);
823            }
824            RegOp::OrRegImm(out, arg, imm) => {
825                let reg = asm.load_imm(imm);
826                asm.build_or(out, arg, reg);
827            }
828            RegOp::CopyImm(out, imm) => {
829                let reg = asm.load_imm(imm);
830                asm.build_copy(out, reg);
831            }
832            RegOp::CompareRegReg(out, lhs, rhs) => {
833                asm.build_compare(out, lhs, rhs);
834            }
835            RegOp::CompareRegImm(out, arg, imm) => {
836                let reg = asm.load_imm(imm);
837                asm.build_compare(out, arg, reg);
838            }
839            RegOp::CompareImmReg(out, arg, imm) => {
840                let reg = asm.load_imm(imm);
841                asm.build_compare(out, reg, arg);
842            }
843        }
844    }
845
846    asm.finalize().expect("failed to build JIT function")
847    // JIT execute mode is restored here when the _guard is dropped
848}
849
850/// Function for use with a JIT evaluator
851#[derive(Clone)]
852pub struct JitFunction(GenericVmFunction<REGISTER_LIMIT>);
853
854impl JitFunction {
855    fn tracing_tape<A: Assembler>(
856        &self,
857        storage: Mmap,
858    ) -> JitTracingFn<A::Data> {
859        let f = build_asm_fn_with_storage::<A>(self.0.data(), storage);
860        let ptr = f.as_ptr();
861        JitTracingFn {
862            mmap: f.into(),
863            vars: self.0.data().vars.clone(),
864            choice_count: self.0.choice_count(),
865            output_count: self.0.output_count(),
866            fn_trace: unsafe {
867                std::mem::transmute::<
868                    *const std::ffi::c_void,
869                    JitTracingFnPointer<A::Data>,
870                >(ptr)
871            },
872        }
873    }
874    fn bulk_tape<A: Assembler>(&self, storage: Mmap) -> JitBulkFn<A::Data> {
875        let f = build_asm_fn_with_storage::<A>(self.0.data(), storage);
876        let ptr = f.as_ptr();
877        JitBulkFn {
878            mmap: f.into(),
879            output_count: self.0.output_count(),
880            vars: self.0.data().vars.clone(),
881            fn_bulk: unsafe {
882                std::mem::transmute::<
883                    *const std::ffi::c_void,
884                    JitBulkFnPointer<A::Data>,
885                >(ptr)
886            },
887        }
888    }
889}
890
891impl Function for JitFunction {
892    type Trace = VmTrace;
893    type Storage = VmData<REGISTER_LIMIT>;
894    type Workspace = VmWorkspace<REGISTER_LIMIT>;
895
896    type TapeStorage = Mmap;
897
898    type IntervalEval = JitIntervalEval;
899    type PointEval = JitPointEval;
900    type FloatSliceEval = JitFloatSliceEval;
901    type GradSliceEval = JitGradSliceEval;
902
903    #[inline]
904    fn point_tape(&self, storage: Mmap) -> JitTracingFn<f32> {
905        self.tracing_tape::<point::PointAssembler>(storage)
906    }
907
908    #[inline]
909    fn interval_tape(&self, storage: Mmap) -> JitTracingFn<Interval> {
910        self.tracing_tape::<interval::IntervalAssembler>(storage)
911    }
912
913    #[inline]
914    fn float_slice_tape(&self, storage: Mmap) -> JitBulkFn<f32> {
915        self.bulk_tape::<float_slice::FloatSliceAssembler>(storage)
916    }
917
918    #[inline]
919    fn grad_slice_tape(&self, storage: Mmap) -> JitBulkFn<Grad> {
920        self.bulk_tape::<grad_slice::GradSliceAssembler>(storage)
921    }
922
923    #[inline]
924    fn simplify(
925        &self,
926        trace: &Self::Trace,
927        storage: Self::Storage,
928        workspace: &mut Self::Workspace,
929    ) -> Result<Self, BadTrace> {
930        self.0.simplify(trace, storage, workspace).map(JitFunction)
931    }
932
933    #[inline]
934    fn recycle(self) -> Option<Self::Storage> {
935        self.0.recycle()
936    }
937
938    #[inline]
939    fn size(&self) -> usize {
940        self.0.size()
941    }
942
943    #[inline]
944    fn vars(&self) -> &VarMap {
945        self.0.vars()
946    }
947
948    #[inline]
949    fn can_simplify(&self) -> bool {
950        self.0.choice_count() > 0
951    }
952
953    #[inline]
954    fn output_count(&self) -> usize {
955        self.0.output_count()
956    }
957}
958
959impl RenderHints for JitFunction {
960    fn tile_sizes_3d() -> TileSizes {
961        TileSizes::new(&[64, 16, 8]).unwrap()
962    }
963
964    fn tile_sizes_2d() -> TileSizes {
965        TileSizes::new(&[128, 16]).unwrap()
966    }
967
968    fn simplify_tree_during_meshing(d: usize) -> bool {
969        // Unscientifically selected, but similar to tile_sizes_3d
970        d % 8 == 4
971    }
972}
973
974impl MathFunction for JitFunction {
975    fn new(ctx: &Context, nodes: &[Node]) -> Result<Self, BadNode> {
976        GenericVmFunction::new(ctx, nodes).map(JitFunction)
977    }
978}
979
980impl From<GenericVmFunction<REGISTER_LIMIT>> for JitFunction {
981    fn from(v: GenericVmFunction<REGISTER_LIMIT>) -> Self {
982        Self(v)
983    }
984}
985
986impl<'a> From<&'a JitFunction> for &'a GenericVmFunction<REGISTER_LIMIT> {
987    fn from(v: &'a JitFunction) -> Self {
988        &v.0
989    }
990}
991
992////////////////////////////////////////////////////////////////////////////////
993
994// Selects the calling convention based on platform; this is forward-looking for
995// eventual x86 Windows support, where we still want to use the sysv64 calling
996// convention.
997/// Macro to build a function type with a `extern "sysv64"` calling convention
998///
999/// This is selected at compile time, based on `target_arch`
1000#[cfg(target_arch = "x86_64")]
1001macro_rules! jit_fn {
1002    (unsafe fn($($args:tt)*)) => {
1003        unsafe extern "sysv64" fn($($args)*)
1004    };
1005}
1006
1007/// Macro to build a function type with the `extern "C"` calling convention
1008///
1009/// This is selected at compile time, based on `target_arch`
1010#[cfg(target_arch = "aarch64")]
1011macro_rules! jit_fn {
1012    (unsafe fn($($args:tt)*)) => {
1013        unsafe extern "C" fn($($args)*)
1014    };
1015}
1016
1017////////////////////////////////////////////////////////////////////////////////
1018
1019/// Evaluator for a JIT-compiled tracing function
1020///
1021/// Users are unlikely to use this directly, but it's public because it's an
1022/// associated type on [`JitFunction`].
1023struct JitTracingEval<T> {
1024    choices: VmTrace,
1025    out: Vec<T>,
1026}
1027
1028impl<T> Default for JitTracingEval<T> {
1029    fn default() -> Self {
1030        Self {
1031            choices: VmTrace::default(),
1032            out: Vec::default(),
1033        }
1034    }
1035}
1036
1037/// Typedef for a tracing function pointer
1038pub type JitTracingFnPointer<T> = jit_fn!(
1039    unsafe fn(
1040        *const T, // vars
1041        *mut u8,  // choices
1042        *mut u8,  // simplify (single boolean)
1043        *mut T,   // output (array)
1044    )
1045);
1046
1047/// Handle to an owned function pointer for tracing evaluation
1048#[derive(Clone)]
1049pub struct JitTracingFn<T> {
1050    mmap: Arc<Mmap>,
1051    choice_count: usize,
1052    output_count: usize,
1053    vars: Arc<VarMap>,
1054    fn_trace: JitTracingFnPointer<T>,
1055}
1056
1057impl<T: Clone> Tape for JitTracingFn<T> {
1058    type Storage = Mmap;
1059    fn recycle(self) -> Option<Self::Storage> {
1060        Arc::into_inner(self.mmap)
1061    }
1062
1063    fn vars(&self) -> &VarMap {
1064        &self.vars
1065    }
1066
1067    fn output_count(&self) -> usize {
1068        self.output_count
1069    }
1070}
1071
1072// SAFETY: there is no mutable state in a `JitTracingFn`, and the pointer
1073// inside of it points to its own `Mmap`, which is owned by an `Arc`
1074unsafe impl<T> Send for JitTracingFn<T> {}
1075unsafe impl<T> Sync for JitTracingFn<T> {}
1076
1077impl<T: From<f32> + Clone> JitTracingEval<T> {
1078    /// Evaluates a single point, capturing an evaluation trace
1079    fn eval(
1080        &mut self,
1081        tape: &JitTracingFn<T>,
1082        vars: &[T],
1083    ) -> (&[T], Option<&VmTrace>) {
1084        let mut simplify = 0;
1085        self.choices.resize(tape.choice_count, Choice::Unknown);
1086        self.choices.fill(Choice::Unknown);
1087        self.out.resize(tape.output_count, f32::NAN.into());
1088        self.out.fill(f32::NAN.into());
1089        unsafe {
1090            (tape.fn_trace)(
1091                vars.as_ptr(),
1092                self.choices.as_mut_ptr() as *mut u8,
1093                &mut simplify,
1094                self.out.as_mut_ptr(),
1095            )
1096        };
1097
1098        (
1099            &self.out,
1100            if simplify != 0 {
1101                Some(&self.choices)
1102            } else {
1103                None
1104            },
1105        )
1106    }
1107}
1108
1109/// JIT-based tracing evaluator for interval values
1110#[derive(Default)]
1111pub struct JitIntervalEval(JitTracingEval<Interval>);
1112impl TracingEvaluator for JitIntervalEval {
1113    type Data = Interval;
1114    type Tape = JitTracingFn<Interval>;
1115    type Trace = VmTrace;
1116    type TapeStorage = Mmap;
1117
1118    #[inline]
1119    fn eval(
1120        &mut self,
1121        tape: &Self::Tape,
1122        vars: &[Self::Data],
1123    ) -> Result<(&[Self::Data], Option<&Self::Trace>), TracingEvalError> {
1124        tape.vars().check_tracing_arguments(vars)?;
1125        Ok(self.0.eval(tape, vars))
1126    }
1127}
1128
1129/// JIT-based tracing evaluator for point values
1130#[derive(Default)]
1131pub struct JitPointEval(JitTracingEval<f32>);
1132impl TracingEvaluator for JitPointEval {
1133    type Data = f32;
1134    type Tape = JitTracingFn<f32>;
1135    type Trace = VmTrace;
1136    type TapeStorage = Mmap;
1137
1138    #[inline]
1139    fn eval(
1140        &mut self,
1141        tape: &Self::Tape,
1142        vars: &[Self::Data],
1143    ) -> Result<(&[Self::Data], Option<&Self::Trace>), TracingEvalError> {
1144        tape.vars().check_tracing_arguments(vars)?;
1145        Ok(self.0.eval(tape, vars))
1146    }
1147}
1148
1149////////////////////////////////////////////////////////////////////////////////
1150
1151/// Typedef for a bulk function pointer
1152pub type JitBulkFnPointer<T> = jit_fn!(
1153    unsafe fn(
1154        *const *const T, // vars
1155        *const *mut T,   // out
1156        u64,             // size
1157    )
1158);
1159
1160/// Handle to an owned function pointer for bulk evaluation
1161#[derive(Clone)]
1162pub struct JitBulkFn<T> {
1163    mmap: Arc<Mmap>,
1164    vars: Arc<VarMap>,
1165    output_count: usize,
1166    fn_bulk: JitBulkFnPointer<T>,
1167}
1168
1169impl<T: Clone> Tape for JitBulkFn<T> {
1170    type Storage = Mmap;
1171    fn recycle(self) -> Option<Self::Storage> {
1172        Arc::into_inner(self.mmap)
1173    }
1174
1175    fn vars(&self) -> &VarMap {
1176        &self.vars
1177    }
1178
1179    fn output_count(&self) -> usize {
1180        self.output_count
1181    }
1182}
1183
1184/// Maximum SIMD width for any type, checked at runtime (alas)
1185///
1186/// We can't use `T::SIMD_SIZE` directly here due to Rust limitations. Instead we
1187/// hard-code a maximum SIMD size along with an assertion that should be
1188/// optimized out; we can't use a constant assertion here due to the same
1189/// compiler limitations.
1190const MAX_SIMD_WIDTH: usize = 8;
1191
1192/// Bulk evaluator for JIT functions
1193struct JitBulkEval<T> {
1194    /// Array of pointers used when calling into the JIT function
1195    input_ptrs: Vec<*const T>,
1196
1197    /// Array of pointers used when calling into the JIT function
1198    output_ptrs: Vec<*mut T>,
1199
1200    /// Scratch array for evaluation of less-than-SIMD-size slices
1201    scratch: Vec<[T; MAX_SIMD_WIDTH]>,
1202
1203    /// Output arrays, written to during evaluation
1204    out: Vec<Vec<T>>,
1205}
1206
1207// SAFETY: the pointers in `JitBulkEval` are transient and only scoped to a
1208// single evaluation.
1209unsafe impl<T> Sync for JitBulkEval<T> {}
1210unsafe impl<T> Send for JitBulkEval<T> {}
1211
1212impl<T> Default for JitBulkEval<T> {
1213    fn default() -> Self {
1214        Self {
1215            out: vec![],
1216            scratch: vec![],
1217            input_ptrs: vec![],
1218            output_ptrs: vec![],
1219        }
1220    }
1221}
1222
1223// SAFETY: there is no mutable state in a `JitBulkFn`, and the pointer
1224// inside of it points to its own `Mmap`, which is owned by an `Arc`
1225unsafe impl<T> Send for JitBulkFn<T> {}
1226unsafe impl<T> Sync for JitBulkFn<T> {}
1227
1228impl<T: From<f32> + Copy + SimdSize> JitBulkEval<T> {
1229    /// Evaluate multiple points
1230    fn eval<V: std::ops::Deref<Target = [T]>>(
1231        &mut self,
1232        tape: &JitBulkFn<T>,
1233        vars: &[V],
1234    ) -> BulkOutput<'_, T> {
1235        let n = vars.first().map(|v| v.deref().len()).unwrap_or(0);
1236
1237        self.out.resize_with(tape.output_count(), Vec::new);
1238        for o in &mut self.out {
1239            o.resize(n.max(T::SIMD_SIZE), f32::NAN.into());
1240            o.fill(f32::NAN.into());
1241        }
1242
1243        // Special case for when we have fewer items than the native SIMD size,
1244        // in which case the input slices can't be used as workspace (because
1245        // they are not valid for the entire range of values read in assembly)
1246        if n < T::SIMD_SIZE {
1247            assert!(T::SIMD_SIZE <= MAX_SIMD_WIDTH);
1248
1249            self.scratch
1250                .resize(vars.len(), [f32::NAN.into(); MAX_SIMD_WIDTH]);
1251            for (v, t) in vars.iter().zip(self.scratch.iter_mut()) {
1252                t[0..n].copy_from_slice(v);
1253            }
1254
1255            self.input_ptrs.clear();
1256            self.input_ptrs
1257                .extend(self.scratch[..vars.len()].iter().map(|t| t.as_ptr()));
1258
1259            self.output_ptrs.clear();
1260            self.output_ptrs
1261                .extend(self.out.iter_mut().map(|t| t.as_mut_ptr()));
1262
1263            unsafe {
1264                (tape.fn_bulk)(
1265                    self.input_ptrs.as_ptr(),
1266                    self.output_ptrs.as_ptr(),
1267                    T::SIMD_SIZE as u64,
1268                );
1269            }
1270        } else {
1271            // Our vectorized function only accepts sets of a particular width,
1272            // so we'll find the biggest multiple, then do an extra operation to
1273            // process any remainders.
1274            let m = (n / T::SIMD_SIZE) * T::SIMD_SIZE; // Round down
1275            self.input_ptrs.clear();
1276            self.input_ptrs.extend(vars.iter().map(|v| v.as_ptr()));
1277
1278            self.output_ptrs.clear();
1279            self.output_ptrs
1280                .extend(self.out.iter_mut().map(|v| v.as_mut_ptr()));
1281            unsafe {
1282                (tape.fn_bulk)(
1283                    self.input_ptrs.as_ptr(),
1284                    self.output_ptrs.as_ptr(),
1285                    m as u64,
1286                );
1287            }
1288            // If we weren't given an even multiple of vector width, then we'll
1289            // handle the remaining items by simply evaluating the *last* full
1290            // vector in the array again.
1291            if n != m {
1292                self.input_ptrs.clear();
1293                self.output_ptrs.clear();
1294                unsafe {
1295                    self.input_ptrs.extend(
1296                        vars.iter().map(|v| v.as_ptr().add(n - T::SIMD_SIZE)),
1297                    );
1298                    self.output_ptrs.extend(
1299                        self.out
1300                            .iter_mut()
1301                            .map(|v| v.as_mut_ptr().add(n - T::SIMD_SIZE)),
1302                    );
1303                    (tape.fn_bulk)(
1304                        self.input_ptrs.as_ptr(),
1305                        self.output_ptrs.as_ptr(),
1306                        T::SIMD_SIZE as u64,
1307                    );
1308                }
1309            }
1310        }
1311        BulkOutput::new(&self.out, n)
1312    }
1313}
1314
1315/// JIT-based bulk evaluator for arrays of points, yielding point values
1316#[derive(Default)]
1317pub struct JitFloatSliceEval(JitBulkEval<f32>);
1318impl BulkEvaluator for JitFloatSliceEval {
1319    type Data = f32;
1320    type Tape = JitBulkFn<Self::Data>;
1321    type TapeStorage = Mmap;
1322
1323    #[inline]
1324    fn eval<V: std::ops::Deref<Target = [Self::Data]>>(
1325        &mut self,
1326        tape: &Self::Tape,
1327        vars: &[V],
1328    ) -> Result<BulkOutput<'_, f32>, BulkEvalError> {
1329        tape.vars().check_bulk_arguments(vars)?;
1330        Ok(self.0.eval(tape, vars))
1331    }
1332}
1333
1334/// JIT-based bulk evaluator for arrays of points, yielding gradient values
1335#[derive(Default)]
1336pub struct JitGradSliceEval(JitBulkEval<Grad>);
1337impl BulkEvaluator for JitGradSliceEval {
1338    type Data = Grad;
1339    type Tape = JitBulkFn<Self::Data>;
1340    type TapeStorage = Mmap;
1341
1342    #[inline]
1343    fn eval<V: std::ops::Deref<Target = [Self::Data]>>(
1344        &mut self,
1345        tape: &Self::Tape,
1346        vars: &[V],
1347    ) -> Result<BulkOutput<'_, Grad>, BulkEvalError> {
1348        tape.vars().check_bulk_arguments(vars)?;
1349        Ok(self.0.eval(tape, vars))
1350    }
1351}
1352
1353/// A [`Shape`](fidget_core::shape::Shape) which uses the JIT evaluator
1354pub type JitShape = fidget_core::shape::Shape<JitFunction>;
1355
1356////////////////////////////////////////////////////////////////////////////////
1357
1358#[cfg(test)]
1359mod test {
1360    use super::*;
1361    fidget_core::grad_slice_tests!(JitFunction);
1362    fidget_core::interval_tests!(JitFunction);
1363    fidget_core::float_slice_tests!(JitFunction);
1364    fidget_core::point_tests!(JitFunction);
1365
1366    #[test]
1367    fn test_mmap_expansion() {
1368        let mmap = Mmap::new(0).unwrap();
1369
1370        let mut asm = MmapAssembler::from(mmap);
1371        const COUNT: u32 = 23456; // larger than 1 page (4 KiB)
1372
1373        for i in 0..COUNT {
1374            asm.push_u32(i);
1375        }
1376        let mmap = asm.finalize().unwrap();
1377        let ptr = mmap.as_ptr() as *const u32;
1378        for i in 0..COUNT {
1379            let v = unsafe { *ptr.add(i as usize) };
1380            assert_eq!(v, i);
1381        }
1382    }
1383}