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