From 7678ad545be9f221782c54c6d94773b976e58c88 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 7 Nov 2026 08:48:11 +0000 Subject: [PATCH 1/8] compile speed --- Cargo.toml | 22 ++++++------- DEV.md | 10 +++--- image/Cargo.toml | 19 +++++++++++ {src/image => image/src}/codecs.rs | 2 +- {src/image => image/src}/imageio.rs | 2 +- image/src/lib.rs | 50 +++++++++++++++++++++++++++++ jit/Cargo.toml | 20 ++++++++++++ src/jit.rs => jit/src/lib.rs | 10 +++--- pyproject.toml | 2 +- src/image.rs | 43 +------------------------ src/lib.rs | 2 +- src/pervasive/fold.rs | 2 +- src/pervasive/scan.rs | 2 +- src/program/blocks.rs | 7 ++-- src/program/blocks/items.rs | 2 +- tests/jit.rs | 2 +- 16 files changed, 123 insertions(+), 74 deletions(-) create mode 100644 image/Cargo.toml rename {src/image => image/src}/codecs.rs (97%) rename {src/image => image/src}/imageio.rs (99%) create mode 100644 image/src/lib.rs create mode 100644 jit/Cargo.toml rename src/jit.rs => jit/src/lib.rs (99%) diff --git a/Cargo.toml b/Cargo.toml index 6711ceec..76d6a928 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["cli", "py", "wasm", "xtask"] +members = ["cli", "image", "jit", "py", "wasm", "xtask"] default-members = [".", "cli", "py"] [workspace.metadata.fastfmt] @@ -12,7 +12,7 @@ rust-version = "1.98" license = "Apache-2.0" [workspace.dependencies] -rustymath = { version = ">=0.1.55, <1", default-features = false } +rustymath = { version = ">=0.1.56, <1", default-features = false } rustymath-buffer = ">=0.1.55, <1" pyo3 = ">=0.29.2, <1" pyo3-build-config = ">=0.29.2, <1" @@ -20,6 +20,7 @@ ctrlc = ">=3.5.2, <4" kernmini = ">=0.1.22, <1" # Custom bindings override Emacs Alt-digit prefixes in this fork. rustyline = { git = "https://github.com/AnswerDotAI/rustyline.git", branch = "master", default-features = false, features = ["custom-bindings", "with-file-history"] } +flate2 = { version = ">=1.1.10, <2", default-features = false, features = ["zlib"] } [package] name = "basedpl" @@ -52,6 +53,10 @@ incremental = true # Line tables change with every shifted line and recompile the code below it. debug = false +[profile.quick] +inherits = "release" +lto = "off" + [profile.dist] inherits = "release" lto = "thin" @@ -81,6 +86,7 @@ num-bigint = ">=0.4.6, <0.5" num-traits = ">=0.2.19, <1" half = { version = ">=2.7.1, <3", features = ["num-traits", "bytemuck"] } rustymath.workspace = true +basedpl-image = { path = "image" } strength_reduce = ">=0.2.4, <1" rand = { version = ">=0.10.2, <1", features = ["chacha"] } # AnswerDotAI's fork measures text with a canvas, which works in a Web Worker: plotters-rs/plotters#746. @@ -101,13 +107,10 @@ nix = { version = ">=0.31.3, <1", default-features = false, features = ["user"] [target.'cfg(target_vendor = "apple")'.dependencies] # The Mac build links the zlib that macOS ships, in place of compiling `miniz_oxide`. -flate2 = { version = ">=1.1.10, <2", default-features = false, features = ["zlib"] } +flate2.workspace = true [target.'cfg(not(target_vendor = "apple"))'.dependencies] flate2 = ">=1.1.10, <2" -# Macs decode and encode images through ImageIO instead. -image = { version = ">=0.25.6, <1", default-features = false, features = ["png", "jpeg"] } -png = ">=0.18.1, <1" [target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies] chrono = { version = ">=0.4.45, <0.5", features = ["unstable-locales"] } @@ -115,12 +118,7 @@ icu_normalizer = ">=2, <3" fancy-regex = "0.20" walkdir = ">=2.5.0, <3" globset = ">=0.4.20, <1" -# Cranelift compiles a typed program's loops while BPL runs. The browser build has no JIT. -cranelift-codegen = { version = "0.137", default-features = false, features = ["std", "host-arch"] } -cranelift-frontend = "0.137" -cranelift-jit = "0.137" -cranelift-module = "0.137" -cranelift-native = "0.137" +basedpl-jit = { path = "jit" } # The browser build. `build.rs` sets `cfg(web)` for the same target. [target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] diff --git a/DEV.md b/DEV.md index 0449e485..19a6d394 100644 --- a/DEV.md +++ b/DEV.md @@ -179,7 +179,7 @@ Default workspace members: core, CLI, Python; wasm and xtask build only on reque Profiles: Rust 1.98 or later (algebraic float operations). `test` inherits `release`: opt-level 3, incremental, thin LTO across each crate's codegen units; no debug information, overflow checks or debug assertions → a `debug_assert!` never runs: write a check that matters as an always-on test. `.cargo/config.toml`: `-Zshare-generics` on every target and x86-64-v3 on x86, in `target` entries Cargo joins. `dist`: no incremental compilation; strips symbols. Keep these aligned with rustymath's. -Packaging: `fastws`'s `cargo develop` builds the project-selected profile (`quick` by default), installs the extension, stages `bpl` into the venv. `py/`: PyO3's `extension-module`, link arguments without libpython. Editable installs: `fastws.build_backend` installs metadata and a path to `python/`, no Rust compile → after a standalone `uv sync` or editable pip install, run `cargo develop` (`ws-sync` does after syncing). Wheels: maturin for the extension, `cargo stage` for the Python-free CLI; Linux staging inside the manylinux container. +Packaging: `fastws`'s `cargo develop` builds the project-selected profile, installs the extension, stages `bpl` into the venv. `py/`: PyO3's `extension-module`, link arguments without libpython. Editable installs: `fastws.build_backend` installs metadata and a path to `python/`, no Rust compile → after a standalone `uv sync` or editable pip install, run `cargo develop` (`ws-sync` does after syncing). Wheels: maturin for the extension, `cargo stage` for the Python-free CLI; Linux staging inside the manylinux container. Features: basedpl's crates pass on rustymath's default `native` feature (rustymath's DEV.md): each depends on the crate below with `default-features = false` and makes `native` its own default. Release staging and wheel builds pass `--no-default-features` → released Mac builds omit rustymath's own linear algebra. The Python binding inherits the CLI's pure-Rust frontend dependencies → identical shared dependency features in release builds. `flate2`: the system zlib on Apple targets; elsewhere its Rust backend, `miniz_oxide`. @@ -193,6 +193,8 @@ CI: `Test` runs correctness checks on pushes to main and pull requests. Native c `cargo develop` and `fastws.cargo.run_tests(root, ...)` build the `release` profile, selected by `[tool.fastws] dev-profile = "release"` in `pyproject.toml`, so development and routine tests share one build in `target/release/`. It uses optimization level 3, local ThinLTO (`lto = false`) and incremental compilation. Bare `cargo test` still uses Cargo's built-in `test` profile, which has `release`'s settings but builds again in `target/debug/`; pass `--release` when not using `run_tests`. +For shorter compile feedback, use `cargo check --all-targets --profile quick` or `cargo develop --profile quick`. The optional `quick` profile keeps O3 and incremental compilation but disables LTO. Its cache is separate in `target/quick/`. Default development remains `release`; use the existing release or dist profile for performance comparisons. + The project Cargo configs enable cross-crate generic sharing with `-Zshare-generics=yes`. They set `RUSTC_BOOTSTRAP=1` for Cargo's compiler processes, without changing the shell or requiring nightly. The tested toolchain is rustc 1.99.1 (`58cf8f9c35b355199727627d07ae34a4e2846d67`), LLVM 23.1.1. Keep the existing Rust minimum and toolchain-selection policy. The unstable switch should be checked when upgrading the compiler. The `dist` profile owns authoritative x86 speed gates: O3, explicit cross-crate `lto = "thin"`, 16 codegen units, nonincremental. Routine `release`/`test` stays local ThinLTO. Run `cargo test --profile dist --test speed` in either repo; use `ship-rs-build --profile dist` for the Python wheel and run its existing speed test; build Node budgets with `cargo wasm --profile dist --out DIR`, then `speed_costs(wasm=DIR)`. Syd1 targets were recalibrated from actual BPL v0.1.82 (shipped rustymath v0.1.53) and standalone rustymath v0.1.54 with this profile and the existing budget multipliers; macOS targets and holds are unchanged. LTO changes runtime code as well as build time. Keep the profile, optimization level, features, target, compiler and LLVM version fixed within a comparison. Rustymath's DEV.md "Loops and codegen" covers runtime inlining and vectorization. @@ -276,7 +278,7 @@ A small repro must retain the concrete types, widths, generic caller chain, inli | `derivative.rs`, `linear.rs`, `optimize.rs` | `∂`: duals, tags, seeding, rustymath's partials through rows, operator rules, loops' JVP and transpose; the transpose and substitution passes over the graph's linear nodes; `⇄` through `rustymath::optimize` | | `transform.rs` | core shared by running transformations: one stack of levels; routing to the newest level among a call's arguments; the read point for code that isn't a call | | `batch.rs` | batched maps: the batch carrier; `each_pair`'s try, calling the operand of an Each, a Rank or an outer product once on batches of cells while the `batch` pref is on (default); batch rules routing hands calls to. `tests/reference.rs` runs every reference case with it off and on; `bpl-tests check_page PAGE --unbatched` checks a page with it off | -| `plan.rs`, `program.rs`, `program/`, `jit.rs` | plans giving a call's result kind and layout from its arguments' (`plan.rs`); `Program`, `Step`, `Desc`, `Key` and the program cache: `Programs` on a dfn's definition, `program::tacit` by structure for other functions (`program.rs`); real-run traces, recordings and planning runs (`program/trace.rs`); replay and step execution (`program/run.rs`); the rule table (`program/rules.rs`) and contraction's builders (`program/contraction.rs`); block stages running elementwise steps a block of items at a time (`program/blocks.rs`), on compiled kernels (`jit.rs`) where compiling repays, else the interpreter's kernels; storage orders (`program/layout.rs`); contract and range checks in reference runs (`program/verify.rs`) | +| `plan.rs`, `program.rs`, `program/`, `jit/src/lib.rs` | plans giving a call's result kind and layout from its arguments' (`plan.rs`); `Program`, `Step`, `Desc`, `Key` and the program cache: `Programs` on a dfn's definition, `program::tacit` by structure for other functions (`program.rs`); real-run traces, recordings and planning runs (`program/trace.rs`); replay and step execution (`program/run.rs`); the rule table (`program/rules.rs`) and contraction's builders (`program/contraction.rs`); block stages running elementwise steps a block of items at a time (`program/blocks.rs`), on compiled kernels (`jit/src/lib.rs`) where compiling repays, else the interpreter's kernels; storage orders (`program/layout.rs`); contract and range checks in reference runs (`program/verify.rs`) | | `search.rs`, `number_theory.rs`, `polynomial.rs`, `distribution.rs` | array-level adapters to rustymath | | `system.rs`, `data.rs`, `files.rs`, `date.rs`, `regex.rs` | system-function metadata, native functions, options, import rules; paths and the filesystem; `•date`; `•r`, `•replace` | | `csv.rs`, `json.rs`, `xml.rs`, `image.rs`, `plot.rs`, `canvas.rs`, `display.rs` | formats, rich values, rendering | @@ -441,7 +443,7 @@ Fusion (`fusion.rs`; its module doc owns eligibility rules and limits: don't cop - A read forces via `ArrayData::storage`: gathers in the programs of pending arrays it reads in order (`Pending::gathered`), runs the result through its plan's units. `program::planned` plans each program structure once, from a copy with inputs for held arrays, reusing the plan for later forces of the same chain on new arrays. Shape, kind and prototype readers (`Value::len`, `Value::environment`) don't force. `Value::force` forces, as timing does. - Fusion and dfn traces both add nodes via `program::add`, which returns an earlier node making the same value: a held atom of the same identity, the same held array, or the same step on the same values. - A pending array read in order that a name or another array also holds joins as an output (`Program::outputs`), computed in the same pass; a block stage stores its items with the result's. Other pending arrays the program reads but doesn't compute as steps compute first, alone (`Pending::gathered`): those not readable in order, and held copies. A fold of a held pending result stores that result as an output of the fold's pass, once for all readers (`fusion::fold`); a pending copy folds so, held or not. -- A block stage runs elementwise steps block by block, in parts (`blocks::Part`): each run of steps lowering to machine operations that give their kernels' exact results (`blocks::lowered_op`) through one compiled kernel (`jit.rs`); other steps through the same pervasive kernels (`pervasive::node`). A part whose kernel can't take a run's items (e.g. an input a broadcast reads as one item) runs those kernels for that run. A stage compiles all parts or none: where the same kernel compiled or ran before, or where one run of its items repays compiling (`blocks::Blocks::compiles`, `repays`). `settings::Compiles` can compile every stage or none: the reference harness; timings via `BASEDPL_COMPILE`. A call waits where fusing it saves more than its node costs (`fusion::waits`: `NODE_NS`, the allowance per call, against `SAVED_NS_PER_BYTE`); `BASEDPL_DEFER=all` makes every call wait, for timing what a node costs. +- A block stage runs elementwise steps block by block, in parts (`blocks::Part`): each run of steps lowering to machine operations that give their kernels' exact results (`blocks::lowered_op`) through one compiled kernel (`jit/src/lib.rs`); other steps through the same pervasive kernels (`pervasive::node`). A part whose kernel can't take a run's items (e.g. an input a broadcast reads as one item) runs those kernels for that run. A stage compiles all parts or none: where the same kernel compiled or ran before, or where one run of its items repays compiling (`blocks::Blocks::compiles`, `repays`). `settings::Compiles` can compile every stage or none: the reference harness; timings via `BASEDPL_COMPILE`. A call waits where fusing it saves more than its node costs (`fusion::waits`: `NODE_NS`, the allowance per call, against `SAVED_NS_PER_BYTE`); `BASEDPL_DEFER=all` makes every call wait, for timing what a node costs. - Each step writes a block to a buffer later steps reuse once its readers ran. A comparison read only by later steps of the stage writes floats 1 and 0 there (`Dyad::compared`, rustymath's `tolerant::Indicator`). `⍚` and `@[p]` run cases that can't fail on every item, selecting by a Boolean mask (`selected`, `eval/operators/agenda.rs`); `plan::select` gives its type and the maps its mask and cases read through; `fusion::select` defers it where the mask is such a comparison, else runs it at once via rustymath's `operation::choose` (`fusion::chosen`). A one-step stage runs only where it reads through a copy, recording no `fused` row. - A structural copy of one source through rustymath `Steps` (`remapped`) large enough to wait (`fusion::waits`) is a pending copy (`fusion::copy`), any compact kind; its map is a `Mapping::Steps`, one entry per result axis, as broadcasting's are. Forcing composes a read of a copy with its map and moves a read with no repeat or fill under the pervasive calls it reaches (`fusion::resolved`): `2×⌽v+1` reads `v` backwards in one program. A copying read (rustymath's `Mapping::copies`) moves only under a one-array call. A call whose two array arguments copy same-layout arrays through one map runs on those arrays, its result copied once (`program::rules`, `copy_lifted`). Other arrays: a `Copy` step, which a block stage reads through without making the copy. A step reads items in order under another shape through its operand's `program::Read`. - A block stage shrinks blocks whose buffers wouldn't fit in cache. Rustymath's `agreement::expanded` reads a forward run in place, else copies via `gather::walk_plan`. A `Mapping::Steps` keeps the plan `Steps::simplify` makes on its first walk (`agreement::Route`) for every later block. A copy no call reads through is walked as `remapped` walks it. @@ -572,7 +574,7 @@ The playground and gallery load released assets from jsDelivr, or local `nbs/pla CSV columns, JSON arrays and Python lists share `data::imported`: a non-fill finite float, or any nonfinite float, promotes to float when every number converts losslessly; finite fills alone don't; otherwise items keep their kinds. JSON objects and Python dicts read as literals do (`keyed::vector`, `Value::from_parts`). Binary data: exact bytes. Writes validate before opening and create exclusively unless overwrite is explicit. Exports: `•csv⁻¹`, `•json⁻¹`, `•xml⁻¹`. -`regex.rs` shares host regex state between match and replace native functions; hosts keep their own regex syntax. `xml.rs`: keyed element trees; XML escaping and validation kept apart from HTML raw script/style and void-element rules. SVG has its own renderer. `image.rs` owns pictures, their options and animation timing, as plain pixel buffers; its `codec` module decodes and encodes via ImageIO on Apple targets (`image/imageio.rs`), the `image` and `png` crates elsewhere (`image/codecs.rs`). Native and browser renderers use the MIME and byte formats their frontends support. `plot.rs` stores specs and draws SVG on demand; the renderer owns defaults and validation. Rendering options are documented in their user pages and implementation comments. +`regex.rs` shares host regex state between match and replace native functions; hosts keep their own regex syntax. `xml.rs`: keyed element trees; XML escaping and validation kept apart from HTML raw script/style and void-element rules. SVG has its own renderer. `image.rs` owns pictures, their options and animation timing, as plain pixel buffers; `basedpl-image` decodes and encodes via ImageIO on Apple targets (`image/src/imageio.rs`), the `image` and `png` crates elsewhere (`image/src/codecs.rs`). Native and browser renderers use the MIME and byte formats their frontends support. `plot.rs` stores specs and draws SVG on demand; the renderer owns defaults and validation. Rendering options are documented in their user pages and implementation comments. `display.rs` owns plain, boxed and function display, elision and MIME rendering. Display settings affect implicit presentation, not returned values or `⍕`; explicit output prints all items. Display and source text read items, not storage: `Value::shown_dtype` (`array/text.rs`) gives the dtype compact storage of the items would take (`storage::items_dtype`'s items rule), read by the shared `ₓ` or width marker, plain display's choice between brackets and a strand, the bracketed grid plain display and `⍕` share (`display::table`, `display::bracketed`), a keyed table's corner and the boxed marker. `•mime` calls a renderer with echo off and cancellation active. Failed implicit rendering falls back to text; interrupts and timeouts propagate. `canvas.rs` serializes drawing data and typed bytes; `canvas.js` and `nbs/driver.js` own browser playback. Keep formatting, serialization and rich output separate contracts. diff --git a/image/Cargo.toml b/image/Cargo.toml new file mode 100644 index 00000000..6e75e3a1 --- /dev/null +++ b/image/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "basedpl-image" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +publish = false + +[lib] +test = false +doctest = false + +[target.'cfg(not(target_vendor = "apple"))'.dependencies] +# Macs decode and encode images through ImageIO instead. +image = { version = ">=0.25.6, <1", default-features = false, features = ["png", "jpeg"] } +png = ">=0.18.1, <1" + +[target.'cfg(target_vendor = "apple")'.dependencies] +flate2.workspace = true diff --git a/src/image/codecs.rs b/image/src/codecs.rs similarity index 97% rename from src/image/codecs.rs rename to image/src/codecs.rs index 987a45fa..cdd25d23 100644 --- a/src/image/codecs.rs +++ b/image/src/codecs.rs @@ -5,7 +5,7 @@ use ::image::{AnimationDecoder, ExtendedColorType, ImageDecoder, ImageEncoder, I use std::io::Cursor; /// The pixels of a PNG or JPEG file, widened to 16 bits. -pub(super) fn decode(bytes: &[u8]) -> Result, String> { +pub fn decode(bytes: &[u8]) -> Result, String> { let text = |e: ImageError| e.to_string(); if let Ok(png) = PngDecoder::new(Cursor::new(bytes)) { if png.is_apng().map_err(text)? { return animation(png); } } let image = ::image::load_from_memory(bytes).map_err(text)?; diff --git a/src/image/imageio.rs b/image/src/imageio.rs similarity index 99% rename from src/image/imageio.rs rename to image/src/imageio.rs index c0bbd03f..1b3bc2bc 100644 --- a/src/image/imageio.rs +++ b/image/src/imageio.rs @@ -88,7 +88,7 @@ impl Cf { impl Drop for Cf { fn drop(&mut self) { unsafe { CFRelease(self.get()) } } } /// The pixels of an image file in any format that ImageIO reads, widened to 16 bits. An animation's frames come composited. An animated PNG's frames keep the channels that the file's header gives. -pub(super) fn decode(bytes: &[u8]) -> Result, String> { +pub fn decode(bytes: &[u8]) -> Result, String> { let source = Cf::new(unsafe { CGImageSourceCreateWithData(cf_data(bytes).get(), null()) }).ok_or("unreadable data")?; let count = unsafe { CGImageSourceGetCount(source.get()) }; let frame = |i| { diff --git a/image/src/lib.rs b/image/src/lib.rs new file mode 100644 index 00000000..cae5c0f9 --- /dev/null +++ b/image/src/lib.rs @@ -0,0 +1,50 @@ +/// Decoding and encoding of image files: ImageIO on Apple platforms, and the `image` and `png` crates elsewhere. +#[cfg_attr(target_vendor = "apple", path = "imageio.rs")] +#[cfg_attr(not(target_vendor = "apple"), path = "codecs.rs")] +mod codec; +pub use codec::decode; + +/// Encodes frames whose dimensions and channels match their samples. +pub fn encode(frames: &Raster, delays: Option<&[f64]>, format: Format) -> Result, String> { + let size = [frames.count, frames.height, frames.width, frames.channels].into_iter().try_fold(1usize, usize::checked_mul); + if !(1..=4).contains(&frames.channels) || size != Some(frames.data.len()) { return Err("invalid raster dimensions".into()); } + codec::encode(frames, delays, format) +} + +/// The image file formats that `•image⁻¹` writes. +#[derive(Clone, Copy, PartialEq)] +pub enum Format { Png, Jpeg } + +/// The samples of `count` frames, each of `height` rows of `width` pixels with `channels` samples: grey, or red, green and blue, then alpha where `channels` is even. `data` holds the frames in turn, each row by row. +pub struct Raster { pub count: usize, pub height: usize, pub width: usize, pub channels: usize, pub data: Vec } + +impl Raster { + /// The samples of frame `i`. + fn frame(&self, i: usize) -> &[T] { let size = self.height * self.width * self.channels; &self.data[i * size..(i + 1) * size] } +} + +/// `data`'s pixels with `to` channels in place of `from`, as `rechannel_into` gives them. +pub fn rechannelled(data: &[T], from: usize, to: usize, opaque: T) -> Vec { + let mut out = Vec::new(); + rechannel_into(&mut out, data, from, to, opaque, |x| x); + out +} + +/// Appends `data`'s pixels to `out` through `f`, with `to` channels in place of `from`. Grey repeats into red, green and blue, and red stands for grey. Alpha is dropped, or added as `opaque`. +fn rechannel_into(out: &mut Vec, data: &[T], from: usize, to: usize, opaque: T, f: impl Fn(T) -> U) { + let colours = |channels: usize| if channels < 3 { 1 } else { 3 }; + out.reserve(data.len() / from * to); + for pixel in data.chunks_exact(from) { + out.extend((0..colours(to)).map(|k| f(pixel[if colours(from) == 1 { 0 } else { k }]))); + if to % 2 == 0 { out.push(f(if from % 2 == 0 { pixel[from - 1] } else { opaque })); } + } +} + +/// `seconds` as an APNG frame delay, a ratio of 16-bit integers: exactly `1/n` for a whole rate `n`, otherwise in milliseconds, or in whole seconds for a delay longer than 65 seconds. +fn ratio(seconds: f64) -> (u16, u16) { + let max = f64::from(u16::MAX); + let rate = (1. / seconds).round(); + if (1. ..=max).contains(&rate) && (rate * seconds - 1.).abs() < 1e-9 { return (1, rate as u16); } + let ms = (seconds * 1000.).round(); + if ms <= max { (ms.max(1.) as u16, 1000) } else { (seconds.round().min(max) as u16, 1) } +} diff --git a/jit/Cargo.toml b/jit/Cargo.toml new file mode 100644 index 00000000..1e1bf831 --- /dev/null +++ b/jit/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "basedpl-jit" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +publish = false + +[lib] +test = false +doctest = false + +[dependencies] +rustymath.workspace = true +# Cranelift compiles a typed program's loops while BPL runs. The browser build has no JIT. +cranelift-codegen = { version = "0.137", default-features = false, features = ["std", "host-arch"] } +cranelift-frontend = "0.137" +cranelift-jit = "0.137" +cranelift-module = "0.137" +cranelift-native = "0.137" diff --git a/src/jit.rs b/jit/src/lib.rs similarity index 99% rename from src/jit.rs rename to jit/src/lib.rs index 49d4b782..d6f1e561 100644 --- a/src/jit.rs +++ b/jit/src/lib.rs @@ -17,14 +17,14 @@ pub enum Ty { Bool, U8, I16, I32, I64, F32, F64 } impl Ty { /// The item type that holds values of this type. - pub(crate) fn dtype(self) -> DType { + pub fn dtype(self) -> DType { match self { Ty::Bool => DType::Boolean, Ty::U8 => DType::Integer(Width::U8), Ty::I16 => DType::Integer(Width::I16), Ty::I32 => DType::Integer(Width::I32), Ty::I64 => DType::Integer(Width::I64), Ty::F32 => DType::Float(FloatWidth::F32), Ty::F64 => DType::Float(FloatWidth::F64) } } /// The type of a compiled kernel's values for items of `dtype`, or `None` for a type that a stage doesn't hold (`program::blocks::readable`). - pub(crate) fn of(dtype: DType) -> Option { [Ty::Bool, Ty::U8, Ty::I16, Ty::I32, Ty::I64, Ty::F32, Ty::F64].into_iter().find(|t| t.dtype() == dtype) } + pub fn of(dtype: DType) -> Option { [Ty::Bool, Ty::U8, Ty::I16, Ty::I32, Ty::I64, Ty::F32, Ty::F64].into_iter().find(|t| t.dtype() == dtype) } fn bytes(self) -> i64 { self.dtype().bytes() as i64 } /// The type of one item in registers. A Boolean is a byte, all ones or all zeros. fn ir(self) -> Type { @@ -65,10 +65,10 @@ impl Input { } /// A float in a scalar slot's form: its value as f64 bits, which code demotes to f32 where its type is f32 (`slot`). Every scalar and constant slot holds a float this way. -pub(crate) fn float_slot(x: f64) -> u64 { x.to_bits() } +pub fn float_slot(x: f64) -> u64 { x.to_bits() } /// An integer or Boolean in a scalar slot's form: its value as i64 bits, which code narrows to its type (`slot`). -pub(crate) fn integer_slot(x: i64) -> u64 { x as u64 } +pub fn integer_slot(x: i64) -> u64 { x as u64 } /// A value that a step reads: the input of that number, or the result of the earlier step of that number. A program's constant is a scalar input, and a number that a recipe fixes is a constant input. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] @@ -135,7 +135,7 @@ fn kernels() -> std::sync::MutexGuard<'static, HashMap(k: &'a mut HashMap>>, key: &Kernel) -> &'a mut HashMap>> { diff --git a/pyproject.toml b/pyproject.toml index d2ce9fc2..6837b1cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ module-name = "basedpl._core" macos-deployment-target = "15.0" [tool.uv] -cache-keys = [{ file = "pyproject.toml" }, { file = "Cargo.toml" }, { file = "cli/Cargo.toml" }, { file = "py/Cargo.toml" }] +cache-keys = [{ file = "pyproject.toml" }, { file = "Cargo.toml" }, { file = "cli/Cargo.toml" }, { file = "image/Cargo.toml" }, { file = "jit/Cargo.toml" }, { file = "py/Cargo.toml" }] [tool.fastws] dev-profile = "release" diff --git a/src/image.rs b/src/image.rs index 93831c94..b82f2486 100644 --- a/src/image.rs +++ b/src/image.rs @@ -1,26 +1,10 @@ use crate::{Described, Error, Span, Value, data, display, element::read_as, keyed, number::Numeric}; +use basedpl_image::{self as codec, Format, Raster, rechannelled}; use rustymath::{buffer::DType, error::Kind}; -/// Decoding and encoding of image files: ImageIO on Apple platforms, and the `image` and `png` crates elsewhere. -#[cfg_attr(target_vendor = "apple", path = "image/imageio.rs")] -#[cfg_attr(not(target_vendor = "apple"), path = "image/codecs.rs")] -mod codec; - /// The rate an animation plays at without an `fps` or `delay` option. const DEFAULT_FPS: f64 = 24.; -/// The image file formats that `•image⁻¹` writes. -#[derive(Clone, Copy, PartialEq)] -enum Format { Png, Jpeg } - -/// The samples of `count` frames, each of `height` rows of `width` pixels with `channels` samples: grey, or red, green and blue, then alpha where `channels` is even. `data` holds the frames in turn, each row by row. -struct Raster { count: usize, height: usize, width: usize, channels: usize, data: Vec } - -impl Raster { - /// The samples of frame `i`. - fn frame(&self, i: usize) -> &[T] { let size = self.height * self.width * self.channels; &self.data[i * size..(i + 1) * size] } -} - /// `X •image Y` returns a picture: the image file that `Y` names, the bytes of one, or the numbers in `Y`. Files are PNG or JPEG. Apple platforms also read the other formats that ImageIO reads. A picture is numbers from 0 to 1 with axes for rows, columns and up to four channels, displayed as an image. An animation has frames along a first axis: a rank-4 array, or any array of pictures with the option `fps`, the frame rate, or `delay`, the seconds each frame stays on screen. pub(crate) fn image(left: Option<&Value>, right: &Value, span: &Span) -> Result { let options = data::Options::new("•image", left, None, &["fps", "delay"])?; @@ -89,23 +73,6 @@ fn encoded(mut frames: Raster, delays: Option<&[f64]>, format: Format) -> Re codec::encode(&frames, delays, format).map_err(|e| Error::domain(format!("image encoding failed: {e}"))) } -/// `data`'s pixels with `to` channels in place of `from`, as `rechannel_into` gives them. -fn rechannelled(data: &[T], from: usize, to: usize, opaque: T) -> Vec { - let mut out = Vec::new(); - rechannel_into(&mut out, data, from, to, opaque, |x| x); - out -} - -/// Appends `data`'s pixels to `out` through `f`, with `to` channels in place of `from`. Grey repeats into red, green and blue, and red stands for grey. Alpha is dropped, or added as `opaque`. -fn rechannel_into(out: &mut Vec, data: &[T], from: usize, to: usize, opaque: T, f: impl Fn(T) -> U) { - let colours = |channels: usize| if channels < 3 { 1 } else { 3 }; - out.reserve(data.len() / from * to); - for pixel in data.chunks_exact(from) { - out.extend((0..colours(to)).map(|k| f(pixel[if colours(from) == 1 { 0 } else { k }]))); - if to % 2 == 0 { out.push(f(if from % 2 == 0 { pixel[from - 1] } else { opaque })); } - } -} - /// The frame count, height, width and channels that `picture` describes, and the seconds each frame of an animation stays on screen: one number for every frame, or one per frame. With `delays`, or with four axes, the first axis is time. It reads the picture's storage kind instead of its samples. A complex picture is read in full, because its imaginary parts must all be zero. fn layout(picture: &Value, delays: Option>) -> Result<([usize; 4], Option>), Error> { let delays = delays.or_else(|| (picture.shape().len() == 4).then(|| vec![1. / DEFAULT_FPS])); @@ -133,11 +100,3 @@ fn frames(picture: &Value, delays: Option>) -> Result<(Raster, Opti Ok((Raster { count, height, width, channels, data }, delays)) } -/// `seconds` as an APNG frame delay, a ratio of 16-bit integers: exactly `1/n` for a whole rate `n`, otherwise in milliseconds, or in whole seconds for a delay longer than 65 seconds. -fn ratio(seconds: f64) -> (u16, u16) { - let max = f64::from(u16::MAX); - let rate = (1. / seconds).round(); - if (1. ..=max).contains(&rate) && (rate * seconds - 1.).abs() < 1e-9 { return (1, rate as u16); } - let ms = (seconds * 1000.).round(); - if ms <= max { (ms.max(1.) as u16, 1000) } else { (seconds.round().min(max) as u16, 1) } -} diff --git a/src/lib.rs b/src/lib.rs index 9d3c89bf..51869183 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,7 +23,7 @@ mod image; pub mod inspection; #[cfg(not(web))] #[doc(hidden)] -pub mod jit; +pub use basedpl_jit as jit; pub mod js; mod json; pub mod keyed; diff --git a/src/pervasive/fold.rs b/src/pervasive/fold.rs index 0370d708..473c7caa 100644 --- a/src/pervasive/fold.rs +++ b/src/pervasive/fold.rs @@ -3,7 +3,7 @@ use super::*; /// A reduction of `right` along `axis`, from the right as the general fold does, through `p`'s formula (`Kernel::fold_read`) from the kind the fold computes in, with `map`'s reruns at wider kinds (`computed`), as an integer `-/[0 1]ₓ` reruns past `u8`. The formula's own fold combines items in any order where `p` allows it, and an unseeded `-` or `÷` folds as an alternating sum or product. A pending copy reads its source through its map, with its prototype at each fill position, so rustymath folds it by sliding windows or through the map (`reduce::fold_route`) without building the copy, unless `reduce::fold_route` chooses stored lanes for a held copy (`FoldRoute::Lanes`): that copy is built once, for its later reads. A seed combines with the total through `map`, for functions that `seed_last` allows. `None` where no kind gives the result, as for an empty lane of a function with no identity: the general fold then gives the result or error. A failed seed combination returns its error. pub(crate) fn fold(p: Primitive, right: &Value, axis: usize, seed: Option<&Value>) -> Option> { - use rustymath::{operation::{Direction, Seed}, reduce::{FoldRoute, fold_route}}; + use rustymath::reduce::{Direction, FoldRoute, Seed, fold_route}; if seed.is_some() && !seed_last(p, right) { return None; } let plan = plan::fold(p, FoldKind::REDUCE, seed.map(Kind::value), Kind::value(right), Numeric::current()); let numeric = plan.numeric; diff --git a/src/pervasive/scan.rs b/src/pervasive/scan.rs index f33dc23a..c375944e 100644 --- a/src/pervasive/scan.rs +++ b/src/pervasive/scan.rs @@ -1,6 +1,6 @@ //! Scans of stored values by the pervasive functions, and their inverses, through `p`'s formula: `Kernel::scan` and `Kernel::adjacent`. use super::*; -use rustymath::{agreement::{Binding, Missing, ReadPlan}, operation::{Direction, Seed, Settings}}; +use rustymath::{agreement::{Binding, Missing, ReadPlan}, operation::Settings, reduce::{Direction, Seed}}; /// The scan by `p` of stored values along `axis`, left to right, through `p`'s formula (`Kernel::scan`) at the kind that a scan by `p` computes in, with `map`'s reruns at wider kinds (`computed`). A seed of rank 0 starts every lane. A comparison's or residue's seed that stands for one integer beside the items (`Pervasive::narrowed`, `Pervasive::whole_float`) scans as that integer at its narrowest width: each step then computes at the items' integer kind. Where the scan with the seed as written gives floats (`Fold::result`), the results take that float kind. `None` where the formula fails or the seed's rank isn't 0: the general scan then gives the result or error. pub(crate) fn scan(p: Primitive, right: &Value, seed: Option<&Value>, axis: &Lanes) -> Option { diff --git a/src/program/blocks.rs b/src/program/blocks.rs index a8e7fa07..d5ade6dd 100644 --- a/src/program/blocks.rs +++ b/src/program/blocks.rs @@ -638,7 +638,7 @@ impl Blocks { /// The fold's value, through its kernel's fold of the pieces that the stage hands over a block at a time (`Kernel::fold_pieces`): each block's items, or their total where a compiled part folds them. The kernel's formula picks the reduction, as the eager fold's does (`pervasive::fold`), so `-` and `÷` fold as alternating sums and products. A seed then combines with each total through `pervasive::map`, as the eager fold's does. A fold of every item gives its one total as an atom, as `folded` in `eval::operators` takes it. `None` where a step or the kernel fails. A failed seed combination returns its error. fn folded(&self, fold: &BlockFold, seed: Option<&Number>, run: &Run<'_>, outs: &[Option]) -> Option> { - use rustymath::{error::{Error, Kind as Failure}, operation::{Direction, Piece, Seed, Total}}; + use rustymath::{error::{Error, Kind as Failure}, reduce::{Direction, Piece, Seed, Total}}; let kernel = fold.kernel.as_ref()?; let mut out = Buffer::with_capacity(fold.dtype, 0); let pieces = |range: Range, give: &mut dyn FnMut(usize, Piece<'_>)| -> Result<(), Error> { @@ -792,9 +792,10 @@ impl Blocks { unsafe { held.set_len(at + n) }; } (true, Last::Fold(_), None) => return false, + // SAFETY: the compiled kernel initializes every one of its `n` output items. (true, last, _) => with_item!(self.steps[self.steps.len() - 1].made(), A => match A::out(last) { - Out::Items(slots) => { slots.fill(n, |s| { call(s.as_mut_ptr().cast()); true }); } - Out::Bools(slots) => { slots.fill(n, |s| { call(s.as_mut_ptr().cast()); true }); } + Out::Items(slots) => { unsafe { slots.fill(n, |s| { call(s.as_mut_ptr().cast()); true }) }; } + Out::Bools(slots) => { unsafe { slots.fill(n, |s| { call(s.as_mut_ptr().cast()); true }) }; } }), } for &store in &lowered.stores { diff --git a/src/program/blocks/items.rs b/src/program/blocks/items.rs index a00fa288..5ba39fc6 100644 --- a/src/program/blocks/items.rs +++ b/src/program/blocks/items.rs @@ -125,7 +125,7 @@ pub(super) trait Item: Stored + Sync { fn read<'a>(got: Got<'a>, n: usize, buf: &'a mut Vec, t: Tolerance) -> Arg<'a, Self>; /// The number `n` as this type, where it converts. fn constant(n: &Number) -> Option; - /// The item as a compiled kernel's 8-byte slot holds it (`jit::Compiled::run`), as a scalar or a fold's total, which `rustymath::operation::Total` reads back: a float as an f64, an integer extended to 64 bits, and a Boolean as 0 or 1. + /// The item as a compiled kernel's 8-byte slot holds it (`jit::Compiled::run`), as a scalar or a fold's total, which `rustymath::reduce::Total` reads back: a float as an f64, an integer extended to 64 bits, and a Boolean as 0 or 1. #[cfg(not(web))] fn slot(self) -> u64; /// Writes `x` where `mask` isn't zero and `y` where it is into the next `n` slots of `out`, through rustymath's `Choose`, which has a kernel at every type. diff --git a/tests/jit.rs b/tests/jit.rs index 600f008d..32adac12 100644 --- a/tests/jit.rs +++ b/tests/jit.rs @@ -56,7 +56,7 @@ fn folds_match_rustymath() { unsafe { kernel.run(n, block, &[x.as_ptr().cast()], &[0.5, 1e-4, 1.0].map(f64::to_bits), &[out.as_mut_ptr().cast()], &mut totals) }; assert!(out.iter().zip(&halves).all(|(a, b)| a.to_bits() == b.to_bits()), "halves of {n} items"); for (k, h) in halves.chunks(block).enumerate() { - let got = |j: usize| f64::from(rustymath::operation::Total { items: h.len(), bits: totals[4 * k + j] }.value::<$f>().unwrap()); + let got = |j: usize| f64::from(rustymath::reduce::Total { items: h.len(), bits: totals[4 * k + j] }.value::<$f>().unwrap()); let nan = h.iter().any(|v| v.is_nan()); let (most, least) = h.iter().filter(|v| !v.is_nan()).fold((f64::NEG_INFINITY, f64::INFINITY), |(a, b), &v| (a.max(v.into()), b.min(v.into()))); From de2a83ed4baed2aac993bc3f4db98d90ebb80732 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 7 Nov 2026 18:08:17 +0000 Subject: [PATCH 2/8] Add superscript Rank notation and improve native array execution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use function superscripts for Rank and bare ⁻ for inverses, with matching parser/editor support and migrated documentation, notebooks and library examples. Validate captured layouts before replaying differentiated programs. Reuse block scratch and shared Mix assembly, and floor dynamic reals directly into the selected integer width. Preserve test inventories while reducing setup and diagnostic overhead. See Scripts, Composition, Autodiff and Development. --- DEV.md | 21 +- .../BPL.tmbundle/Syntaxes/bpl.tmLanguage.json | 4 +- .../BPL.tmbundle/Syntaxes/hpl.tmLanguage.json | 4 +- editors/jetbrains/BPL.xml | 10 + editors/vim/syntax/bpl.vim | 4 +- image/src/lib.rs | 2 +- lib/dyalog.bpl | 22 +- lib/graph.bpl | 4 +- lib/numeric.bpl | 6 +- lib/power.bpl | 2 +- lib/string.bpl | 12 +- nbs/assignment.ipynb | 12 +- nbs/bpl.xml | 4 +- nbs/comparison.qmd | 6 +- nbs/composition.qmd | 14 +- nbs/data.ipynb | 36 +- nbs/foundations.qmd | 2 +- nbs/gallery/attractor.bpl | 4 +- nbs/gallery/boids.bpl | 4 +- nbs/gallery/flow.bpl | 6 +- nbs/gallery/reaction.bpl | 2 +- nbs/getting-started.ipynb | 4 +- nbs/glyphs.qmd | 13 +- nbs/glyphs/at.qmd | 6 +- nbs/glyphs/backslash-bar.qmd | 4 +- nbs/glyphs/backslash.qmd | 6 +- nbs/glyphs/circle-backslash.qmd | 24 +- nbs/glyphs/circle-bar.qmd | 4 +- nbs/glyphs/circle-diaeresis.qmd | 4 +- nbs/glyphs/circle-divide.qmd | 2 +- nbs/glyphs/circle-slash.qmd | 4 +- nbs/glyphs/circle-star.qmd | 6 +- nbs/glyphs/circle-stile.qmd | 4 +- nbs/glyphs/circle-times.qmd | 4 +- nbs/glyphs/circle.qmd | 16 +- nbs/glyphs/colon.qmd | 10 +- nbs/glyphs/comma-bar.qmd | 6 +- nbs/glyphs/comma.qmd | 8 +- nbs/glyphs/del-stile.qmd | 4 +- nbs/glyphs/delta-stile.qmd | 4 +- nbs/glyphs/diaeresis.qmd | 4 +- nbs/glyphs/domino.qmd | 4 +- nbs/glyphs/down-arrow.qmd | 4 +- nbs/glyphs/down-tack.qmd | 6 +- nbs/glyphs/equals.qmd | 2 +- nbs/glyphs/greater-than-or-equal.qmd | 4 +- nbs/glyphs/hash.qmd | 20 +- nbs/glyphs/hydrant.qmd | 4 +- nbs/glyphs/iota-underbar.qmd | 4 +- nbs/glyphs/iota.qmd | 6 +- nbs/glyphs/jot-diaeresis.qmd | 8 +- nbs/glyphs/jot.qmd | 4 +- nbs/glyphs/left-shoe-underbar.qmd | 6 +- nbs/glyphs/left-shoe.qmd | 6 +- nbs/glyphs/left-tack.qmd | 4 +- nbs/glyphs/left-tail-arrow.qmd | 6 +- nbs/glyphs/less-than-or-equal.qmd | 4 +- nbs/glyphs/minus.qmd | 6 +- nbs/glyphs/not-equals.qmd | 2 +- nbs/glyphs/obelus.qmd | 6 +- nbs/glyphs/partial.qmd | 4 +- nbs/glyphs/pi.qmd | 8 +- nbs/glyphs/plus.qmd | 4 +- nbs/glyphs/quad-colon.qmd | 4 +- nbs/glyphs/quad-jot.qmd | 8 +- nbs/glyphs/radical.qmd | 8 +- nbs/glyphs/rho.qmd | 4 +- nbs/glyphs/right-shoe.qmd | 4 +- nbs/glyphs/right-tack.qmd | 4 +- nbs/glyphs/right-tail-arrow.qmd | 6 +- nbs/glyphs/squad.qmd | 4 +- nbs/glyphs/star-diaeresis.qmd | 12 +- nbs/glyphs/star.qmd | 6 +- nbs/glyphs/stile-tilde.qmd | 6 +- nbs/glyphs/superscript-minus.qmd | 23 + nbs/glyphs/tilde-diaeresis.qmd | 6 +- nbs/glyphs/tilde.qmd | 4 +- nbs/glyphs/times.qmd | 6 +- nbs/glyphs/up-arrow.qmd | 4 +- nbs/glyphs/up-down-arrow.qmd | 6 +- nbs/glyphs/up-tack.qmd | 6 +- nbs/glyphs/vee-bar.qmd | 4 +- nbs/glyphs/vee.qmd | 4 +- nbs/glyphs/wedge-bar.qmd | 4 +- nbs/glyphs/wedge.qmd | 4 +- nbs/hpl.xml | 4 +- nbs/index-notation.qmd | 8 +- nbs/keyboard.qmd | 4 +- nbs/keyed.ipynb | 8 +- nbs/llms.txt | 17 +- nbs/numbers.qmd | 12 +- nbs/python.ipynb | 2 +- nbs/repl.qmd | 2 +- nbs/scripts.ipynb | 69 ++- nbs/study/01_basics.ipynb | 62 +- nbs/study/02_functions.ipynb | 44 +- nbs/study/03_operators.ipynb | 92 +-- nbs/study/04_nested-arrays.ipynb | 40 +- nbs/study/05_searching-indexing.ipynb | 6 +- nbs/study/06_rearranging-arrays.ipynb | 28 +- nbs/study/07_numerical-tools.ipynb | 8 +- nbs/syntax.qmd | 6 +- nbs/system-functions.qmd | 4 +- nbs/system/csv.qmd | 2 +- nbs/system/date.qmd | 10 +- nbs/system/decompose.qmd | 4 +- nbs/system/deflate.qmd | 6 +- nbs/system/distribution.qmd | 2 +- nbs/system/image.qmd | 4 +- nbs/system/index.qmd | 6 +- nbs/system/json.qmd | 4 +- nbs/system/literal.qmd | 6 +- nbs/system/path.qmd | 2 +- nbs/system/ucs.qmd | 2 +- nbs/system/xml.qmd | 4 +- nbs/target.qmd | 6 +- nbs/web.ipynb | 2 +- nbs/xml.ipynb | 26 +- py/src/lib.rs | 2 +- python/basedpl/bpl.tmLanguage.json | 4 +- python/basedpl/bpltests.py | 31 +- python/basedpl/browser/language.js | 4 +- python/basedpl/editors.py | 6 +- python/basedpl/functions.py | 6 +- python/basedpl/hpl.tmLanguage.json | 4 +- python/basedpl/keyboards.py | 1 + src/array.rs | 6 +- src/batch.rs | 8 +- src/csv.rs | 4 +- src/data.rs | 2 +- src/date.rs | 16 +- src/derivative.rs | 5 +- src/display.rs | 2 +- src/error.rs | 2 +- src/eval.rs | 42 +- src/eval/assign.rs | 11 +- src/eval/binder.rs | 38 +- src/eval/operators/products.rs | 4 +- src/eval/session.rs | 4 +- src/eval/tree.rs | 4 +- src/files.rs | 8 +- src/fusion.rs | 1 - src/image.rs | 11 +- src/json.rs | 4 +- src/keyed.rs | 2 +- src/number.rs | 2 +- src/number_theory.rs | 2 +- src/pervasive.rs | 31 +- src/polynomial.rs | 2 +- src/primitive.rs | 19 +- src/primitive/facts.rs | 2 +- src/primitive/numeric.rs | 6 +- src/primitive/search.rs | 4 +- src/primitive/structural.rs | 14 +- src/program.rs | 8 +- src/program/blocks.rs | 44 +- src/program/blocks/items.rs | 28 +- src/program/trace.rs | 45 +- src/settings.rs | 12 +- src/symbols.rs | 8 +- src/syntax.rs | 102 ++-- src/system.rs | 8 +- src/timing.rs | 57 +- src/transform.rs | 2 +- src/xml.rs | 2 +- tests/acceptance/held_out.bpl | 10 +- tests/core.rs | 42 ++ tests/derivatives.rs | 14 +- tests/explain.rs | 2 +- tests/reference.rs | 5 +- tests/reference/aplcart.bpl | 552 +++++++++--------- tests/reference/april.bpl | 140 ++--- tests/reference/bqn.bpl | 10 +- tests/reference/core.bpl | 500 ++++++++-------- tests/reference/dyalog.bpl | 4 +- tests/reference/graphics.bpl | 32 +- tests/reference/ngn.bpl | 38 +- tests/reference/static.bpl | 10 +- tests/speed.rs | 11 +- tests/speed/arrays.bpl | 54 +- tests/speed/calls.bpl | 2 +- tests/speed/images.bpl | 11 +- tests/speed/maps.bpl | 32 +- tests/speed/models.bpl | 40 +- tests/speed/structure.bpl | 25 +- tests/speed/tables.bpl | 8 +- tests/test_api.py | 2 +- tests/test_bpltests.py | 15 +- tests/wasm-speed.mjs | 1 + tests/wasm.mjs | 4 +- 190 files changed, 1739 insertions(+), 1534 deletions(-) create mode 100644 nbs/glyphs/superscript-minus.qmd diff --git a/DEV.md b/DEV.md index 19a6d394..12985cb6 100644 --- a/DEV.md +++ b/DEV.md @@ -126,7 +126,8 @@ Budgets: Routes: -- `tests/explain.rs`'s `speed_cases_take_their_routes` captures every speed case after warm-up at normal setup and size, forcing results outside timing (`timing::routes`), then each reference case with route directives once under ordinary preferences. Plain `cargo test` runs it. `check_file` checks a file's route directives likewise, on one thread. Route checks decide which calls wait with the Mac's costs on every platform (`fusion::Waits::Reference`, set in `timing::route`), so a directive observed on the Mac holds on CI; real runs and timings use each platform's own costs. `⍝⍝⍝ route NAME` above a case requires that fast route. A permitted slow event never excuses a missing required route. +- `tests/explain.rs`'s `speed_cases_take_their_routes` captures every speed case after warm-up with small fixture dimensions under `Sizes::SMALL`, forcing results outside timing (`timing::routes`), then each reference case with route directives once under ordinary preferences. Plain `cargo test` runs it. `check_file` checks a file's route directives likewise, on one thread. `⍝⍝⍝ route NAME` above a case requires that fast route. A permitted slow event never excuses a missing required route. +- Speed setups write scalable dimensions as `full size small`. The timing session supplies dyadic `size` as `⊣` for full-size measurements and `⊢` for small route checks, after leading preferences have set the parser's numeric kinds. Fixed feature and window dimensions stay fixed. `timing::costs` captures the two full-size warm-up runs through `Capture::violations`, then times five uncaptured evaluations. Native and WASM cost records retain and display route obligations. `Sizes::batch` owns the repeated-constant chunk bound: 2²¹ by default and 4096 under `SMALL`. - Unlisted slow events fail: `stored` (pending items computed outside a fused group), `forced` (a call computing pending items it reads as items; rows with reason `read as values`, an argument such as a count, or `an effect`, the pending arrays that an effect computes first, aren't slow), `fallbacks` (batched fallback), `mixed` (a write leaving mixed storage), `itemwise`, `stopped` (stopped trace). Name them as `•explain` does: function (`forced`), code (`mixed`), route (`itemwise`), reason (the rest). `⍝⍝⍝ allows RECORD "EVENT": principle` permits work (a matrix product reading its arguments whole); `⍝⍝⍝ gap RECORD "EVENT": what should happen` names a fast-path gap. A stopped trace takes `allows` only for a by-design stop (dynamic kinds' value-dependent types), else `gap`. `declined` (a considered route not taken) is informational; a `gap` or `allows` can name its route and reason (`fusion: an exact argument`). Every annotation must match an observed event; remove it when the event disappears. `cargo test -- --nocapture` prints remaining gaps. - Native selectors (`reference`, `explain`, `speed`): `BASEDPL_SOURCE` matches a source name, with or without `.bpl`; `BASEDPL_SECTION` matches a section prefix; `BASEDPL_CASE` matches an ID or code prefix. A selector matching no cases fails. Filtering precedes setup and case execution. Explain stories use source `explain`, with sections `idioms`, `records`, `read-back`, `accumulation`, `reads`, `declines`, `stops`, `memory`. - `tests/skipped.txt`: full-size cases explicitly set aside, one per line: test name, tab, section (`*` for every section), tab, case key, tab, the open item that records why. Coverage and unit suites use the case's code as its key. Reference-file rows use `reference:.bpl`, the exact section and the existing case ID, or the first comment line when the ID is empty. `reference::mark_skipped` defines that key and rejects ambiguous or stale entries against the full file before selection. Rust checks and worker inventories share the matcher. `check_file` retains skipped identities, reasons and modes; skips are unresolved, not passes. Remove an entry when its case is ready to run again. @@ -138,7 +139,7 @@ Before and after: for a change meant to speed something up, or to code every cal - `cargo develop` keeps each installed build read-only under an id hashing its inputs and files, installing by repointing one link (`fastws.store`). `fastws.store.builds(root)`: the newest builds of the project at `root`, as records: id, when and by whom kept, input files changed since the previous build, inputs possibly modified since build started (mtime diagnostics, not proof of what the compiler read), `*` on the installed one. `bisect(case, good, bad, by=)`: the adjacent kept builds where a case's verdict changes, by `check` (a reference case passes), `value` (an expression prints what it prints on `good`), `instructions` or `time` (cost nearer `good`'s than `bad`'s), with the input files changed between them and every session's journal edits to those files (a shell has no journal, and `bisect` says so); `explain_diff(case, base)`: how `•explain`'s records of `case` differ between two builds, each captured as the route test captures it (the worker's `route` request, `timing::warmed`), with listing diffs; a `good` newer than `bad` finds a fix. Note the installed id before changing anything. Don't restore old code in the shared tree for a baseline. - Every timing tool's `build=`: a kept build's id prefix, a `bpl` path, or `None` (installed; `installed` from the shell), resolved once at start, so another agent's `cargo develop` can't change a run. Time both builds in one call: kernel `time_cases(cases, build=[base, None])` (`base`: the noted id); shell `bpl-tests time_cases CASE --build BASE NEW`. Builds run in fresh workers taking turns in alternating order, `rounds` times; the result holds each case's fastest time per item per build, in picoseconds. `compare_costs(*result)` lists cases that moved. -- `compile=` and `defer=` on `fresh`, `time_cases`, `instructions`, `speed_costs` and `cargo_costs` set the environment of the processes they start, with `native=` (`worker.environment`, which each takes through `@delegates`). `compile=` sets `BASEDPL_COMPILE`: `always` compiles every block stage on its first run, `never` compiles none, None keeps the compile policy (`settings::Compiles`). `defer=` sets `BASEDPL_DEFER`: `all` makes every call wait, as fusion at any size runs; `reference` takes the Mac's costs; None, this platform's (`settings::waits`). Shell: `--compile never`, `--defer all`. +- The shared `native=` and `compile=` options come from `worker.environment`, which `fresh`, `time_cases`, `instructions`, `speed_costs` and `cargo_costs` take through `@delegates`. `compile=` sets `BASEDPL_COMPILE`: `always` compiles every block stage on its first run, `never` compiles none, and None keeps the compile policy (`settings::Compiles`). Shell: `--compile never`. For a controlled deferral experiment, set `BASEDPL_MIN_ITEMS` in the worker's environment before starting it: 1 lowers the result-count threshold to every nonempty fusible call; a value above the workload size keeps those calls eager. There is no `defer=` helper option. - Timing sessions, and the processes `instructions`, `profile` and `call_profile` start, seed random generators at 0: every process and build draws the same data. Builds from before a `rustymath::timing::COPIES` change divide by a different copy. - On this Mac a fused case on a large input can cost up to about 15% more in one process than another (depending on whether `malloc` starts the input on a 32 KB boundary, where every block of a block stage then ends): retime with more `rounds` before calling a lone difference that size a regression. - `pytest_costs`, `run_pytest`: `build=` takes a rustymath build id, for rustymath Python API changes. macOS native timings of matrix products and decompositions mostly measure Accelerate; rustymath's DEV.md says how to time its own kernels. @@ -161,6 +162,12 @@ Browser: during development, run `cargo wasm` only for browser work. Agents use `bpl-tests disasm PATTERN`: machine code (`objdump`, installed `bpl`) of the function whose demangled name matches regex `PATTERN`; `--build BUILD`: another build; several matches → lists names. Always-inlined functions have no code of their own: read their callers. +`prerelease` reruns the existing `arrays.bpl:copies` cases `-v`, `v+w` and `v×2` with compilation disabled, against their existing budgets. This checks native execution without JIT-generated arithmetic. It uses the same build and speed-test executable as the ordinary speed checks. + +For an uncompiled slowdown, measure the affected existing cases with `compile="never"`, then use `profile` on the same pinned build. If a per-item formula has self time, inspect the caller's loop with `disasm`: a scalar call inside that loop can prevent vectorization. Tiny forwarding methods crossing crate boundaries need ordinary `#[inline]` hints; don't force large algorithms inline or rely on shipping LTO to remove the call. + +A high copy count does not by itself show extra memory passes. Compare raw ns/item and a one-read/one-write control at nearby sizes in the same execution mode. Addition touches three arrays; a negating copy touches two. They can cross cache tiers at different sizes. [The addition diagnosis](meta/elementwise-add-cost.md) records a controlled example. + JIT kernels exist only while a process runs. `BASEDPL_JIT_DISASM` set → `bpl` prints each kernel it compiles to stderr: a description line, then machine code. `bpl-tests jit_disasm CASE --setup SETUP --prefs PREFS` runs a case so: `bpl-tests jit_disasm '+/ux>127ₓ₁' --setup 'ux←(¿(2*22)⍴256)ₓ₁' --prefs '•prefs ["kinds":"static"]'`. A stage compiles on its first run only where that run repays compiling (`blocks::Blocks::repays`): its items × the passes its parts save × `SAVED_NS` exceed its parts' compile cost, which is higher for a fold or a comparison. `--x86`: the x86-64 code CI and Linux run. First builds basedpl's library and its linked rustymath library for Linux x86-64 into `/tmp/aai-ws-x86`: minutes after a change → only for an x86-code question. Unfinished edit stops basedpl compiling → reads rustymath's library alone, saying so first. Linux: macOS builds omit rustymath's native linear algebra kernels. Libraries only: the Python extension crates can't build for Linux on this Mac. Their `release` profile matches CI's `test` profile in x86-64 flags and optimization. The build sets `CC_x86_64_unknown_linux_gnu=clang` itself; don't move it into `.cargo/config.toml` (CI's Linux release builds read that file in a manylinux container without `clang`). Don't use `--emit asm`: it splits code into other codegen units and inlines differently. @@ -365,7 +372,7 @@ Rank tries `whole` before `each_pair`. `whole`, in order: Axis nodes select or move axes, reusing Rank where the function has no direct axis meaning. Vector-axis reduction: the general composed-ravel rule, keeping axis order and the whole seed; single-axis reductions keep direct kernels. Layout-only changes share storage. Structural results keep renderers where rank permits; scans and in-place changes copy complete layouts. Match ignores renderers. -Inverse nodes propagate fixed-argument sides through composition. `f⁻¹←g`, `f∂←g` replace the binding with a `Ruled` node: the function plus its hand-written inverse and derivative; calling it calls the function; `⁻¹` and `∂` use the rules; Under counts an attached inverse as keeping every item. Candidate-building primitive inverses validate by applying the primitive again. Inverse scans share forward scan's seed and axis validation. Agenda calls its selector once, then passes the original arguments to the selected branch. Computed operands of Rank, Axis, Stencil and Agenda: shared operand evaluation. Python builds these nodes directly, not by generating source. +Inverse nodes propagate fixed-argument sides through composition. `f⁻←g`, `f∂←g` replace the binding with a `Ruled` node: the function plus its hand-written inverse and derivative; calling it calls the function; `⁻` and `∂` use the rules; Under counts an attached inverse as keeping every item. Candidate-building primitive inverses validate by applying the primitive again. Inverse scans share forward scan's seed and axis validation. Agenda calls its selector once, then passes the original arguments to the selected branch. Computed operands of Rank, Axis, Stencil and Agenda: shared operand evaluation. Python builds these nodes directly, not by generating source. Form rewriter (`eval/rewrite.rs`): the one home of fast combinations. @@ -423,7 +430,7 @@ Rustymath owns mathematical algorithms; basedpl owns pervasion, storage, numeric Hash maps and sets: `rustymath::hashing`'s `HashMap`, `HashSet` and `Hashing` (rustymath's DEV.md), not another hasher → every lookup in both projects inlines its hash the same way. -`•decompose`/`•decompose⁻¹` share domain reading and `Factor` dispatch, with options for kind, vectors and complete QR. Polynomial adapters: compact evaluation or generic field operations; gradients differentiate coefficients before evaluating. Number-theory adapters: cancellation checks, result cell assembly. Distribution constructors expose native density, CDF, quantile and sample functions; compact continuous calls use slice objects; discrete samples and finite discrete quantiles are exact integers. +`•decompose`/`•decompose⁻` share domain reading and `Factor` dispatch, with options for kind, vectors and complete QR. Polynomial adapters: compact evaluation or generic field operations; gradients differentiate coefficients before evaluating. Number-theory adapters: cancellation checks, result cell assembly. Distribution constructors expose native density, CDF, quantile and sample functions; compact continuous calls use slice objects; discrete samples and finite discrete quantiles are exact integers. `•rand seed`: a key and a count of draw calls behind a mutex (`rustymath::random::Counter`), shared by every copy of its record. `¿`, and a distribution's `sample` without a generator, draw from the session's generator; `•prefs ["seed":n]` reseeds it. Draw call c reads stream c under the key from its start. Item k of a call reads its packed words, then its own region (`rustymath::random::Items`), so an item reads the same words on every route. The algorithm is fixed, but range or distribution sampler changes can change draws: the reference sequence detects that. Sampling from a generator record uses its roll function to recover the same generator. @@ -443,9 +450,9 @@ Fusion (`fusion.rs`; its module doc owns eligibility rules and limits: don't cop - A read forces via `ArrayData::storage`: gathers in the programs of pending arrays it reads in order (`Pending::gathered`), runs the result through its plan's units. `program::planned` plans each program structure once, from a copy with inputs for held arrays, reusing the plan for later forces of the same chain on new arrays. Shape, kind and prototype readers (`Value::len`, `Value::environment`) don't force. `Value::force` forces, as timing does. - Fusion and dfn traces both add nodes via `program::add`, which returns an earlier node making the same value: a held atom of the same identity, the same held array, or the same step on the same values. - A pending array read in order that a name or another array also holds joins as an output (`Program::outputs`), computed in the same pass; a block stage stores its items with the result's. Other pending arrays the program reads but doesn't compute as steps compute first, alone (`Pending::gathered`): those not readable in order, and held copies. A fold of a held pending result stores that result as an output of the fold's pass, once for all readers (`fusion::fold`); a pending copy folds so, held or not. -- A block stage runs elementwise steps block by block, in parts (`blocks::Part`): each run of steps lowering to machine operations that give their kernels' exact results (`blocks::lowered_op`) through one compiled kernel (`jit/src/lib.rs`); other steps through the same pervasive kernels (`pervasive::node`). A part whose kernel can't take a run's items (e.g. an input a broadcast reads as one item) runs those kernels for that run. A stage compiles all parts or none: where the same kernel compiled or ran before, or where one run of its items repays compiling (`blocks::Blocks::compiles`, `repays`). `settings::Compiles` can compile every stage or none: the reference harness; timings via `BASEDPL_COMPILE`. A call waits where fusing it saves more than its node costs (`fusion::waits`: `NODE_NS`, the allowance per call, against `SAVED_NS_PER_BYTE`); `BASEDPL_DEFER=all` makes every call wait, for timing what a node costs. +- A block stage runs elementwise steps block by block, in parts (`blocks::Part`): each run of steps lowering to machine operations that give their kernels' exact results (`blocks::lowered_op`) through one compiled kernel (`jit/src/lib.rs`); other steps through the same pervasive kernels (`pervasive::node`). A part whose kernel can't take a run's items (e.g. an input a broadcast reads as one item) runs those kernels for that run. A stage compiles all parts or none: where the same kernel compiled or ran before, or where one run of its items repays compiling (`blocks::Blocks::compiles`, `repays`). `settings::Compiles` can compile every stage or none: the reference harness; timings via `BASEDPL_COMPILE`. Deferral uses the result-count threshold `Sizes::min_items` and the call's eligibility (`fusion::defers`). - Each step writes a block to a buffer later steps reuse once its readers ran. A comparison read only by later steps of the stage writes floats 1 and 0 there (`Dyad::compared`, rustymath's `tolerant::Indicator`). `⍚` and `@[p]` run cases that can't fail on every item, selecting by a Boolean mask (`selected`, `eval/operators/agenda.rs`); `plan::select` gives its type and the maps its mask and cases read through; `fusion::select` defers it where the mask is such a comparison, else runs it at once via rustymath's `operation::choose` (`fusion::chosen`). A one-step stage runs only where it reads through a copy, recording no `fused` row. -- A structural copy of one source through rustymath `Steps` (`remapped`) large enough to wait (`fusion::waits`) is a pending copy (`fusion::copy`), any compact kind; its map is a `Mapping::Steps`, one entry per result axis, as broadcasting's are. Forcing composes a read of a copy with its map and moves a read with no repeat or fill under the pervasive calls it reaches (`fusion::resolved`): `2×⌽v+1` reads `v` backwards in one program. A copying read (rustymath's `Mapping::copies`) moves only under a one-array call. A call whose two array arguments copy same-layout arrays through one map runs on those arrays, its result copied once (`program::rules`, `copy_lifted`). Other arrays: a `Copy` step, which a block stage reads through without making the copy. A step reads items in order under another shape through its operand's `program::Read`. +- A structural copy of one source through rustymath `Steps` (`remapped`) large enough to wait (`fusion::defers`) is a pending copy (`fusion::copy`), any compact kind; its map is a `Mapping::Steps`, one entry per result axis, as broadcasting's are. Forcing composes a read of a copy with its map and moves a read with no repeat or fill under the pervasive calls it reaches (`fusion::resolved`): `2×⌽v+1` reads `v` backwards in one program. A copying read (rustymath's `Mapping::copies`) moves only under a one-array call. A call whose two array arguments copy same-layout arrays through one map runs on those arrays, its result copied once (`program::rules`, `copy_lifted`). Other arrays: a `Copy` step, which a block stage reads through without making the copy. A step reads items in order under another shape through its operand's `program::Read`. - A block stage shrinks blocks whose buffers wouldn't fit in cache. Rustymath's `agreement::expanded` reads a forward run in place, else copies via `gather::walk_plan`. A `Mapping::Steps` keeps the plan `Steps::simplify` makes on its first walk (`agreement::Route`) for every later block. A copy no call reads through is walked as `remapped` walks it. - Fused and ordinary folds share rustymath's `reduce::along`/`fold_into` traversal. A block stage of one compiled part folds the last step's items itself, handing `reduce::fold_pieces` each block's total (`reduce::Part`): sums, float products, maxima, minima. A float fold keeps as many running results as rustymath's folds (`reduce::running`). Boolean folds, float `⌈`/`⌊` folds, and blocks crossing a lane go through the last step's block. - On the pending route, a fold of a windows copy along its own axis slides over the copy's source (`pervasive::slid`, `mapped_fold`: rustymath's `reduce::fold_route` with the copy's source, `structure::Slide`, `reduce::sliding`); a fold along two window axes folds both in one pass (`sliding_2d`). A short fold a stage folds as one step per window position (`blocks::short_window`) runs as a block stage reading the copy's source at each position through the copies' maps: in a dfn's program, and on the pending route where the copy reads another pending copy (e.g. padding) into a result whose last axis exceeds `DIRECT` (`fusion::staged`). Under `∂`, `fold_rule` takes a maximum's or minimum's positions in such windows from the source (`pervasive::mapped_picks`); under a trace, `Step::Picks` gives each lane's pick, via the untraced mask's function (`derivative::chosen`). A traced `⌈`/`⌊` scan, or picks from sliding windows, stops the trace. @@ -572,7 +579,7 @@ The playground and gallery load released assets from jsDelivr, or local `nbs/pla `optimize.rs`: `⇄` on `rustymath::optimize`; the left argument holds options. A method that can use a derivative (none named, L-BFGS, Levenberg–Marquardt, CG, MINRES) → one linearisation at the start (`derivative::linearized`) finds whether `∂` differentiates the function and gives its value there. `Goal` evaluates the function at points in the start's layout for `minimize`; under L-BFGS each value comes from a linearisation that `gradient` transposes at the same point (`linearized_at`) → one function run per value and gradient. With `"solve":$t`, `Residual` is the `Mapping` `solve` takes, or `solve_affine` with `"linear":$t` too. Where `∂` differentiates the function, `Residual` gives JVPs and vector–Jacobian products from one record per point, or the start's record for an affine function. A later point where `∂` gives UNSUPPORTED → `Residual::jvp` declines; rustymath takes forward differences for that Jacobian and later ones. A square linear system takes no transpose products → GMRES on A, not the normal equations. Option `precondition`'s function gives the preconditioner. -CSV columns, JSON arrays and Python lists share `data::imported`: a non-fill finite float, or any nonfinite float, promotes to float when every number converts losslessly; finite fills alone don't; otherwise items keep their kinds. JSON objects and Python dicts read as literals do (`keyed::vector`, `Value::from_parts`). Binary data: exact bytes. Writes validate before opening and create exclusively unless overwrite is explicit. Exports: `•csv⁻¹`, `•json⁻¹`, `•xml⁻¹`. +CSV columns, JSON arrays and Python lists share `data::imported`: a non-fill finite float, or any nonfinite float, promotes to float when every number converts losslessly; finite fills alone don't; otherwise items keep their kinds. JSON objects and Python dicts read as literals do (`keyed::vector`, `Value::from_parts`). Binary data: exact bytes. Writes validate before opening and create exclusively unless overwrite is explicit. Exports: `•csv⁻`, `•json⁻`, `•xml⁻`. `regex.rs` shares host regex state between match and replace native functions; hosts keep their own regex syntax. `xml.rs`: keyed element trees; XML escaping and validation kept apart from HTML raw script/style and void-element rules. SVG has its own renderer. `image.rs` owns pictures, their options and animation timing, as plain pixel buffers; `basedpl-image` decodes and encodes via ImageIO on Apple targets (`image/src/imageio.rs`), the `image` and `png` crates elsewhere (`image/src/codecs.rs`). Native and browser renderers use the MIME and byte formats their frontends support. `plot.rs` stores specs and draws SVG on demand; the renderer owns defaults and validation. Rendering options are documented in their user pages and implementation comments. diff --git a/editors/jetbrains/BPL.tmbundle/Syntaxes/bpl.tmLanguage.json b/editors/jetbrains/BPL.tmbundle/Syntaxes/bpl.tmLanguage.json index b84a65e7..ec30e69e 100644 --- a/editors/jetbrains/BPL.tmbundle/Syntaxes/bpl.tmLanguage.json +++ b/editors/jetbrains/BPL.tmbundle/Syntaxes/bpl.tmLanguage.json @@ -45,7 +45,7 @@ "name": "string.quoted.single.bpl" }, { - "match": "(?:_\\d+|_\\S|_|[^\\s\\p{Cc}0-9←⎕•⍺⍵⍶⍹∇⍢⍝⋄⁝\\?¯∞⍬\\+\\-×÷⌈⌊\\|\\*⍟○π√!∧∨⊼⊽\\~=≠<≤>≥⍳↦:⍴≢≡,⍪⊂⊃⊆∊∪∩⍋⍒↑↓⌽⊖⍉⊤⊥⍎⍕⌷⌹\\#↕⍭⨸⌻⍸⍷⊢⊣¿¨ᵇ⍨↣⍤∘⍠⍥↢\\.⊗⌸⍣⇄⊘@∂⍚⌺/⌿\\\\⍀⁰¹²³⁴⁵⁶⁷⁸⁹⁻ᵀᵘₓ₀₁₂₃₄₅₆₇₈₉₋ₓ₁ₓ₂ₓ₄ₓ₈ₓₕₛₚₐᵣ\\(\\)\\[\\]\\{\\}'\";\\$_ₑⱼᵣ])+", + "match": "(?:_\\d+|_\\S|_|[^\\s\\p{Cc}0-9←⎕•⍺⍵⍶⍹∇⍢⍝⋄⁝\\?¯∞⍬\\+\\-×÷⌈⌊\\|\\*⍟○π√!∧∨⊼⊽\\~=≠<≤>≥⍳↦:⍴≢≡,⍪⊂⊃⊆∊∪∩⍋⍒↑↓⌽⊖⍉⊤⊥⍎⍕⌷⌹\\#↕⍭⨸⌻⍸⍷⊢⊣¿¨ᵇ⍨↣⍤∘⍠⍥↢\\.⊗⌸⍣⇄⊘@∂⍚⌺/⌿\\\\⍀⁻⁰¹²³⁴⁵⁶⁷⁸⁹⁻ᵀᵘₓ₀₁₂₃₄₅₆₇₈₉₋ₓ₁ₓ₂ₓ₄ₓ₈ₓₕₛₚₐᵣ\\(\\)\\[\\]\\{\\}'\";\\$_ₑⱼᵣ])+", "name": "variable.other.bpl" }, { @@ -94,7 +94,7 @@ "name": "keyword.operator.monadic.bpl" }, { - "match": "[¨ᵇ⍨⊗⌸⇄∂/⌿\\\\⍀⁰¹²³⁴⁵⁶⁷⁸⁹⁻ᵀᵘₓ]", + "match": "[¨ᵇ⍨⊗⌸⇄∂/⌿\\\\⍀⁻⁰¹²³⁴⁵⁶⁷⁸⁹⁻ᵀᵘₓ]", "name": "keyword.operator.monadic.bpl" }, { diff --git a/editors/jetbrains/BPL.tmbundle/Syntaxes/hpl.tmLanguage.json b/editors/jetbrains/BPL.tmbundle/Syntaxes/hpl.tmLanguage.json index 795c239d..1c762385 100644 --- a/editors/jetbrains/BPL.tmbundle/Syntaxes/hpl.tmLanguage.json +++ b/editors/jetbrains/BPL.tmbundle/Syntaxes/hpl.tmLanguage.json @@ -19,7 +19,7 @@ "applyEndPatternLast": true }, { - "match": "(?:_\\d+|_\\S|_|[^\\s\\p{Cc}0-9←是⎕出入•系⍺甲⍵乙⍶丙⍹丁∇递遞⍢循⍝注⋄句⁝隔\\?若¯负負∞永⍬空\\+加\\-减減×乘号號÷除⌈上⌊下\\|绝余絕餘\\*幂冪⍟对對○圆圓π周√根!阶拣階揀∧极且極∨析或⊼方否⊽双莫雙\\~非无無=等≠别初別<小≤限退>大≥达进達進⍳位↦至:键卸鍵⍴形≢数异數異≡同深,连連⍪接矩⊂包⊃选合選⊆巢段∊属展屬∪并独併獨∩交重⍋升⍒降↑取↓去拆⌽转轉⊖纵縱⍉翻⊤编編⊥译譯⍎行⍕文⌷索⌹解逆\\#复複↕窗⍭素⨸因⌻零值⍸处间處間⍷寻群尋⊢右⊣左¿掷抽擲¨遍ᵇ按⍨己换換↣先⍤秩∘承⍠轴軸⍥经經↢后後\\.内內⊗表⌸组組⍣次止⇄优優⊘兼@于於∂导梯導⍚择擇⌺邻鄰/总總⌿汇匯\\\\累⍀续續⁰¹²³⁴⁵⁶⁷⁸⁹⁻ᵀᵘₓ₀₁₂₃₄₅₆₇₈₉₋ₓ₁ₓ₂ₓ₄ₓ₈ₓₕₛₚₐᵣ\\(\\)\\[\\]\\{\\}'\";\\$_ₑⱼᵣ])+", + "match": "(?:_\\d+|_\\S|_|[^\\s\\p{Cc}0-9←是⎕出入•系⍺甲⍵乙⍶丙⍹丁∇递遞⍢循⍝注⋄句⁝隔\\?若¯负負∞永⍬空\\+加\\-减減×乘号號÷除⌈上⌊下\\|绝余絕餘\\*幂冪⍟对對○圆圓π周√根!阶拣階揀∧极且極∨析或⊼方否⊽双莫雙\\~非无無=等≠别初別<小≤限退>大≥达进達進⍳位↦至:键卸鍵⍴形≢数异數異≡同深,连連⍪接矩⊂包⊃选合選⊆巢段∊属展屬∪并独併獨∩交重⍋升⍒降↑取↓去拆⌽转轉⊖纵縱⍉翻⊤编編⊥译譯⍎行⍕文⌷索⌹解逆\\#复複↕窗⍭素⨸因⌻零值⍸处间處間⍷寻群尋⊢右⊣左¿掷抽擲¨遍ᵇ按⍨己换換↣先⍤秩∘承⍠轴軸⍥经經↢后後\\.内內⊗表⌸组組⍣次止⇄优優⊘兼@于於∂导梯導⍚择擇⌺邻鄰/总總⌿汇匯\\\\累⍀续續⁻反⁰¹²³⁴⁵⁶⁷⁸⁹⁻ᵀᵘₓ₀₁₂₃₄₅₆₇₈₉₋ₓ₁ₓ₂ₓ₄ₓ₈ₓₕₛₚₐᵣ\\(\\)\\[\\]\\{\\}'\";\\$_ₑⱼᵣ])+", "name": "variable.other.bpl" }, { @@ -39,7 +39,7 @@ "name": "support.function.bpl" }, { - "match": "[遍按己换換表组組优優导梯導总總汇匯累续續]", + "match": "[遍按己换換表组組优優导梯導总總汇匯累续續反]", "name": "keyword.operator.monadic.bpl" }, { diff --git a/editors/jetbrains/BPL.xml b/editors/jetbrains/BPL.xml index 0cf9d492..21b6b5d9 100644 --- a/editors/jetbrains/BPL.xml +++ b/editors/jetbrains/BPL.xml @@ -524,6 +524,11 @@