├── docs ├── CNAME ├── favicon.ico ├── index.html └── install ├── .gitignore ├── cli ├── src │ ├── modules │ │ ├── mod.rs │ │ ├── args.rs │ │ └── shell.rs │ ├── util.rs │ ├── env.rs │ ├── input.rs │ └── main.rs └── Cargo.toml ├── rustfmt.toml ├── src ├── error.rs ├── lib.rs ├── core │ ├── env.rs │ ├── mod.rs │ ├── dictionary.rs │ └── lexer.rs ├── modules │ ├── crypto.rs │ ├── debug_utils.rs │ ├── stack_utils.rs │ ├── arithmetic.rs │ └── mod.rs └── util.rs ├── libs ├── Cargo.toml ├── src │ ├── Color.fif │ ├── lib.rs │ ├── FiftExt.fif │ ├── Fift.fif │ ├── GetOpt.fif │ ├── Disasm.fif │ ├── Lists.fif │ ├── Stack.fif │ ├── Lisp.fif │ └── TonUtil.fif ├── LICENSE.fif └── README.md ├── proc ├── Cargo.toml └── src │ └── lib.rs ├── Cargo.toml ├── LICENSE-MIT ├── .github └── workflows │ ├── master.yml │ └── release.yml ├── README.md └── LICENSE-APACHE /docs/CNAME: -------------------------------------------------------------------------------- 1 | fift.rs -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /.vscode 3 | /target 4 | -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/broxus/fift/HEAD/docs/favicon.ico -------------------------------------------------------------------------------- /cli/src/modules/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::args::CmdArgsUtils; 2 | pub use self::shell::ShellUtils; 3 | 4 | mod args; 5 | mod shell; 6 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | format_code_in_doc_comments = true 2 | imports_granularity = "Module" 3 | normalize_comments = true 4 | overflow_delimited_expr = true 5 | group_imports = "StdExternalCrate" 6 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | pub use anyhow::Error; 2 | 3 | #[derive(Debug, thiserror::Error)] 4 | #[error("Execution aborted: {reason}")] 5 | pub struct ExecutionAborted { 6 | pub reason: String, 7 | } 8 | 9 | #[derive(Debug, thiserror::Error)] 10 | #[error("Unexpected eof")] 11 | pub struct UnexpectedEof; 12 | -------------------------------------------------------------------------------- /libs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fift-libs" 3 | description = "A maintained collection of default Fift libraries" 4 | repository = "https://github.com/broxus/fift" 5 | version = "0.2.0" 6 | edition = "2024" 7 | rust-version = "1.88" 8 | include = ["src/**/*.rs", "src/**/*.fif", "LICENSE", "LICENSE.fif", "README.md"] 9 | license = "LGPL-2.1-or-later" 10 | -------------------------------------------------------------------------------- /proc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fift-proc" 3 | description = "Proc-macro helpers for fift" 4 | repository = "https://github.com/broxus/fift" 5 | version = "0.2.2" 6 | edition = "2024" 7 | include = ["src/**/*.rs", "../LICENSE-*", "../README.md"] 8 | license = "MIT OR Apache-2.0" 9 | 10 | [lib] 11 | proc-macro = true 12 | 13 | [dependencies] 14 | quote = "1.0" 15 | syn = "2.0" 16 | darling = "0.21.2" 17 | -------------------------------------------------------------------------------- /libs/src/Color.fif: -------------------------------------------------------------------------------- 1 | library Color 2 | { 27 emit } : esc 3 | { char " word 27 chr swap $+ 1 ' type does create } :_ make-esc" 4 | make-esc"[0m" ^reset 5 | make-esc"[30m" ^black 6 | make-esc"[31m" ^red 7 | make-esc"[32m" ^green 8 | make-esc"[33m" ^yellow 9 | make-esc"[34m" ^blue 10 | make-esc"[35m" ^magenta 11 | make-esc"[36m" ^cyan 12 | make-esc"[37m" ^white 13 | // bold 14 | make-esc"[30;1m" ^Black 15 | make-esc"[31;1m" ^Red 16 | make-esc"[32;1m" ^Green 17 | make-esc"[33;1m" ^Yellow 18 | make-esc"[34;1m" ^Blue 19 | make-esc"[35;1m" ^Magenta 20 | make-esc"[36;1m" ^Cyan 21 | make-esc"[37;1m" ^White 22 | -------------------------------------------------------------------------------- /cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fift-cli" 3 | description = "A CLI for the Fift esoteric language interpreter" 4 | repository = "https://github.com/broxus/fift" 5 | version = "0.2.2" 6 | edition = "2024" 7 | rust-version = "1.88" 8 | include = ["src/**/*.rs", "src/**/*.fif", "LICENSE", "README.md"] 9 | license = "LGPL-2.1-or-later" 10 | 11 | [[bin]] 12 | name = "fift" 13 | path = "src/main.rs" 14 | 15 | [dependencies] 16 | anyhow = "1.0" 17 | argh = "0.1" 18 | bitflags = "2.3" 19 | console = "0.16" 20 | rustyline = { version = "17.0", default-features = false } 21 | unicode-width = "0.2" 22 | tycho-vm = "0.2.0" 23 | 24 | fift = { path = "..", version = "=0.2.2" } 25 | fift-libs = { path = "../libs", version = "0.2.0" } 26 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate self as fift; 2 | 3 | use anyhow::Result; 4 | 5 | pub use self::core::Context; 6 | 7 | pub mod core; 8 | pub mod error; 9 | pub mod modules; 10 | pub mod util; 11 | 12 | impl Context<'_> { 13 | pub fn with_basic_modules(self) -> Result { 14 | use modules::*; 15 | self.with_module(BaseModule)? 16 | .with_module(Arithmetic)? 17 | .with_module(CellUtils)? 18 | .with_module(DictUtils)? 19 | .with_module(Control)? 20 | .with_module(DebugUtils)? 21 | .with_module(StackUtils)? 22 | .with_module(StringUtils)? 23 | .with_module(Crypto)? 24 | .with_module(VmUtils) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /libs/LICENSE.fif: -------------------------------------------------------------------------------- 1 | These files are part of TON Blockchain Library. 2 | 3 | TON Blockchain Library is free software: you can redistribute it and/or modify 4 | it under the terms of the GNU Lesser General Public License as published by 5 | the Free Software Foundation, either version 2 of the License, or 6 | (at your option) any later version. 7 | 8 | TON Blockchain Library is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Lesser General Public License for more details. 12 | 13 | You should have received a copy of the GNU Lesser General Public License 14 | along with TON Blockchain Library. If not, see . 15 | 16 | Copyright 2017-2020 Telegram Systems LLP 17 | -------------------------------------------------------------------------------- /libs/README.md: -------------------------------------------------------------------------------- 1 | ## A maintained collection of default Fift libraries 2 | 3 | Used by default in the [`fift-cli`](https://github.com/broxus/fift) module. 4 | 5 | ## List 6 | 7 | * [`Fift.fif`](./src/Fift.fif) - base Fift library. 8 | * [`FiftExt.fif`](./src/FiftExt.fif) - extension to the base Fift library. 9 | * [`Asm.fif`](./src/Asm.fif) - TVM assembly implementation. 10 | * [`Color.fif`](./src/Color.fif) - terminal output helpers. 11 | * [`Lists.fif`](./src/Lists.fif) - helpers for the lists syntax. 12 | * [`Stack.fif`](./src/Stack.fif) - stack manipulation helpers. 13 | * [`TonUtil.fif`](./src/TonUtil.fif) - blockchain specific helpers. 14 | * [`Lisp.fif`](./src/Lisp.fif) - simple Lisp implementation in Fift. 15 | 16 | ## License 17 | 18 | Licensed under GNU Lesser General Public License v2.1 ([LICENSE](./LICENSE) or ) 19 | 20 | Uses a modified version of [original fift libraries](https://github.com/ton-blockchain/ton/tree/master/crypto/fift/lib) ([LGPL-2.1](./fift-lib/LICENSE)). 21 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fift" 3 | description = "Rust implementation of the Fift esoteric language" 4 | repository = "https://github.com/broxus/fift" 5 | version = "0.2.2" 6 | edition = "2024" 7 | rust-version = "1.88" 8 | include = ["src/**/*.rs", "src/**/*.fif", "LICENSE-*", "README.md"] 9 | license = "MIT OR Apache-2.0" 10 | 11 | [lib] 12 | name = "fift" 13 | 14 | [workspace] 15 | members = ["proc", "cli", "libs"] 16 | 17 | [dependencies] 18 | ahash = "0.8" 19 | anyhow = "1.0" 20 | base64 = "0.22" 21 | bitflags = "2.9.1" 22 | crc = "3.0" 23 | hex = "0.4" 24 | num-bigint = "0.4" 25 | num-integer = "0.1" 26 | num-traits = "0.2" 27 | rand = "0.9" 28 | sha2 = "0.10" 29 | thiserror = "2.0" 30 | tracing = "0.1.41" 31 | tycho-crypto = { version = "0.4.0", features = ["rand9"] } 32 | unicode-segmentation = "1.0" 33 | 34 | tycho-types = { version = "0.2.1", default-features = false, features = [ 35 | "sync", 36 | "base64", 37 | "models", 38 | ] } 39 | tycho-vm = { version = "0.2.1", features = ["dump", "tracing"] } 40 | 41 | fift-proc = { path = "./proc", version = "=0.2.2" } 42 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /.github/workflows/master.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: master 4 | 5 | jobs: 6 | clippy: 7 | name: Clippy 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout sources 11 | uses: actions/checkout@v2 12 | 13 | - name: Install stable toolchain 14 | uses: actions-rs/toolchain@v1 15 | with: 16 | profile: minimal 17 | toolchain: stable 18 | override: true 19 | components: clippy 20 | 21 | - name: Run cargo clippy 22 | uses: actions-rs/cargo@v1 23 | with: 24 | command: clippy 25 | args: -- -D warnings 26 | 27 | test: 28 | name: Test Suite 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout sources 32 | uses: actions/checkout@v2 33 | 34 | - name: Install stable toolchain 35 | uses: actions-rs/toolchain@v1 36 | with: 37 | profile: minimal 38 | toolchain: stable 39 | override: true 40 | 41 | - name: Run cargo test 42 | uses: actions-rs/cargo@v1 43 | with: 44 | command: test 45 | 46 | fmt: 47 | name: Formatter 48 | runs-on: ubuntu-latest 49 | steps: 50 | - name: Checkout sources 51 | uses: actions/checkout@v2 52 | 53 | - name: Install nightly toolchain 54 | uses: actions-rs/toolchain@v1 55 | with: 56 | profile: minimal 57 | toolchain: nightly 58 | override: true 59 | components: rustfmt 60 | 61 | - name: Run cargo fmt 62 | run: cargo +nightly fmt --all -- --check 63 | -------------------------------------------------------------------------------- /libs/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::OnceLock; 3 | 4 | /// Returns a pair of name and contents of the base Fift library. 5 | pub fn base_lib() -> LibraryDefinition { 6 | def::fift() 7 | } 8 | 9 | /// Returns a map with all predefined libraries. 10 | pub fn all() -> &'static HashMap<&'static str, &'static str> { 11 | static MAP: OnceLock> = OnceLock::new(); 12 | MAP.get_or_init(|| { 13 | let mut libraries = HashMap::with_capacity(LIBRARIES.len()); 14 | for LibraryDefinition { name, content } in LIBRARIES { 15 | libraries.insert(*name, *content); 16 | } 17 | libraries 18 | }) 19 | } 20 | 21 | pub struct LibraryDefinition { 22 | pub name: &'static str, 23 | pub content: &'static str, 24 | } 25 | 26 | macro_rules! define_libs { 27 | ($prefix:literal, [ 28 | $($name:ident => $file:literal),*$(,)? 29 | ]) => { 30 | /// Raw libraries. 31 | pub mod def { 32 | $(/// Returns a content of a ` 33 | #[doc = $file] 34 | /// ` library. 35 | pub const fn $name() -> crate::LibraryDefinition { 36 | crate::LibraryDefinition { 37 | name: $file, 38 | content: include_str!(concat!($prefix, $file)), 39 | } 40 | })* 41 | } 42 | 43 | const LIBRARIES: &[LibraryDefinition] = &[ 44 | $(def::$name()),* 45 | ]; 46 | }; 47 | } 48 | 49 | define_libs!( 50 | "./", 51 | [ 52 | asm => "Asm.fif", 53 | disasm => "Disasm.fif", 54 | color => "Color.fif", 55 | fift => "Fift.fif", 56 | fift_ext => "FiftExt.fif", 57 | lisp => "Lisp.fif", 58 | lists => "Lists.fif", 59 | stack => "Stack.fif", 60 | ton_util => "TonUtil.fif", 61 | get_opt => "GetOpt.fif", 62 | ] 63 | ); 64 | -------------------------------------------------------------------------------- /src/core/env.rs: -------------------------------------------------------------------------------- 1 | use std::io::BufRead; 2 | 3 | pub trait Environment { 4 | fn now_ms(&self) -> u64; 5 | 6 | fn get_env(&self, name: &str) -> Option; 7 | 8 | fn file_exists(&self, name: &str) -> bool; 9 | 10 | fn write_file(&mut self, name: &str, contents: &[u8]) -> std::io::Result<()>; 11 | 12 | fn read_file(&mut self, name: &str) -> std::io::Result>; 13 | 14 | fn read_file_part(&mut self, name: &str, offset: u64, len: u64) -> std::io::Result>; 15 | 16 | fn include(&self, name: &str) -> std::io::Result; 17 | } 18 | 19 | pub struct SourceBlock { 20 | name: String, 21 | buffer: Box, 22 | } 23 | 24 | impl SourceBlock { 25 | pub fn new, B: BufRead + 'static>(name: N, buffer: B) -> Self { 26 | Self { 27 | name: name.into(), 28 | buffer: Box::new(buffer), 29 | } 30 | } 31 | 32 | pub fn name(&self) -> &str { 33 | &self.name 34 | } 35 | 36 | pub fn buffer_mut(&mut self) -> &mut dyn BufRead { 37 | &mut self.buffer 38 | } 39 | } 40 | 41 | pub struct EmptyEnvironment; 42 | 43 | impl Environment for EmptyEnvironment { 44 | fn now_ms(&self) -> u64 { 45 | 0 46 | } 47 | 48 | fn get_env(&self, _: &str) -> Option { 49 | None 50 | } 51 | 52 | fn file_exists(&self, _: &str) -> bool { 53 | false 54 | } 55 | 56 | fn write_file(&mut self, _: &str, _: &[u8]) -> std::io::Result<()> { 57 | Ok(()) 58 | } 59 | 60 | fn read_file(&mut self, name: &str) -> std::io::Result> { 61 | Err(not_found(name)) 62 | } 63 | 64 | fn read_file_part(&mut self, name: &str, _: u64, _: u64) -> std::io::Result> { 65 | self.read_file(name) 66 | } 67 | 68 | fn include(&self, name: &str) -> std::io::Result { 69 | Err(not_found(name)) 70 | } 71 | } 72 | 73 | fn not_found(name: &str) -> std::io::Error { 74 | std::io::Error::new( 75 | std::io::ErrorKind::NotFound, 76 | format!("`{name}` file not found"), 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v[0-9]+.* 7 | 8 | jobs: 9 | create-release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: taiki-e/create-gh-release-action@v1 14 | with: 15 | token: ${{ secrets.GITHUB_TOKEN }} 16 | 17 | upload-assets: 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | include: 22 | - { os: macos-latest, target: x86_64-apple-darwin, build_tool: cargo } 23 | - { 24 | os: macos-latest, 25 | target: aarch64-apple-darwin, 26 | build_tool: cargo, 27 | } 28 | - { 29 | os: ubuntu-latest, 30 | target: x86_64-unknown-linux-gnu, 31 | glibc: ".2.17", 32 | build_tool: cargo-zigbuild, 33 | } 34 | - { 35 | os: ubuntu-latest, 36 | target: aarch64-unknown-linux-gnu, 37 | glibc: ".2.17", 38 | build_tool: cargo-zigbuild, 39 | } 40 | - { 41 | os: ubuntu-latest, 42 | target: x86_64-unknown-linux-musl, 43 | build_tool: cargo-zigbuild, 44 | } 45 | - { 46 | os: ubuntu-latest, 47 | target: aarch64-unknown-linux-musl, 48 | build_tool: cargo-zigbuild, 49 | } 50 | - { 51 | os: windows-latest, 52 | target: x86_64-pc-windows-msvc, 53 | build_tool: cargo, 54 | } 55 | - { 56 | os: windows-latest, 57 | target: aarch64-pc-windows-msvc, 58 | build_tool: cargo, 59 | } 60 | 61 | runs-on: ${{ matrix.os }} 62 | steps: 63 | - name: Checkout repository 64 | uses: actions/checkout@v3 65 | 66 | - name: Setup Rust 67 | uses: dtolnay/rust-toolchain@stable 68 | with: 69 | targets: ${{ matrix.target }} 70 | 71 | - name: Build and upload binary 72 | uses: taiki-e/upload-rust-binary-action@v1 73 | with: 74 | bin: fift 75 | target: ${{ matrix.target }}${{ matrix.glibc || '' }} 76 | build_tool: ${{ matrix.build_tool }} 77 | token: ${{ secrets.GITHUB_TOKEN }} 78 | checksum: sha512 79 | manifest_path: cli/Cargo.toml 80 | -------------------------------------------------------------------------------- /cli/src/modules/args.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use anyhow::Result; 4 | use fift::core::*; 5 | use tycho_vm::SafeRc; 6 | 7 | pub struct CmdArgsUtils { 8 | name: SafeRc, 9 | args: Vec>, 10 | } 11 | 12 | impl CmdArgsUtils { 13 | pub fn new(args: Vec) -> Self { 14 | let mut args = args.into_iter(); 15 | 16 | let name = SafeRc::new_dyn_fift_value(args.next().unwrap_or_default()); 17 | 18 | let args = args.map(SafeRc::new_dyn_fift_value).collect::>(); 19 | 20 | Self { name, args } 21 | } 22 | } 23 | 24 | #[fift_module] 25 | impl CmdArgsUtils { 26 | #[init] 27 | fn init(&self, d: &mut Dictionary) -> Result<()> { 28 | d.define_word( 29 | "$0 ", 30 | RcFiftCont::new_dyn_fift_cont(cont::LitCont(self.name.clone())), 31 | )?; 32 | 33 | let mut list = Stack::make_null(); 34 | for (i, arg) in self.args.iter().enumerate().rev() { 35 | list = cons(arg.clone(), list); 36 | d.define_word( 37 | format!("${} ", i + 1), 38 | RcFiftCont::new_dyn_fift_cont(cont::LitCont(arg.clone())), 39 | )?; 40 | } 41 | 42 | d.define_word( 43 | "$# ", 44 | RcFiftCont::new_dyn_fift_cont(cont::IntLitCont::from(self.args.len())), 45 | )?; 46 | 47 | let mut all_args = Vec::with_capacity(1 + self.args.len()); 48 | all_args.push(self.name.clone()); 49 | all_args.extend_from_slice(&self.args); 50 | d.define_word("$() ", RcFiftCont::new_dyn_fift_cont(CmdArgCont(all_args)))?; 51 | 52 | d.define_word( 53 | "$* ", 54 | RcFiftCont::new_dyn_fift_cont(cont::LitCont(SafeRc::new_dyn_fift_value( 55 | SharedBox::new(list), 56 | ))), 57 | )?; 58 | 59 | Ok(()) 60 | } 61 | } 62 | 63 | #[derive(Clone)] 64 | struct CmdArgCont(Vec>); 65 | 66 | impl FiftCont for CmdArgCont { 67 | fn run(self: Rc, ctx: &mut Context) -> Result> { 68 | let n = ctx.stack.pop_smallint_range(0, 999999)? as usize; 69 | match self.0.get(n).cloned() { 70 | None => ctx.stack.push_null()?, 71 | Some(value) => ctx.stack.push_raw(value)?, 72 | } 73 | Ok(None) 74 | } 75 | 76 | fn fmt_name(&self, _: &Dictionary, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 77 | f.write_str("$()") 78 | } 79 | } 80 | 81 | fn cons(head: SafeRc, tail: SafeRc) -> SafeRc { 82 | SafeRc::new_dyn_fift_value(vec![head, tail]) 83 | } 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fift   [![crates-io-batch]][crates-io-link] [![docs-badge]][docs-url] [![rust-version-badge]][rust-version-link] [![workflow-badge]][workflow-link] 2 | 3 | [crates-io-batch]: https://img.shields.io/crates/v/fift.svg 4 | 5 | [crates-io-link]: https://crates.io/crates/fift 6 | 7 | [docs-badge]: https://docs.rs/fift/badge.svg 8 | 9 | [docs-url]: https://docs.rs/fift 10 | 11 | [rust-version-badge]: https://img.shields.io/badge/rustc-1.88+-lightgray.svg 12 | 13 | [rust-version-link]: https://blog.rust-lang.org/2025/06/26/Rust-1.88.0/ 14 | 15 | [workflow-badge]: https://img.shields.io/github/actions/workflow/status/broxus/fift/master.yml?branch=master 16 | 17 | [workflow-link]: https://github.com/broxus/fift/actions?query=workflow%3Amaster 18 | 19 | Rust implementation of the Fift esoteric language. 20 | 21 | Try it online: https://play.fift.rs 22 | 23 | ## Installation 24 | 25 | Compile from source: 26 | ```bash 27 | curl https://sh.rustup.rs -sSf | sh 28 | cargo install --locked fift-cli 29 | ``` 30 | 31 | Or install as a binary: 32 | ```bash 33 | curl -fsSL https://fift.rs/install | bash 34 | ``` 35 | 36 | ## Usage 37 | 38 | ``` 39 | Usage: fift [] [-n] [-i] [-I ] [-L ] 40 | 41 | A simple Fift interpreter. Type `bye` to quie, or `words` to get a list of all commands 42 | 43 | Positional Arguments: 44 | source_files a list of source files to execute (stdin will be used if 45 | empty) 46 | 47 | Options: 48 | -n, --bare do not preload standard preamble file `Fift.fif` 49 | -i, --interactive force interactive mode even if explicit source file names 50 | are indicated 51 | -I, --include sets color-separated library source include path. If not 52 | indicated, $FIFTPATH is used instead 53 | -L, --lib sets an explicit path to the library source file. If not 54 | indicated, a default one will be used 55 | --help display usage information 56 | -v, --version print version information and exit 57 | -s script mode: use first argument as a fift source file and 58 | import remaining arguments as $n 59 | ``` 60 | 61 | ## Contributing 62 | 63 | We welcome contributions to the project! If you notice any issues or errors, feel free to open an issue or submit a pull request. 64 | 65 | ## License 66 | 67 | * The `fift` and `fift-proc` library crates are licensed under either of 68 | * Apache License, Version 2.0 ([/LICENSE-APACHE](LICENSE-APACHE) or ) 69 | * MIT license ([/LICENSE-MIT](LICENSE-MIT) or ) 70 | 71 | at your option. 72 | 73 | * The `fift-cli` and `fift-libs` crates are licensed under 74 | * GNU Lesser General Public License v2.1 ([/cli/LICENSE](./cli/LICENSE) or ) 75 | -------------------------------------------------------------------------------- /cli/src/util.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicBool, Ordering}; 2 | 3 | pub const VERSION: &str = env!("CARGO_PKG_VERSION"); 4 | 5 | static FALLBACK_TO_HELP: AtomicBool = AtomicBool::new(true); 6 | 7 | pub struct ArgsOrVersion(pub T); 8 | 9 | impl argh::TopLevelCommand for ArgsOrVersion {} 10 | 11 | impl argh::FromArgs for ArgsOrVersion { 12 | fn from_args(command_name: &[&str], args: &[&str]) -> Result { 13 | /// Also use argh for catching `--version`-only invocations 14 | #[derive(Debug, argh::FromArgs)] 15 | struct Version { 16 | /// print version information and exit 17 | #[argh(switch, short = 'v')] 18 | pub version: bool, 19 | } 20 | 21 | match Version::from_args(command_name, args) { 22 | Ok(v) if v.version => { 23 | FALLBACK_TO_HELP.store(false, Ordering::Release); 24 | 25 | Err(argh::EarlyExit { 26 | output: format!("{} {}", command_name.first().unwrap_or(&""), VERSION), 27 | status: Ok(()), 28 | }) 29 | } 30 | Err(exit) if exit.status.is_ok() => { 31 | let help = match T::from_args(command_name, &["--help"]) { 32 | Ok(_) => unreachable!(), 33 | Err(exit) => exit.output, 34 | }; 35 | Err(argh::EarlyExit { 36 | output: format!("{help} -v, --version print version information and exit"), 37 | status: Ok(()), 38 | }) 39 | } 40 | _ => T::from_args(command_name, args).map(|app| Self(app)), 41 | } 42 | } 43 | } 44 | 45 | pub struct RestArgs(pub T, pub Vec, pub D); 46 | 47 | impl argh::TopLevelCommand for RestArgs {} 48 | 49 | impl argh::FromArgs for RestArgs { 50 | fn from_args(command_name: &[&str], args: &[&str]) -> Result { 51 | let (args, rest_args) = if let Some(pos) = args.iter().position(|arg| *arg == D::DELIM) { 52 | let (args, rest) = args.split_at(pos); 53 | (args, &rest[1..]) 54 | } else { 55 | (args, [].as_slice()) 56 | }; 57 | 58 | match T::from_args(command_name, args) { 59 | Ok(args) => Ok(Self( 60 | args, 61 | rest_args.iter().map(ToString::to_string).collect(), 62 | D::default(), 63 | )), 64 | Err(exit) if exit.status.is_ok() && FALLBACK_TO_HELP.load(Ordering::Acquire) => { 65 | let help = match T::from_args(command_name, &["--help"]) { 66 | Ok(_) => unreachable!(), 67 | Err(exit) => exit.output, 68 | }; 69 | Err(argh::EarlyExit { 70 | output: format!("{help}\n {:<16} {}", D::DELIM, D::DESCR), 71 | status: Ok(()), 72 | }) 73 | } 74 | Err(exit) => Err(exit), 75 | } 76 | } 77 | } 78 | 79 | pub trait RestArgsDelimiter: Default { 80 | const DELIM: &'static str; 81 | const DESCR: &'static str; 82 | } 83 | -------------------------------------------------------------------------------- /cli/src/env.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::{BufReader, Read, Result, Seek, SeekFrom}; 3 | use std::path::{Path, PathBuf}; 4 | 5 | use fift::core::{Environment, SourceBlock}; 6 | 7 | pub struct SystemEnvironment { 8 | include_dirs: Vec, 9 | } 10 | 11 | impl SystemEnvironment { 12 | pub fn with_include_dirs(dirs: &str) -> Self { 13 | let dirs = dirs.trim(); 14 | let include_dirs = if dirs.is_empty() { 15 | Vec::new() 16 | } else { 17 | dirs.split(':') 18 | .map(|item| PathBuf::from(item.trim())) 19 | .collect() 20 | }; 21 | Self { include_dirs } 22 | } 23 | 24 | fn resolve_file(&self, name: &str) -> Result { 25 | if Path::new(name).is_file() { 26 | return Ok(Resolved::File(PathBuf::from(name))); 27 | } 28 | 29 | for dir in &self.include_dirs { 30 | let path = dir.join(name); 31 | if path.is_file() { 32 | return Ok(Resolved::File(path)); 33 | } 34 | } 35 | 36 | if let Some(lib) = fift_libs::all().get(name) { 37 | return Ok(Resolved::Lib(lib)); 38 | } 39 | 40 | Err(std::io::Error::new( 41 | std::io::ErrorKind::NotFound, 42 | format!("`{name}` file not found"), 43 | )) 44 | } 45 | } 46 | 47 | impl Environment for SystemEnvironment { 48 | fn now_ms(&self) -> u64 { 49 | std::time::SystemTime::now() 50 | .duration_since(std::time::UNIX_EPOCH) 51 | .unwrap() 52 | .as_millis() as u64 53 | } 54 | 55 | fn get_env(&self, name: &str) -> Option { 56 | std::env::var(name).ok() 57 | } 58 | 59 | fn file_exists(&self, name: &str) -> bool { 60 | self.resolve_file(name).is_ok() 61 | } 62 | 63 | fn write_file(&mut self, name: &str, contents: &[u8]) -> std::io::Result<()> { 64 | std::fs::write(name, contents)?; 65 | Ok(()) 66 | } 67 | 68 | fn read_file(&mut self, name: &str) -> std::io::Result> { 69 | match self.resolve_file(name)? { 70 | Resolved::File(path) => std::fs::read(path), 71 | Resolved::Lib(lib) => Ok(lib.as_bytes().to_vec()), 72 | } 73 | } 74 | 75 | fn read_file_part(&mut self, name: &str, offset: u64, len: u64) -> std::io::Result> { 76 | fn read_part(mut r: R, offset: u64, len: u64) -> std::io::Result> 77 | where 78 | R: Read + Seek, 79 | { 80 | let mut result = Vec::new(); 81 | r.seek(SeekFrom::Start(offset))?; 82 | r.take(len).read_to_end(&mut result)?; 83 | Ok(result) 84 | } 85 | 86 | match self.resolve_file(name)? { 87 | Resolved::File(path) => { 88 | let r = BufReader::new(File::open(path)?); 89 | read_part(r, offset, len) 90 | } 91 | Resolved::Lib(lib) => read_part(std::io::Cursor::new(lib), offset, len), 92 | } 93 | } 94 | 95 | fn include(&self, name: &str) -> std::io::Result { 96 | Ok(match self.resolve_file(name)? { 97 | Resolved::File(path) => { 98 | let file = File::open(path)?; 99 | let buffer = BufReader::new(file); 100 | fift::core::SourceBlock::new(name, buffer) 101 | } 102 | Resolved::Lib(lib) => fift::core::SourceBlock::new(name, std::io::Cursor::new(lib)), 103 | }) 104 | } 105 | } 106 | 107 | enum Resolved { 108 | File(PathBuf), 109 | Lib(&'static str), 110 | } 111 | -------------------------------------------------------------------------------- /cli/src/input.rs: -------------------------------------------------------------------------------- 1 | use std::cell::Cell; 2 | use std::io::{BufRead, Write}; 3 | use std::rc::Rc; 4 | 5 | use anyhow::Result; 6 | use rustyline::{DefaultEditor, ExternalPrinter}; 7 | 8 | pub struct LineReader { 9 | editor: DefaultEditor, 10 | line: String, 11 | offset: usize, 12 | add_newline: Rc>, 13 | finished: bool, 14 | } 15 | 16 | impl LineReader { 17 | pub fn new() -> Result { 18 | let editor = DefaultEditor::new()?; 19 | Ok(Self { 20 | editor, 21 | line: String::default(), 22 | offset: 0, 23 | add_newline: Default::default(), 24 | finished: false, 25 | }) 26 | } 27 | 28 | pub fn create_external_printer(&mut self) -> Result> { 29 | let printer = self.editor.create_external_printer()?; 30 | Ok(Box::new(TerminalWriter { 31 | printer, 32 | add_newline: self.add_newline.clone(), 33 | })) 34 | } 35 | } 36 | 37 | struct TerminalWriter { 38 | printer: T, 39 | add_newline: Rc>, 40 | } 41 | 42 | impl Write for TerminalWriter { 43 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 44 | let output = String::from_utf8_lossy(buf).into_owned(); 45 | self.add_newline.set(!output.ends_with('\n')); 46 | 47 | self.printer.print(output).expect("External print failure"); 48 | Ok(buf.len()) 49 | } 50 | 51 | fn flush(&mut self) -> std::io::Result<()> { 52 | Ok(()) 53 | } 54 | } 55 | 56 | impl std::io::Read for LineReader { 57 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result { 58 | if self.finished { 59 | return Ok(0); 60 | } 61 | 62 | let n = { 63 | let mut rem = self.fill_buf()?; 64 | rem.read(buf)? 65 | }; 66 | self.consume(n); 67 | Ok(n) 68 | } 69 | } 70 | 71 | impl std::io::BufRead for LineReader { 72 | fn fill_buf(&mut self) -> std::io::Result<&[u8]> { 73 | use rustyline::error::ReadlineError; 74 | 75 | if self.offset >= self.line.len() { 76 | loop { 77 | if self.add_newline.get() { 78 | self.add_newline.set(false); 79 | println!(); 80 | } 81 | 82 | match self.editor.readline("> ") { 83 | Ok(line) if line.is_empty() => continue, 84 | Ok(mut line) => { 85 | { 86 | let line = line.trim(); 87 | if !line.is_empty() { 88 | self.editor.add_history_entry(line.to_owned()).ok(); 89 | } 90 | } 91 | 92 | line.push('\n'); 93 | self.line = line; 94 | self.offset = 0; 95 | break; 96 | } 97 | Err(ReadlineError::Interrupted | ReadlineError::Eof) => { 98 | self.line = Default::default(); 99 | self.offset = 0; 100 | self.finished = true; 101 | break; 102 | } 103 | Err(ReadlineError::Io(e)) => return Err(e), 104 | Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, e)), 105 | } 106 | } 107 | } 108 | Ok(&self.line.as_bytes()[self.offset..]) 109 | } 110 | 111 | fn consume(&mut self, amt: usize) { 112 | self.offset += amt; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /cli/src/modules/shell.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Write}; 2 | use std::process::Stdio; 3 | 4 | use anyhow::{Context as _, Result}; 5 | use bitflags::bitflags; 6 | use fift::core::*; 7 | 8 | pub struct ShellUtils; 9 | 10 | #[fift_module] 11 | impl ShellUtils { 12 | // runshell (cmd:string args:tuple(string...) -- exit_code:int) 13 | // runshellx (cmd:string args:tuple(string...) [stdin:string] mode:int -- [stdout:string/bytes] [stderr:string] exit_code:int) 14 | #[cmd(name = "runshell", stack, args(mode = Some(ShellMode::DEFAULT)))] 15 | #[cmd(name = "runshellx", stack, args(mode = None))] 16 | fn interpret_run_shell(stack: &mut Stack, mode: Option) -> Result<()> { 17 | let mode = match mode { 18 | Some(m) => m, 19 | None => ShellMode::from_bits_retain(stack.pop_smallint_range(0, 7)? as u8), 20 | }; 21 | 22 | let mut stdin = None; 23 | let (stdin_descr, stdin) = if mode.contains(ShellMode::WRITE_STDIN) { 24 | let value = stdin.insert(stack.pop()?); 25 | let value = if mode.contains(ShellMode::STDIN_AS_BYTES) { 26 | value.as_bytes()? 27 | } else { 28 | value.as_string()?.as_bytes() 29 | }; 30 | 31 | (Stdio::piped(), value) 32 | } else { 33 | (Stdio::null(), [].as_slice()) 34 | }; 35 | 36 | let args = stack.pop_tuple()?; 37 | let args = args 38 | .iter() 39 | .map(|arg| arg.as_string()) 40 | .collect::>>()?; 41 | 42 | let cmd = stack.pop_string()?; 43 | 44 | let mut child = std::process::Command::new(cmd.as_ref()) 45 | .args(args) 46 | .stdin(stdin_descr) 47 | .stdout(Stdio::piped()) 48 | .stderr(Stdio::piped()) 49 | .spawn() 50 | .context("Failed to spawn a child process")?; 51 | 52 | if let Some(mut child_stdin) = child.stdin.take() { 53 | child_stdin 54 | .write_all(stdin) 55 | .context("Failed to write to stdin")?; 56 | } 57 | 58 | let exit_code = child 59 | .wait()? 60 | .code() 61 | .context("The child process was terminated by signal")?; 62 | 63 | if mode.contains(ShellMode::READ_STDOUT) { 64 | let mut bytes = Vec::new(); 65 | if let Some(mut stdout) = child.stdout.take() { 66 | stdout.read_to_end(&mut bytes)?; 67 | } 68 | if mode.contains(ShellMode::STDOUT_AS_BYTES) { 69 | stack.push(bytes)?; 70 | } else { 71 | stack.push(String::from_utf8_lossy(&bytes).to_string())?; 72 | } 73 | } 74 | 75 | if mode.contains(ShellMode::READ_STDERR) { 76 | let mut bytes = Vec::new(); 77 | if let Some(mut stderr) = child.stderr.take() { 78 | stderr.read_to_end(&mut bytes)?; 79 | } 80 | stack.push(String::from_utf8_lossy(&bytes).to_string())?; 81 | } 82 | 83 | stack.push_int(exit_code) 84 | } 85 | } 86 | 87 | bitflags! { 88 | struct ShellMode: u8 { 89 | /// +1 = use stdin as string from stack (empty otherwise) 90 | const WRITE_STDIN = 1; 91 | /// +2 = push stdout as string on stack after execution 92 | const READ_STDOUT = 2; 93 | /// +4 = push stderr as string on stack after execution 94 | const READ_STDERR = 4; 95 | /// +8 = if stdin is present it is required to be bytes 96 | const STDIN_AS_BYTES = 8; 97 | /// +16 = if stdout is present it is required to be bytes 98 | const STDOUT_AS_BYTES = 16; 99 | 100 | const DEFAULT = 0; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/modules/crypto.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context as _, Result}; 2 | use tycho_crypto::ed25519; 3 | 4 | use crate::core::*; 5 | use crate::util::{CRC_16, CRC_32, CRC_32_C}; 6 | 7 | pub struct Crypto; 8 | 9 | #[fift_module] 10 | impl Crypto { 11 | #[cmd(name = "newkeypair", stack)] 12 | fn interpret_newkeypair(stack: &mut Stack) -> Result<()> { 13 | let secret = rand::random::(); 14 | let public = ed25519::PublicKey::from(&secret); 15 | stack.push(secret.as_bytes().to_vec())?; 16 | stack.push(public.as_bytes().to_vec()) 17 | } 18 | 19 | #[cmd(name = "priv>pub", stack)] 20 | fn interpret_priv_key_to_pub(stack: &mut Stack) -> Result<()> { 21 | let secret = pop_secret_key(stack)?; 22 | stack.push(ed25519::PublicKey::from(&secret).as_bytes().to_vec()) 23 | } 24 | 25 | #[cmd(name = "ed25519_sign", stack)] 26 | fn interpret_ed25519_sign(stack: &mut Stack) -> Result<()> { 27 | let secret = pop_secret_key(stack)?; 28 | let public = ed25519::PublicKey::from(&secret); 29 | let data = stack.pop_bytes()?; 30 | let signature = secret.expand().sign_raw(&data, &public); 31 | stack.push(signature.to_vec()) 32 | } 33 | 34 | #[cmd(name = "ed25519_chksign", stack)] 35 | fn interpret_ed25519_chksign(stack: &mut Stack) -> Result<()> { 36 | let public = pop_public_key(stack)?; 37 | let signature = pop_signature(stack)?; 38 | let data = stack.pop_bytes()?; 39 | stack.push_bool(public.verify_raw(&data, &signature)) 40 | } 41 | 42 | #[cmd(name = "ed25519_sign_uint", stack)] 43 | fn interpret_ed25519_sign_uint(stack: &mut Stack) -> Result<()> { 44 | let secret = pop_secret_key(stack)?; 45 | let public = ed25519::PublicKey::from(&secret); 46 | let int = stack.pop_int()?; 47 | anyhow::ensure!( 48 | int.sign() != num_bigint::Sign::Minus, 49 | "Expected a positive number" 50 | ); 51 | anyhow::ensure!( 52 | int.bits() <= 256, 53 | "Ed25519 data to be signed must fit into 256 bits" 54 | ); 55 | let (_, mut data) = int.to_bytes_le(); 56 | data.resize(32, 0); 57 | data.reverse(); 58 | 59 | let signature = secret.expand().sign_raw(&data, &public); 60 | stack.push(signature.to_vec()) 61 | } 62 | 63 | #[cmd(name = "crc16", stack)] 64 | fn interpret_crc16(stack: &mut Stack) -> Result<()> { 65 | let bytes = stack.pop_bytes()?; 66 | let mut res = CRC_16.digest(); 67 | res.update(bytes.as_slice()); 68 | stack.push_int(res.finalize()) 69 | } 70 | 71 | #[cmd(name = "crc32", stack)] 72 | fn interpret_crc32(stack: &mut Stack) -> Result<()> { 73 | let bytes = stack.pop_bytes()?; 74 | let mut res = CRC_32.digest(); 75 | res.update(bytes.as_slice()); 76 | stack.push_int(res.finalize()) 77 | } 78 | 79 | #[cmd(name = "crc32c", stack)] 80 | fn interpret_crc32c(stack: &mut Stack) -> Result<()> { 81 | let bytes = stack.pop_bytes()?; 82 | let mut res = CRC_32_C.digest(); 83 | res.update(bytes.as_slice()); 84 | stack.push_int(res.finalize()) 85 | } 86 | } 87 | 88 | fn pop_secret_key(stack: &mut Stack) -> Result { 89 | let b = stack.pop_bytes()?; 90 | Ok(ed25519::SecretKey::from_bytes( 91 | b.as_slice().try_into().ok().context("Invalid secret key")?, 92 | )) 93 | } 94 | 95 | fn pop_public_key(stack: &mut Stack) -> Result { 96 | let b = stack.pop_bytes()?; 97 | if let Ok(b) = b.as_slice().try_into() 98 | && let Some(key) = ed25519::PublicKey::from_bytes(b) 99 | { 100 | return Ok(key); 101 | } 102 | anyhow::bail!("Invalid public key") 103 | } 104 | 105 | fn pop_signature(stack: &mut Stack) -> Result<[u8; 64]> { 106 | let b = stack.pop_bytes()?; 107 | b.as_slice().try_into().ok().context("Invalid signature") 108 | } 109 | -------------------------------------------------------------------------------- /libs/src/FiftExt.fif: -------------------------------------------------------------------------------- 1 | { ?dup { 1+ { execute } { 0 swap } cond } 2 | { (number) ?dup 0= abort"-?" 'nop } cond 3 | } : (interpret-prepare) 4 | { { include-depth 0= (seekeof?) not } { 5 | (word-prefix-find) (interpret-prepare) (execute) 6 | } while 7 | } : interpret 8 | { ({) 9 | { 0 (seekeof?) abort"no }" (word-prefix-find) (interpret-prepare) (compile) over atom? not } until 10 | (}) swap execute 11 | } : begin-block 12 | { swap 0 'nop } : end-block 13 | { { 1 'nop } `{ begin-block } 14 | { { swap `{ eq? not abort"} without {" swap execute } end-block } 15 | :: } :: { 16 | 17 | // if{ ... }then{ ... }elseif{ ... }then{ ... }else{ ... } 18 | { eq? not abort"unexpected" } : ?pairs 19 | { dup `if eq? swap `ifnot eq? over or not abort"without if{" } : if-ifnot? 20 | // cond then ? -- exec 21 | { { ' if } { ' ifnot } cond rot ({) 0 rot (compile) -rot 1 swap (compile) (}) 22 | } : (make-if) 23 | // cond then else -- exec 24 | { rot ({) 0 rot (compile) -rot 2 ' cond (compile) (}) 25 | } : (make-cond) 26 | { `noelse `if begin-block } :: if{ 27 | { `noelse `ifnot begin-block } :: ifnot{ 28 | { 1 ' end-block does } : end-block-does 29 | { { over `else eq? } { 30 | nip rot if-ifnot? ' swap ifnot (make-cond) 31 | } while 32 | swap `noelse ?pairs 0 swap 33 | } : finish-else-chain 34 | { swap dup if-ifnot? drop `then { 35 | swap `then ?pairs 36 | swap if-ifnot? (make-if) finish-else-chain 37 | } `{ begin-block 38 | } end-block-does :: }then{ 39 | { swap `{ ?pairs nip 40 | swap `then eq? not abort"without }then{" `else 41 | } : ?else-ok 42 | { ?else-ok { finish-else-chain } `{ begin-block } end-block-does :: }else{ 43 | { ?else-ok `if begin-block } end-block-does :: }elseif{ 44 | { ?else-ok `ifnot begin-block } end-block-does :: }elseifnot{ 45 | 46 | // while{ ... }do{ ... } 47 | { 2 ' while does } : (make-while) 48 | { `while begin-block } :: while{ 49 | { swap `while eq? not abort"without while{" `while-do { 50 | swap `while-do ?pairs (make-while) 0 swap 51 | } `{ begin-block 52 | } end-block-does :: }do{ 53 | 54 | // repeat{ ... }until{ ... } 55 | { swap ({) 0 rot (compile) 0 rot (compile) (}) 1 ' until does } : (make-until) 56 | { `repeat begin-block } :: repeat{ 57 | { swap `repeat eq? not abort"without repeat{" `until { 58 | swap `until ?pairs (make-until) 0 swap 59 | } `{ begin-block 60 | } end-block-does :: }until{ 61 | 62 | // def { ... } instead of { ... } : 63 | { bl word swap bl word "{" $cmp abort"{ expected" `def { 64 | swap `def ?pairs -rot 3 ' (create) 65 | } `{ begin-block 66 | } : (def) 67 | { 0 (def) } :: def 68 | { 1 (def) } :: def:: 69 | 70 | // defrec { ... } instead of recursive { ... } swap ! 71 | { recursive bl word "{" $cmp abort"{ expected" `defrec { 72 | swap `defrec ?pairs swap ! 0 'nop 73 | } `{ begin-block 74 | } :: defrec 75 | 76 | def .sgn { 77 | if{ ?dup 0= }then{ 78 | ."zero" 79 | }elseif{ 0> }then{ 80 | ."positive" 81 | }else{ 82 | ."negative" 83 | } 84 | cr 85 | } 86 | // equivalent to: { ?dup 0= { ."zero" } { 0> { ."positive" } { ."negative" } cond } cond cr } : .sgn 87 | 88 | defrec fact { 89 | if{ dup }then{ 90 | dup 1- fact * 91 | }else{ 92 | drop 1 93 | } 94 | } 95 | // equivalent to: recursive fact { dup { dup 1- fact * } { drop 1 } cond } swap ! 96 | 97 | // [[ ... ]] computes arbitrary constants inside definitions 98 | // { [[ 5 dup * ]] + } : add25 99 | // is equivalent to 100 | // { 25 + } : add25 101 | { "without [[" abort } box constant ']] 102 | { ']] @ execute } : ]] 103 | { { ']] @ 2 { ']] ! call/cc } does ']] ! 104 | interpret 'nop ']] ! "]] not found" abort 105 | } call/cc 106 | drop 1 'nop 107 | } :: [[ 108 | 109 | { { over @ swap 2 { call/cc } does swap ! 110 | interpret "literal to eof" abort 111 | } call/cc 112 | drop execute 1 'nop 113 | } : interpret-literal-to 114 | // use next line only if Lists.fif is loaded (or move it to Lists.fif if FiftExt.fif becomes part of Fift.fif) 115 | // { ( ') interpret-literal-to } :: '( 116 | // then you can use list literals '( a b c ... ) inside definitions: 117 | // { '( 1 2 3 ) } : test 118 | // { '( ( `a { ."A" } ) ( `b { ."B" } ) ) assoc { cadr execute } { ."???" } cond } : test2 119 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Fift 8 | 143 | 144 | 145 |
146 |

Rust implementation of the
Fift esoteric language

147 |
148 |
149 |

150 | # Run the following, then follow the onscreen instructions: 151 |

152 |
153 | curl -fsSL https://fift.rs/install | bash 154 | COPY 155 |
156 |
157 | 171 | 172 | 193 | 194 | -------------------------------------------------------------------------------- /libs/src/Fift.fif: -------------------------------------------------------------------------------- 1 | { 0 word drop 0 'nop } :: // 2 | { char " word 1 { swap { abort } if drop } } ::_ abort" 3 | { { bl word dup "" $= abort"comment extends after end of file" "*/" $= } until 0 'nop } :: /* 4 | // { bl word 1 2 ' (create) } "::" 1 (create) 5 | // { bl word 0 2 ' (create) } :: : 6 | // { bl word 2 2 ' (create) } :: :_ 7 | // { bl word 3 2 ' (create) } :: ::_ 8 | // { bl word 0 (create) } : create 9 | // { bl word (forget) } : forget 10 | { bl word 1 ' (forget) } :: [forget] 11 | { char " word 1 ' type } ::_ ." 12 | { char } word x>B 1 'nop } ::_ B{ 13 | { swap ({) over 2+ -roll swap (compile) (}) } : does 14 | { 1 'nop does create } : constant 15 | { 2 'nop does create } : 2constant 16 | { hole constant } : variable 17 | 10 constant ten 18 | { bl word 1 { find 0= abort"word not found" } } :: (') 19 | { bl word find not abort"-?" 0 swap } :: [compile] 20 | { bl word 1 { 21 | dup find { " -?" $+ abort } ifnot nip execute 22 | } } :: @' 23 | { bl word 1 { swap 1 'nop does swap 0 (create) } 24 | } :: =: 25 | { bl word 1 { -rot 2 'nop does swap 0 (create) } 26 | } :: 2=: 27 | { } : s>c 28 | { s>c hashB } : shash 29 | // to be more efficiently re-implemented in C++ in the future 30 | { dup 0< ' negate if } : abs 31 | { 2dup > ' swap if } : minmax 32 | { minmax drop } : min 33 | { minmax nip } : max 34 | "" constant <# 35 | ' $reverse : #> 36 | { swap 10 /mod char 0 + rot swap hold } : # 37 | { { # over 0<= } until } : #s 38 | { 0< { char - hold } if } : sign 39 | // { dup abs <# #s rot sign #> nip } : (.) 40 | // { (.) type } : ._ 41 | // { ._ space } : . 42 | { dup 10 < { 48 } { 55 } cond + } : Digit 43 | { dup 10 < { 48 } { 87 } cond + } : digit 44 | // x s b -- x' s' 45 | { rot swap /mod Digit rot swap hold } : B# 46 | { rot swap /mod digit rot swap hold } : b# 47 | { 16 B# } : X# 48 | { 16 b# } : x# 49 | // x s b -- 0 s' 50 | { -rot { 2 pick B# over 0<= } until rot drop } : B#s 51 | { -rot { 2 pick b# over 0<= } until rot drop } : b#s 52 | { 16 B#s } : X#s 53 | { 16 b#s } : x#s 54 | variable base 55 | { 10 base ! } : decimal 56 | { 16 base ! } : hex 57 | { 8 base ! } : octal 58 | { 2 base ! } : binary 59 | { base @ B# } : Base# 60 | { base @ b# } : base# 61 | { base @ B#s } : Base#s 62 | { base @ b#s } : base#s 63 | // x w -- s 64 | { over abs <# rot 1- ' X# swap times X#s rot sign #> nip } : (0X.) 65 | { over abs <# rot 1- ' x# swap times x#s rot sign #> nip } : (0x.) 66 | { (0X.) type } : 0X._ 67 | { 0X._ space } : 0X. 68 | { (0x.) type } : 0x._ 69 | { 0x._ space } : 0x. 70 | { bl (-trailing) } : -trailing 71 | { char 0 (-trailing) } : -trailing0 72 | { char " word 1 ' $+ } ::_ +" 73 | { find 0<> dup ' nip if } : (def?) 74 | { bl word 1 ' (def?) } :: def? 75 | { bl word 1 { (def?) not } } :: undef? 76 | { def? ' skip-to-eof if } : skip-ifdef 77 | { bl word dup (def?) { drop skip-to-eof } { 'nop swap 0 (create) } cond } : library 78 | { bl word dup (def?) { 2drop skip-to-eof } { swap 1 'nop does swap 0 (create) } cond } : library-version 79 | { hole dup 1 'nop does swap 1 { context! } does bl word tuck 0 (create) +"-wordlist" 0 (create) } : namespace 80 | { context@ current! } : definitions 81 | { char ) word "$" swap $+ 1 { find 0= abort"undefined parameter" execute } } ::_ $( 82 | // b s -- ? 83 | { sbitrefs rot brembitrefs rot >= -rot <= and } : s-fits? 84 | // b s x -- ? 85 | { swap sbitrefs -rot + rot brembitrefs -rot <= -rot <= and } : s-fits-with? 86 | { 0 swap ! } : 0! 87 | { tuck @ + swap ! } : +! 88 | { tuck @ swap - swap ! } : -! 89 | { 1 swap +! } : 1+! 90 | { -1 swap +! } : 1-! 91 | { null swap ! } : null! 92 | { not 2 pick @ and xor swap ! } : ~! 93 | 0 tuple constant nil 94 | { 1 tuple } : single 95 | { 2 tuple } : pair 96 | { 3 tuple } : triple 97 | { 1 untuple } : unsingle 98 | { 2 untuple } : unpair 99 | { 3 untuple } : untriple 100 | { over tuple? { swap count = } { 2drop false } cond } : tuple-len? 101 | { 0 tuple-len? } : nil? 102 | { 1 tuple-len? } : single? 103 | { 2 tuple-len? } : pair? 104 | { 3 tuple-len? } : triple? 105 | { 0 [] } : first 106 | { 1 [] } : second 107 | { 2 [] } : third 108 | ' pair : cons 109 | ' unpair : uncons 110 | { 0 [] } : car 111 | { 1 [] } : cdr 112 | { cdr car } : cadr 113 | { cdr cdr } : cddr 114 | { cdr cdr car } : caddr 115 | { null ' cons rot times } : list 116 | { -rot pair swap ! } : 2! 117 | { @ unpair } : 2@ 118 | { true (atom) drop } : atom 119 | { bl word atom 1 'nop } ::_ ` 120 | { hole dup 1 { @ execute } does create } : recursive 121 | // { 0 { 1+ dup 1 ' $() does over (.) "$" swap $+ 0 (create) } rot times drop } : :$1..n 122 | { 10 hold } : +cr 123 | { 9 hold } : +tab 124 | { "" swap { 0 word 2dup $cmp } { rot swap $+ +cr swap } while 2drop } : scan-until-word 125 | { 0 word -trailing scan-until-word 1 'nop } ::_ $<< 126 | { 0x40 runvmx } : runvmcode 127 | { 0x48 runvmx } : gasrunvmcode 128 | { 0xc8 runvmx } : gas2runvmcode 129 | { 0x43 runvmx } : runvmdict 130 | { 0x4b runvmx } : gasrunvmdict 131 | { 0xcb runvmx } : gas2runvmdict 132 | { 0x45 runvmx } : runvm 133 | { 0x4d runvmx } : gasrunvm 134 | { 0xcd runvmx } : gas2runvm 135 | { 0x55 runvmx } : runvmctx 136 | { 0x5d runvmx } : gasrunvmctx 137 | { 0xdd runvmx } : gas2runvmctx 138 | { 0x75 runvmx } : runvmctxact 139 | { 0x7d runvmx } : gasrunvmctxact 140 | { 0xfd runvmx } : gas2runvmctxact 141 | { 0x35 runvmx } : runvmctxactq 142 | { 0x3d runvmx } : gasrunvmctxactq 143 | -------------------------------------------------------------------------------- /libs/src/GetOpt.fif: -------------------------------------------------------------------------------- 1 | library GetOpt // Simple command-line options parser 2 | "Lists.fif" include 3 | 4 | // May be used as follows: 5 | // begin-options 6 | // "h" { ."Help Message" 0 halt } short-option 7 | // "v" { parse-int =: verbosity } short-option-arg 8 | // "i" "--interactive" { true =: interactive } short-long-option 9 | // parse-options 10 | 11 | // ( l -- l') computes tail of list l if non-empty; else () 12 | { dup null? ' cdr ifnot } : safe-cdr 13 | // ( l c -- l') deletes first c elements from list l 14 | { ' safe-cdr swap times } : list-delete-first 15 | // ( l n c -- l' ) deletes c elements starting from n-th in list l 16 | recursive list-delete-range { 17 | dup 0<= { 2drop } { 18 | over 0<= { nip list-delete-first } { 19 | swap 1- swap rot uncons 2swap list-delete-range cons 20 | } cond } cond 21 | } swap ! 22 | // ( n c -- ) deletes $n .. $(n+c-1) from the argument list $* 23 | { swap 1- $* @ swap rot list-delete-range $* ! } : $*del.. 24 | // ( s s' -- ? ) checks whether s' is a prefix of s 25 | { tuck $len over $len over >= { $| drop $= } { 2drop drop false } cond 26 | } : $pfx? 27 | // ( s -- ? ) checks whether s is an option (a string beginning with '-') 28 | { dup $len 1 > { "-" $pfx? } { drop false } cond } : is-opt? 29 | // ( s -- ? ) checks whether s is a digit option 30 | { 2 $| drop 1 $| nip $>B 8 B>u@ dup 57 <= swap 48 >= and } : is-digit-opt? 31 | 0 box constant disable-digit-opts 32 | // ( l -- s i or 0 ) finds first string in l beginning with '-' 33 | { 0 { 1+ over null? { 2drop 0 true } { 34 | swap uncons over is-opt? 35 | { disable-digit-opts @ { over is-digit-opt? not } { true } cond } { false } cond 36 | { drop swap true } { nip swap false } cond 37 | } cond } until 38 | } : list-find-opt 39 | // ( -- s i or 0 ) finds first option in cmdline args 40 | { $* @ list-find-opt } : first-opt 41 | ' second : get-opt-flags 42 | ' first : get-opt-exec 43 | // ( s t -- ? ) checks whether short/long option s matches description t 44 | { third $= } : short-option-matches 45 | { dup get-opt-flags 4 and 0= 3 + [] $= 46 | } : long-option-matches 47 | // ( t -- s -1 or 0 ) extracts help message from description 48 | { dup get-opt-flags 4 and 0= 4 + over count over > 49 | { [] true } { 2drop false } cond 50 | } : get-opt-help 51 | // ( s l -- t -1 or 0 ) finds short/long option s in list l 52 | { swap 1 { swap short-option-matches } does assoc-gen 53 | } : lookup-short-option 54 | { swap 1 { swap long-option-matches } does assoc-gen 55 | } : lookup-long-option 56 | // ( s -- s' null or s' s'' ) Splits long option --opt=arg at '=' 57 | { dup "=" $pos 1+ ?dup { tuck $| swap rot 1- $| drop swap } { null } cond 58 | } : split-longopt 59 | // ( l -- f or 0 ) Extracts global option flags from first entry of l 60 | { dup null? { drop 0 } { car get-opt-flags -256 and } cond 61 | } : get-global-option-flags 62 | variable options-list 63 | // ( l -- i or 0 ) 64 | // parses command line arguments according to option description list l 65 | // and returns index i of first incorrect option 66 | { dup options-list ! get-global-option-flags 67 | 256 and disable-digit-opts ! 68 | { first-opt dup 0= { true } { 69 | swap dup "--" $pfx? { // i s 70 | dup $len 2 = { drop dup 1 $*del.. 0 true } { 71 | split-longopt swap options-list @ 72 | lookup-long-option not { drop true } { // i s' t f 73 | dup get-opt-exec swap get-opt-flags 3 and // i s' e f' 74 | 2 pick null? { dup 1 = } { dup 0= negate } cond // i s' e f' f'' 75 | dup 1 = { 2drop 2drop true } { 76 | { drop nip over 1+ $() swap execute 2 $*del.. false } { 77 | ' nip ifnot execute 1 $*del.. false 78 | } cond } cond } cond } cond } { // i s 79 | 1 $| nip { 80 | dup $len 0= { drop 1 $*del.. false true } { 81 | 1 $| swap options-list @ // i s' s l 82 | lookup-short-option not { drop true true } { // i s' t 83 | dup get-opt-exec swap get-opt-flags 3 and // i s' e f' 84 | ?dup 0= { execute false } { 85 | 2 pick $len { drop execute "" false } { 86 | 2 = { nip null swap execute "" false } { // i e 87 | nip over 1+ $() swap execute 2 $*del.. false true 88 | } cond } cond } cond } cond } cond } until 89 | } cond 90 | } cond } until 91 | } : getopt 92 | // ( t -- ) Displays help message for one option 93 | { dup get-opt-flags dup 4 and 2 pick third swap { 94 | ."-" type ."/" over 3 [] type } { 95 | dup $len { dup "--" $pfx? { ."-" } ifnot type } { 96 | drop ."usage: " $0 type 97 | } cond } cond 98 | dup 3 and ?dup { 99 | 2 = { ."[=]" } { ."=" } cond 100 | } if 101 | 8 and { 9 emit } ifnot 102 | get-opt-help { type } { ."No help available" } cond cr 103 | } : show-opt-help 104 | // ( -- ) Displays options help message according to options-list 105 | { options-list @ { dup null? not } { 106 | uncons swap show-opt-help 107 | } while drop 108 | } : show-options-help 109 | // ( l -- ) Parses options and throws an error on failure 110 | { getopt ?dup { 111 | $() "cannot parse command line options near `" swap $+ +"`" 112 | show-options-help abort } if 113 | } : run-getopt 114 | anon constant opt-list-marker 115 | ' opt-list-marker : begin-options 116 | { opt-list-marker list-until-marker } : end-options 117 | { end-options run-getopt } : parse-options 118 | // ( s e -- o ) Creates short/long option s with execution token e 119 | { 0 rot triple } dup : short-option : long-option 120 | // ( s s' e -- o ) Creates a combined short option s and long option s' with execution token e 121 | { 4 2swap 4 tuple } : short-long-option 122 | { 1 rot triple } dup : short-option-arg : long-option-arg 123 | { 2 rot triple } dup : short-option-?arg : long-option-?arg 124 | { 5 2swap 4 tuple } : short-long-option-arg 125 | { 6 2swap 4 tuple } : short-long-option-?arg 126 | // ( o s -- s' ) Adds help message to option 127 | ' , : option-help 128 | // ( s f -- o ) Creates a generic help message 129 | { swap 'nop rot "" 3 roll 4 tuple } : generic-help-setopt 130 | { 0 generic-help-setopt } : generic-help 131 | 256 constant disable-digit-options 132 | -------------------------------------------------------------------------------- /src/modules/debug_utils.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use crate::core::*; 4 | use crate::util::*; 5 | 6 | pub struct DebugUtils; 7 | 8 | #[fift_module] 9 | impl DebugUtils { 10 | #[cmd(name = ".", args(space_after = true))] 11 | #[cmd(name = "._", args(space_after = false))] 12 | fn interpret_dot(ctx: &mut Context, space_after: bool) -> Result<()> { 13 | let int = ctx.stack.pop_int()?; 14 | write!(ctx.stdout, "{int}{}", opt_space(space_after))?; 15 | Ok(()) 16 | } 17 | 18 | #[cmd(name = "x.", args(uppercase = false, space_after = true))] 19 | #[cmd(name = "x._", args(uppercase = false, space_after = false))] 20 | #[cmd(name = "X.", args(uppercase = true, space_after = true))] 21 | #[cmd(name = "X._", args(uppercase = true, space_after = false))] 22 | fn interpret_dothex(ctx: &mut Context, uppercase: bool, space_after: bool) -> Result<()> { 23 | let int = ctx.stack.pop_int()?; 24 | let space = opt_space(space_after); 25 | if uppercase { 26 | write!(ctx.stdout, "{:X}{space}", int.as_ref()) 27 | } else { 28 | write!(ctx.stdout, "{:x}{space}", int.as_ref()) 29 | }?; 30 | Ok(()) 31 | } 32 | 33 | #[cmd(name = "b.", args(space_after = true))] 34 | #[cmd(name = "b._", args(space_after = false))] 35 | fn interpret_dotbin(ctx: &mut Context, space_after: bool) -> Result<()> { 36 | let int = ctx.stack.pop_int()?; 37 | write!(ctx.stdout, "{:b}{}", int.as_ref(), opt_space(space_after))?; 38 | Ok(()) 39 | } 40 | 41 | #[cmd(name = "csr.", args(pop_limit = false))] 42 | #[cmd(name = "lcsr.", args(pop_limit = true))] 43 | fn interpret_dot_cellslice_rec(ctx: &mut Context, pop_limit: bool) -> Result<()> { 44 | const DEFAULT_RECURSIVE_PRINT_LIMIT: usize = 100; 45 | 46 | let limit = if pop_limit { 47 | ctx.stack.pop_smallint_range(0, u16::MAX as u32)? as usize 48 | } else { 49 | DEFAULT_RECURSIVE_PRINT_LIMIT 50 | }; 51 | 52 | let cs = ctx.stack.pop_cell_slice()?; 53 | write!(ctx.stdout, "{}", cs.apply().display_slice_tree(limit))?; 54 | Ok(()) 55 | } 56 | 57 | #[cmd(name = "Bx.")] 58 | fn interpret_bytes_hex_print_raw(ctx: &mut Context) -> Result<()> { 59 | const CHUNK: usize = 16; 60 | let bytes = ctx.stack.pop_bytes()?; 61 | let mut buffer: [u8; CHUNK * 2] = Default::default(); 62 | for chunk in bytes.chunks(CHUNK) { 63 | let buffer = &mut buffer[..chunk.len() * 2]; 64 | hex::encode_to_slice(chunk, buffer).unwrap(); 65 | ctx.stdout.write_all(buffer)?; 66 | } 67 | Ok(()) 68 | } 69 | 70 | #[cmd(name = ".s")] 71 | fn interpret_dotstack(ctx: &mut Context) -> Result<()> { 72 | writeln!(ctx.stdout, "{}", ctx.stack.display_dump())?; 73 | Ok(()) 74 | } 75 | 76 | #[cmd(name = ".sl")] 77 | fn interpret_dotstack_list(ctx: &mut Context) -> Result<()> { 78 | writeln!(ctx.stdout, "{}", ctx.stack.display_list())?; 79 | Ok(()) 80 | } 81 | 82 | #[cmd(name = ".dump")] 83 | fn interpret_dump(ctx: &mut Context) -> Result<()> { 84 | let item = ctx.stack.pop()?; 85 | write!(ctx.stdout, "{} ", item.display_dump())?; 86 | Ok(()) 87 | } 88 | 89 | #[cmd(name = ".l")] 90 | fn interpret_print_list(ctx: &mut Context) -> Result<()> { 91 | let item = ctx.stack.pop()?; 92 | write!(ctx.stdout, "{} ", item.display_list())?; 93 | Ok(()) 94 | } 95 | 96 | #[cmd(name = ".bt")] 97 | fn interpret_print_backtrace(ctx: &mut Context) -> Result<()> { 98 | if let Some(next) = &ctx.next { 99 | writeln!(ctx.stdout, "{}", next.display_backtrace(&ctx.dicts.current))?; 100 | } 101 | Ok(()) 102 | } 103 | 104 | #[cmd(name = "cont.")] 105 | fn interpret_print_continuation(ctx: &mut Context) -> Result<()> { 106 | let cont = ctx.stack.pop_cont()?; 107 | writeln!(ctx.stdout, "{}", cont.display_backtrace(&ctx.dicts.current))?; 108 | Ok(()) 109 | } 110 | 111 | #[cmd(name = "(dump)", stack)] 112 | fn interpret_dump_internal(stack: &mut Stack) -> Result<()> { 113 | let string = stack.pop()?.display_dump().to_string(); 114 | stack.push(string) 115 | } 116 | 117 | #[cmd(name = "(ldump)", stack)] 118 | fn interpret_list_dump_internal(stack: &mut Stack) -> Result<()> { 119 | let string = stack.pop()?.display_list().to_string(); 120 | stack.push(string) 121 | } 122 | 123 | #[cmd(name = "(.)", stack)] 124 | fn interpret_dot_internal(stack: &mut Stack) -> Result<()> { 125 | let string = stack.pop_int()?.to_string(); 126 | stack.push(string) 127 | } 128 | 129 | #[cmd(name = "(x.)", stack, args(upper = false))] 130 | #[cmd(name = "(X.)", stack, args(upper = true))] 131 | fn interpret_dothex_internal(stack: &mut Stack, upper: bool) -> Result<()> { 132 | let int = stack.pop_int()?; 133 | let string = if upper { 134 | format!("{:x}", int.as_ref()) 135 | } else { 136 | format!("{:X}", int.as_ref()) 137 | }; 138 | stack.push(string) 139 | } 140 | 141 | #[cmd(name = "(b.)", stack)] 142 | fn interpret_dotbin_internal(stack: &mut Stack) -> Result<()> { 143 | let int = stack.pop_int()?; 144 | let string = format!("{:b}", int.as_ref()); 145 | stack.push(string) 146 | } 147 | 148 | #[cmd(name = "words")] 149 | fn interpret_words(ctx: &mut Context) -> Result<()> { 150 | let Some(map) = ctx.dicts.current.clone_words_map()? else { 151 | return Ok(()); 152 | }; 153 | 154 | let mut all_words = map 155 | .as_ref() 156 | .into_iter() 157 | .map(|entry| entry.key.stack_value.as_string()) 158 | .collect::>>()?; 159 | all_words.sort(); 160 | 161 | let mut first = true; 162 | for word in all_words { 163 | let space = if std::mem::take(&mut first) { "" } else { " " }; 164 | write!(ctx.stdout, "{space}{word}")?; 165 | } 166 | Ok(()) 167 | } 168 | } 169 | 170 | const fn opt_space(space_after: bool) -> &'static str { 171 | if space_after { " " } else { "" } 172 | } 173 | -------------------------------------------------------------------------------- /libs/src/Disasm.fif: -------------------------------------------------------------------------------- 1 | library TVM_Disasm 2 | // simple TVM Disassembler 3 | "Lists.fif" include 4 | 5 | variable 'disasm 6 | { 'disasm @ execute } : disasm // disassemble a slice 7 | // usage: x{74B0} disasm 8 | 9 | variable @dismode @dismode 0! 10 | { rot over @ and rot xor swap ! } : andxor! 11 | { -2 0 @dismode andxor! } : stack-disasm // output 's1 s4 XCHG' 12 | { -2 1 @dismode andxor! } : std-disasm // output 'XCHG s1, s4' 13 | { -3 2 @dismode andxor! } : show-vm-code 14 | { -3 0 @dismode andxor! } : hide-vm-code 15 | { @dismode @ 1 and 0= } : stack-disasm? 16 | 17 | variable @indent @indent 0! 18 | { ' space @indent @ 2* times } : .indent 19 | { @indent 1+! } : +indent 20 | { @indent 1-! } : -indent 21 | 22 | { " " $pos } : spc-pos 23 | { dup " " $pos swap "," $pos dup 0< { drop } { 24 | over 0< { nip } { min } cond } cond 25 | } : spc-comma-pos 26 | { { dup spc-pos 0= } { 1 $| nip } while } : -leading 27 | { -leading -trailing dup spc-pos dup 0< { 28 | drop dup $len { atom single } { drop nil } cond } { 29 | $| swap atom swap -leading 2 { over spc-comma-pos dup 0>= } { 30 | swap 1+ -rot $| 1 $| nip -leading rot 31 | } while drop tuple 32 | } cond 33 | } : parse-op 34 | { dup "s-1" $= { drop "s(-1)" true } { 35 | dup "s-2" $= { drop "s(-2)" true } { 36 | dup 1 $| swap "x" $= { nip "x{" swap $+ +"}" true } { 37 | 2drop false } cond } cond } cond 38 | } : adj-op-arg 39 | { over count over <= { drop } { 2dup [] adj-op-arg { swap []= } { drop } cond } cond } : adj-arg[] 40 | { 1 adj-arg[] 2 adj-arg[] 3 adj-arg[] 41 | dup first 42 | dup `XCHG eq? { 43 | drop dup count 2 = { tpop swap "s0" , swap , } if } { 44 | dup `LSHIFT eq? { 45 | drop dup count 2 = stack-disasm? and { second `LSHIFT# swap pair } if } { 46 | dup `RSHIFT eq? { 47 | drop dup count 2 = stack-disasm? and { second `RSHIFT# swap pair } if } { 48 | drop 49 | } cond } cond } cond 50 | } : adjust-op 51 | 52 | variable @cp @cp 0! 53 | variable @curop 54 | variable @contX variable @contY variable @cdict 55 | 56 | { atom>$ type } : .atom 57 | { dup first .atom dup count 1 > { space 0 over count 2- { 1+ 2dup [] type .", " } swap times 1+ [] type } { drop } cond } : std-show-op 58 | { 0 over count 1- { 1+ 2dup [] type space } swap times drop first .atom } : stk-show-op 59 | { @dismode @ 2 and { .indent ."// " @curop @ csr. } if } : .curop? 60 | { .curop? .indent @dismode @ 1 and ' std-show-op ' stk-show-op cond cr 61 | } : show-simple-op 62 | { dup 4 u@ 9 = { 8 u@+ swap 15 and 3 << s@ } { 63 | dup 7 u@ 0x47 = { 7 u@+ nip 2 u@+ 7 u@+ -rot 3 << swap sr@ } { 64 | dup 8 u@ 0x8A = { ref@ " cr } : show-cont-op 74 | { swap scont-swap ":<{" show-cont-bodyx scont-swap 75 | "" show-cont-bodyx .indent ."}>" cr } : show-cont2-op 76 | 77 | { @contX @ null? { "CONT" show-cont-op } ifnot 78 | } : flush-contX 79 | { @contY @ null? { scont-swap "CONT" show-cont-op scont-swap } ifnot 80 | } : flush-contY 81 | { flush-contY flush-contX } : flush-cont 82 | { @contX @ null? not } : have-cont? 83 | { @contY @ null? not } : have-cont2? 84 | { flush-contY @contY ! scont-swap } : save-cont-body 85 | 86 | { @cdict ! } : save-const-dict 87 | { @cdict null! } : flush-dict 88 | { @cdict @ null? not } : have-dict? 89 | 90 | { flush-cont .indent type .":<{" cr 91 | @curop @ ref@ " cr 92 | } : show-ref-op 93 | { flush-contY .indent rot type .":<{" cr 94 | @curop @ ref@ " cr 97 | } : show-cont-ref-op 98 | { flush-cont .indent swap type .":<{" cr 99 | @curop @ ref@+ " cr 101 | } : show-ref2-op 102 | 103 | { flush-cont first atom>$ dup 5 $| drop "DICTI" $= swap 104 | .indent type ." {" cr +indent @cdict @ @cdict null! unpair 105 | rot { 106 | swap .indent . ."=> <{" cr +indent disasm -indent .indent ."}>" cr true 107 | } swap ' idictforeach ' dictforeach cond drop 108 | -indent .indent ."}" cr 109 | } : show-const-dict-op 110 | 111 | ( `PUSHCONT `PUSHREFCONT ) constant @PushContL 112 | ( `REPEAT `UNTIL `IF `IFNOT `IFJMP `IFNOTJMP ) constant @CmdC1 113 | ( `IFREF `IFNOTREF `IFJMPREF `IFNOTJMPREF `CALLREF `JMPREF ) constant @CmdR1 114 | ( `DICTIGETJMP `DICTIGETJMPZ `DICTUGETJMP `DICTUGETJMPZ `DICTIGETEXEC `DICTUGETEXEC ) constant @JmpDictL 115 | { dup first `DICTPUSHCONST eq? { 116 | flush-cont @curop @ get-const-dict save-const-dict show-simple-op } { 117 | dup first @JmpDictL list-member? have-dict? and { 118 | flush-cont show-const-dict-op } { 119 | flush-dict 120 | dup first @PushContL list-member? { 121 | drop @curop @ get-cont-body save-cont-body } { 122 | dup first @CmdC1 list-member? have-cont? and { 123 | flush-contY first atom>$ .curop? show-cont-op } { 124 | dup first @CmdR1 list-member? { 125 | flush-cont first atom>$ dup $len 3 - $| drop .curop? show-ref-op } { 126 | dup first `WHILE eq? have-cont2? and { 127 | drop "WHILE" "}>DO<{" .curop? show-cont2-op } { 128 | dup first `IFELSE eq? have-cont2? and { 129 | drop "IF" "}>ELSE<{" .curop? show-cont2-op } { 130 | dup first dup `IFREFELSE eq? swap `IFELSEREF eq? or have-cont? and { 131 | first `IFREFELSE eq? "IF" "}>ELSE<{" rot .curop? show-cont-ref-op } { 132 | dup first `IFREFELSEREF eq? { 133 | drop "IF" "}>ELSE<{" .curop? show-ref2-op } { 134 | flush-cont show-simple-op 135 | } cond } cond } cond } cond } cond } cond } cond } cond } cond 136 | } : show-op 137 | { dup @cp @ (vmoplen) dup 0> { 65536 /mod swap sr@+ swap dup @cp @ (vmopdump) parse-op swap s> true } { drop false } cond } : fetch-one-op 138 | { { fetch-one-op } { swap @curop ! adjust-op show-op } while } : disasm-slice 139 | { { disasm-slice dup sbitrefs 1- or 0= } { ref@ >, 24 | } 25 | 26 | #[proc_macro_attribute] 27 | pub fn fift_module(_: TokenStream, input: TokenStream) -> TokenStream { 28 | let mut input = syn::parse_macro_input!(input as ItemImpl); 29 | 30 | let dict_arg = quote::format_ident!("__dict"); 31 | 32 | let mut definitions = Vec::new(); 33 | let mut errors = Vec::new(); 34 | 35 | let mut init_function_names = Vec::new(); 36 | let mut init_functions = Vec::new(); 37 | let mut other_functions = Vec::new(); 38 | 39 | for impl_item in input.items.drain(..) { 40 | let syn::ImplItem::Fn(mut fun) = impl_item else { 41 | other_functions.push(impl_item); 42 | continue; 43 | }; 44 | 45 | let mut has_init = false; 46 | 47 | let mut cmd_attrs = Vec::with_capacity(fun.attrs.len()); 48 | let mut remaining_attr = Vec::new(); 49 | for attr in fun.attrs.drain(..) { 50 | if let Some(path) = attr.meta.path().get_ident() { 51 | if path == "cmd" { 52 | cmd_attrs.push(attr); 53 | continue; 54 | } else if path == "init" { 55 | has_init = true; 56 | continue; 57 | } 58 | } 59 | 60 | remaining_attr.push(attr); 61 | } 62 | fun.attrs = remaining_attr; 63 | 64 | if has_init { 65 | fun.sig.ident = quote::format_ident!("__{}", fun.sig.ident); 66 | init_function_names.push(fun.sig.ident.clone()); 67 | init_functions.push(fun); 68 | } else { 69 | for attr in cmd_attrs { 70 | match process_cmd_definition(&fun, &dict_arg, attr) { 71 | Ok(definition) => definitions.push(definition), 72 | Err(e) => errors.push(e), 73 | } 74 | } 75 | 76 | other_functions.push(syn::ImplItem::Fn(fun)); 77 | } 78 | } 79 | 80 | if !errors.is_empty() { 81 | return TokenStream::from(Error::multiple(errors).write_errors()); 82 | } 83 | 84 | let ty = input.self_ty; 85 | let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); 86 | 87 | quote! { 88 | impl #impl_generics #ty #ty_generics #where_clause { 89 | #(#init_functions)* 90 | } 91 | 92 | #[automatically_derived] 93 | impl #impl_generics ::fift::core::Module for #ty #ty_generics #where_clause { 94 | fn init( 95 | &self, 96 | #dict_arg: &mut ::fift::core::Dictionary, 97 | ) -> ::core::result::Result<(), ::fift::error::Error> { 98 | #(self.#init_function_names(#dict_arg)?;)* 99 | #(#definitions?;)* 100 | Ok(()) 101 | } 102 | } 103 | 104 | #(#other_functions)* 105 | } 106 | .into() 107 | } 108 | 109 | fn process_cmd_definition( 110 | function: &syn::ImplItemFn, 111 | dict_arg: &syn::Ident, 112 | attr: syn::Attribute, 113 | ) -> Result { 114 | let cmd = FiftCmdArgs::from_meta(&attr.meta)?; 115 | 116 | let reg_fn = match (cmd.tail, cmd.active, cmd.stack) { 117 | (false, false, false) => quote! { define_context_word }, 118 | (true, false, false) => quote! { define_context_tail_word }, 119 | (false, true, false) => quote! { define_active_word }, 120 | (false, false, true) => quote! { define_stack_word }, 121 | _ => { 122 | return Err(Error::custom( 123 | "`tail`, `active` and `stack` cannot be used together", 124 | )); 125 | } 126 | }; 127 | 128 | let cmd_name = if cmd.without_space { 129 | cmd.name.trim().to_owned() 130 | } else { 131 | format!("{} ", cmd.name.trim()) 132 | }; 133 | 134 | let function_name = function.sig.ident.clone(); 135 | let expr = match cmd.args { 136 | None => { 137 | quote! { #function_name } 138 | } 139 | Some(mut provided_args) => { 140 | let ctx_arg = quote::format_ident!("__c"); 141 | let required_args = find_command_args(function)?; 142 | 143 | let mut errors = Vec::new(); 144 | let mut closure_args = vec![quote! { #ctx_arg }]; 145 | for arg in required_args { 146 | match provided_args.remove(&arg) { 147 | Some(value) => closure_args.push(quote! { #value }), 148 | None => errors.push(Error::custom(format!( 149 | "No value provided for the argument `{arg}`" 150 | ))), 151 | } 152 | } 153 | 154 | for arg in provided_args.into_keys() { 155 | errors.push(Error::custom(format!("Unknown function argument `{arg}`"))); 156 | } 157 | 158 | if !errors.is_empty() { 159 | return Err(Error::multiple(errors).with_span(&attr)); 160 | } 161 | 162 | quote! { |#ctx_arg| #function_name(#(#closure_args),*) } 163 | } 164 | }; 165 | 166 | Ok(syn::parse_quote! { #dict_arg.#reg_fn(#cmd_name, #expr) }) 167 | } 168 | 169 | fn find_command_args(function: &syn::ImplItemFn) -> Result, Error> { 170 | let mut inputs = function.sig.inputs.iter(); 171 | 172 | if let Some(first) = inputs.next() 173 | && !matches!(first, syn::FnArg::Typed(_)) 174 | { 175 | return Err(Error::custom("Command context argument not found").with_span(&function)); 176 | } 177 | 178 | let mut args = Vec::new(); 179 | for input in inputs { 180 | let syn::FnArg::Typed(input) = input else { 181 | continue; 182 | }; 183 | let syn::Pat::Ident(pat) = &*input.pat else { 184 | return Err(Error::custom("Unsupported argument binding").with_span(&input.pat)); 185 | }; 186 | args.push(pat.ident.to_string()); 187 | } 188 | 189 | Ok(args) 190 | } 191 | -------------------------------------------------------------------------------- /docs/install: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | if [[ ${OS:-} = Windows_NT ]]; then 5 | echo 'error: Please install fift using Windows Subsystem for Linux' 6 | exit 1 7 | fi 8 | 9 | # Reset 10 | Color_Off='' 11 | 12 | # Regular Colors 13 | Red='' 14 | Green='' 15 | Dim='' # White 16 | 17 | # Bold 18 | Bold_White='' 19 | Bold_Green='' 20 | 21 | if [[ -t 1 ]]; then 22 | # Reset 23 | Color_Off='\033[0m' # Text Reset 24 | 25 | # Regular Colors 26 | Red='\033[0;31m' # Red 27 | Green='\033[0;32m' # Green 28 | Dim='\033[0;2m' # White 29 | 30 | # Bold 31 | Bold_Green='\033[1;32m' # Bold Green 32 | Bold_White='\033[1m' # Bold White 33 | fi 34 | 35 | error() { 36 | echo -e "${Red}error${Color_Off}:" "$@" >&2 37 | exit 1 38 | } 39 | 40 | info() { 41 | echo -e "${Dim}$@ ${Color_Off}" 42 | } 43 | 44 | info_bold() { 45 | echo -e "${Bold_White}$@ ${Color_Off}" 46 | } 47 | 48 | success() { 49 | echo -e "${Green}$@ ${Color_Off}" 50 | } 51 | 52 | command -v tar >/dev/null || 53 | error 'tar is required to install fift' 54 | 55 | if [[ $# -gt 1 ]]; then 56 | error 'Too many arguments, one is allowed. You can provide a specific tag of fift to install (e.g. "v0.1.15")' 57 | fi 58 | 59 | case $(uname -ms) in 60 | 'Darwin x86_64') 61 | target=x86_64-apple-darwin 62 | ;; 63 | 'Darwin arm64') 64 | target=aarch64-apple-darwin 65 | ;; 66 | 'Linux aarch64' | 'Linux arm64') 67 | target=aarch64-unknown-linux-gnu 68 | ;; 69 | 'Linux x86_64' | *) 70 | target=x86_64-unknown-linux-gnu 71 | ;; 72 | esac 73 | 74 | if [[ $target = darwin-x64 ]]; then 75 | # Is this process running in Rosetta? 76 | # redirect stderr to devnull to avoid error message when not running in Rosetta 77 | if [[ $(sysctl -n sysctl.proc_translated 2>/dev/null) = 1 ]]; then 78 | target=aarch64-apple-darwin 79 | info "Your shell is running in Rosetta 2. Downloading fift for $target instead" 80 | fi 81 | fi 82 | 83 | GITHUB=${GITHUB-"https://github.com"} 84 | 85 | github_repo="$GITHUB/broxus/fift" 86 | 87 | exe_name=fift 88 | 89 | if [[ $# = 0 ]]; then 90 | fift_uri=$github_repo/releases/latest/download/fift-$target.tar.gz 91 | else 92 | fift_uri=$github_repo/releases/download/$1/fift-$target.tar.gz 93 | fi 94 | 95 | install_env=FIFT_INSTALL 96 | bin_env=\$$install_env/bin 97 | 98 | install_dir=${!install_env:-$HOME/.fift} 99 | bin_dir=$install_dir/bin 100 | exe=$bin_dir/fift 101 | 102 | if [[ ! -d $bin_dir ]]; then 103 | mkdir -p "$bin_dir" || 104 | error "Failed to create install directory \"$bin_dir\"" 105 | fi 106 | 107 | curl --fail --location --progress-bar --output "$exe.tar.gz" "$fift_uri" || 108 | error "Failed to download fift from \"$fift_uri\"" 109 | 110 | tar -xf "$exe.tar.gz" -C "$bin_dir" || 111 | error 'Failed to extract fift' 112 | 113 | chmod +x "$exe" || 114 | error 'Failed to set permissions on fift executable' 115 | 116 | rm -r "$exe.tar.gz" 117 | 118 | tildify() { 119 | if [[ $1 = $HOME/* ]]; then 120 | local replacement=\~/ 121 | 122 | echo "${1/$HOME\//$replacement}" 123 | else 124 | echo "$1" 125 | fi 126 | } 127 | 128 | success "fift was installed successfully to $Bold_Green$(tildify "$exe")" 129 | 130 | if command -v fift >/dev/null; then 131 | echo "Run 'fift --help' to get started" 132 | exit 133 | fi 134 | 135 | refresh_command='' 136 | 137 | tilde_bin_dir=$(tildify "$bin_dir") 138 | quoted_install_dir=\"${install_dir//\"/\\\"}\" 139 | 140 | if [[ $quoted_install_dir = \"$HOME/* ]]; then 141 | quoted_install_dir=${quoted_install_dir/$HOME\//\$HOME/} 142 | fi 143 | 144 | echo 145 | 146 | case $(basename "$SHELL") in 147 | fish) 148 | commands=( 149 | "set --export $install_env $quoted_install_dir" 150 | "set --export PATH $bin_env \$PATH" 151 | ) 152 | 153 | fish_config=$HOME/.config/fish/config.fish 154 | tilde_fish_config=$(tildify "$fish_config") 155 | 156 | if [[ -w $fish_config ]]; then 157 | { 158 | echo -e '\n# fift' 159 | 160 | for command in "${commands[@]}"; do 161 | echo "$command" 162 | done 163 | } >>"$fish_config" 164 | 165 | info "Added \"$tilde_bin_dir\" to \$PATH in \"$tilde_fish_config\"" 166 | 167 | refresh_command="source $tilde_fish_config" 168 | else 169 | echo "Manually add the directory to $tilde_fish_config (or similar):" 170 | 171 | for command in "${commands[@]}"; do 172 | info_bold " $command" 173 | done 174 | fi 175 | ;; 176 | zsh) 177 | commands=( 178 | "export $install_env=$quoted_install_dir" 179 | "export PATH=\"$bin_env:\$PATH\"" 180 | ) 181 | 182 | zsh_config=$HOME/.zshrc 183 | tilde_zsh_config=$(tildify "$zsh_config") 184 | 185 | if [[ -w $zsh_config ]]; then 186 | { 187 | echo -e '\n# fift' 188 | 189 | for command in "${commands[@]}"; do 190 | echo "$command" 191 | done 192 | } >>"$zsh_config" 193 | 194 | info "Added \"$tilde_bin_dir\" to \$PATH in \"$tilde_zsh_config\"" 195 | 196 | refresh_command="exec $SHELL" 197 | else 198 | echo "Manually add the directory to $tilde_zsh_config (or similar):" 199 | 200 | for command in "${commands[@]}"; do 201 | info_bold " $command" 202 | done 203 | fi 204 | ;; 205 | bash) 206 | commands=( 207 | "export $install_env=$quoted_install_dir" 208 | "export PATH=$bin_env:\$PATH" 209 | ) 210 | 211 | bash_configs=( 212 | "$HOME/.bashrc" 213 | "$HOME/.bash_profile" 214 | ) 215 | 216 | if [[ ${XDG_CONFIG_HOME:-} ]]; then 217 | bash_configs+=( 218 | "$XDG_CONFIG_HOME/.bash_profile" 219 | "$XDG_CONFIG_HOME/.bashrc" 220 | "$XDG_CONFIG_HOME/bash_profile" 221 | "$XDG_CONFIG_HOME/bashrc" 222 | ) 223 | fi 224 | 225 | set_manually=true 226 | for bash_config in "${bash_configs[@]}"; do 227 | tilde_bash_config=$(tildify "$bash_config") 228 | 229 | if [[ -w $bash_config ]]; then 230 | { 231 | echo -e '\n# fift' 232 | 233 | for command in "${commands[@]}"; do 234 | echo "$command" 235 | done 236 | } >>"$bash_config" 237 | 238 | info "Added \"$tilde_bin_dir\" to \$PATH in \"$tilde_bash_config\"" 239 | 240 | refresh_command="source $bash_config" 241 | set_manually=false 242 | break 243 | fi 244 | done 245 | 246 | if [[ $set_manually = true ]]; then 247 | echo "Manually add the directory to $tilde_bash_config (or similar):" 248 | 249 | for command in "${commands[@]}"; do 250 | info_bold " $command" 251 | done 252 | fi 253 | ;; 254 | *) 255 | echo 'Manually add the directory to ~/.bashrc (or similar):' 256 | info_bold " export $install_env=$quoted_install_dir" 257 | info_bold " export PATH=\"$bin_env:\$PATH\"" 258 | ;; 259 | esac 260 | 261 | echo 262 | info "To get started, run:" 263 | echo 264 | 265 | if [[ $refresh_command ]]; then 266 | info_bold " $refresh_command" 267 | fi 268 | 269 | info_bold " fift --help" 270 | -------------------------------------------------------------------------------- /src/core/mod.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroU32; 2 | 3 | use anyhow::{Context as _, Result}; 4 | pub use fift_proc::fift_module; 5 | use tycho_vm::SafeRc; 6 | 7 | pub use self::cont::{DynFiftCont, FiftCont, IntoDynFiftCont, RcFiftCont}; 8 | pub use self::dictionary::{Dictionaries, Dictionary, DictionaryEntry}; 9 | pub use self::env::{Environment, SourceBlock}; 10 | pub use self::lexer::Lexer; 11 | pub use self::stack::{ 12 | Atom, DynFiftValue, HashMapTreeKey, HashMapTreeNode, IntoDynFiftValue, OwnedCellSlice, 13 | SharedBox, Stack, StackTuple, StackValue, StackValueType, WordList, 14 | }; 15 | 16 | pub mod cont; 17 | pub mod dictionary; 18 | pub mod env; 19 | pub mod lexer; 20 | pub mod stack; 21 | 22 | pub struct Context<'a> { 23 | pub state: State, 24 | pub stack: Stack, 25 | pub exit_code: u8, 26 | pub next: Option, 27 | pub dicts: Dictionaries, 28 | 29 | pub limits: ExecutionLimits, 30 | pub stats: ExecutionStats, 31 | 32 | pub input: Lexer, 33 | pub exit_interpret: SharedBox, 34 | 35 | pub env: &'a mut dyn Environment, 36 | pub stdout: &'a mut dyn std::io::Write, 37 | pub stderr: &'a mut dyn std::fmt::Write, 38 | } 39 | 40 | impl<'a> Context<'a> { 41 | pub fn new( 42 | env: &'a mut dyn Environment, 43 | stdout: &'a mut dyn std::io::Write, 44 | stderr: &'a mut dyn std::fmt::Write, 45 | ) -> Self { 46 | Self { 47 | state: Default::default(), 48 | stack: Stack::new(None), 49 | exit_code: 0, 50 | next: None, 51 | dicts: Default::default(), 52 | limits: Default::default(), 53 | stats: Default::default(), 54 | input: Default::default(), 55 | exit_interpret: Default::default(), 56 | env, 57 | stdout, 58 | stderr, 59 | } 60 | } 61 | 62 | pub fn with_module(mut self, module: T) -> Result { 63 | self.add_module(module)?; 64 | Ok(self) 65 | } 66 | 67 | pub fn add_module(&mut self, module: T) -> Result<()> { 68 | module.init(&mut self.dicts.current) 69 | } 70 | 71 | pub fn with_source_block(mut self, block: SourceBlock) -> Self { 72 | self.add_source_block(block); 73 | self 74 | } 75 | 76 | pub fn add_source_block(&mut self, block: SourceBlock) { 77 | self.input.push_source_block(block); 78 | } 79 | 80 | pub fn with_limits(mut self, limits: ExecutionLimits) -> Self { 81 | self.set_limits(limits); 82 | self 83 | } 84 | 85 | pub fn set_limits(&mut self, limits: ExecutionLimits) { 86 | self.limits = limits; 87 | } 88 | 89 | pub fn run(&mut self) -> Result { 90 | self.stats = Default::default(); 91 | let mut current = Some(RcFiftCont::new_dyn_fift_cont(cont::InterpreterCont)); 92 | while let Some(cont) = current.take() { 93 | self.stats.inc_step(&self.limits)?; 94 | current = SafeRc::into_inner(cont).run(self)?; 95 | if current.is_none() { 96 | current = self.next.take(); 97 | } 98 | } 99 | 100 | Ok(self.exit_code) 101 | } 102 | 103 | pub(crate) fn execute_stack_top(&mut self) -> Result { 104 | let cont = self.stack.pop_cont()?; 105 | let count = self.stack.pop_smallint_range(0, 255)? as usize; 106 | self.stack.check_underflow(count)?; 107 | Ok(cont) 108 | } 109 | 110 | pub(crate) fn compile_stack_top(&mut self) -> Result<()> { 111 | let word_def = self.stack.pop_cont()?; 112 | let count = self.stack.pop_smallint_range(0, 255)? as usize; 113 | 114 | let cont = match count { 115 | 0 => None, 116 | 1 => Some(RcFiftCont::new_dyn_fift_cont(cont::LitCont( 117 | self.stack.pop()?, 118 | ))), 119 | _ => { 120 | let mut literals = Vec::with_capacity(count); 121 | for _ in 0..count { 122 | literals.push(self.stack.pop()?); 123 | } 124 | literals.reverse(); 125 | Some(RcFiftCont::new_dyn_fift_cont(cont::MultiLitCont(literals))) 126 | } 127 | }; 128 | 129 | let mut word_list = self.stack.pop_word_list()?; 130 | { 131 | let word_list = SafeRc::make_mut(&mut word_list); 132 | word_list.items.extend(cont); 133 | 134 | if !cont::NopCont::is_nop(word_def.as_ref()) { 135 | word_list.items.push(SafeRc::clone(&word_def)); 136 | } 137 | } 138 | 139 | self.stack.push_raw(word_list.into_dyn_fift_value()) 140 | } 141 | } 142 | 143 | #[derive(Debug, Default)] 144 | pub enum State { 145 | #[default] 146 | Interpret, 147 | Compile(NonZeroU32), 148 | InterpretInternal(NonZeroU32), 149 | } 150 | 151 | impl State { 152 | pub fn is_compile(&self) -> bool { 153 | matches!(self, Self::Compile(_)) 154 | } 155 | 156 | pub fn begin_compile(&mut self) -> Result<()> { 157 | match self { 158 | Self::Interpret => { 159 | *self = Self::Compile(NonZeroU32::MIN); 160 | Ok(()) 161 | } 162 | Self::Compile(depth) => { 163 | *depth = depth.checked_add(1).context("Compiler depth overflow")?; 164 | Ok(()) 165 | } 166 | Self::InterpretInternal(_) => anyhow::bail!("Expected non-internal interpreter mode"), 167 | } 168 | } 169 | 170 | pub fn end_compile(&mut self) -> Result<()> { 171 | if let Self::Compile(depth) = self { 172 | match NonZeroU32::new(depth.get() - 1) { 173 | Some(new_depth) => *depth = new_depth, 174 | None => *self = Self::Interpret, 175 | } 176 | Ok(()) 177 | } else { 178 | anyhow::bail!("Expected compilation mode") 179 | } 180 | } 181 | 182 | pub fn begin_interpret_internal(&mut self) -> Result<()> { 183 | if let Self::Compile(depth) = self { 184 | *self = Self::InterpretInternal(*depth); 185 | Ok(()) 186 | } else { 187 | anyhow::bail!("Expected compilation mode") 188 | } 189 | } 190 | 191 | pub fn end_interpret_internal(&mut self) -> Result<()> { 192 | if let Self::InterpretInternal(depth) = self { 193 | *self = Self::Compile(*depth); 194 | Ok(()) 195 | } else { 196 | anyhow::bail!("Expected internal interpreter mode") 197 | } 198 | } 199 | } 200 | 201 | pub trait Module { 202 | fn init(&self, d: &mut Dictionary) -> Result<()>; 203 | } 204 | 205 | impl Module for &T { 206 | fn init(&self, d: &mut Dictionary) -> Result<()> { 207 | T::init(self, d) 208 | } 209 | } 210 | 211 | #[derive(Debug, Default, Clone)] 212 | pub struct ExecutionLimits { 213 | pub max_steps: Option, 214 | pub max_include_depth: Option, 215 | } 216 | 217 | #[derive(Debug, Default, Clone)] 218 | pub struct ExecutionStats { 219 | pub step: usize, 220 | } 221 | 222 | impl ExecutionStats { 223 | pub fn inc_step(&mut self, limits: &ExecutionLimits) -> Result<()> { 224 | self.step += 1; 225 | if let Some(max_steps) = limits.max_steps { 226 | anyhow::ensure!( 227 | self.step <= max_steps, 228 | "Max execution steps exceeded: {max_steps}/{max_steps}" 229 | ); 230 | } 231 | Ok(()) 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /cli/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::io::{IsTerminal, Write}; 2 | use std::process::ExitCode; 3 | 4 | use anyhow::Result; 5 | use argh::FromArgs; 6 | use console::style; 7 | use fift::core::lexer::LexerPosition; 8 | use fift::core::{Environment, SourceBlock}; 9 | use unicode_width::UnicodeWidthStr; 10 | 11 | use self::env::SystemEnvironment; 12 | use self::input::LineReader; 13 | use self::modules::*; 14 | use self::util::{ArgsOrVersion, RestArgs, RestArgsDelimiter}; 15 | 16 | mod env; 17 | mod input; 18 | mod util; 19 | 20 | mod modules; 21 | 22 | /// A simple Fift interpreter. Type `bye` to quie, 23 | /// or `words` to get a list of all commands 24 | #[allow(dead_code)] 25 | #[derive(FromArgs)] 26 | struct App { 27 | /// do not preload standard preamble file `Fift.fif` 28 | #[argh(switch, short = 'n')] 29 | bare: bool, 30 | 31 | /// force interactive mode even if explicit source file names are indicated 32 | #[argh(switch, short = 'i')] 33 | interactive: bool, 34 | 35 | /// sets color-separated library source include path. 36 | /// If not indicated, $FIFTPATH is used instead 37 | #[argh(option, short = 'I')] 38 | include: Option, 39 | 40 | /// sets an explicit path to the library source file. 41 | /// If not indicated, a default one will be used 42 | #[argh(option, short = 'L')] 43 | lib: Option, 44 | 45 | /// a list of source files to execute (stdin will be used if empty) 46 | #[argh(positional)] 47 | source_files: Vec, 48 | } 49 | 50 | #[allow(unused)] 51 | #[derive(Default)] 52 | struct ScriptModeDelim; 53 | 54 | impl RestArgsDelimiter for ScriptModeDelim { 55 | const DELIM: &'static str = "-s"; 56 | const DESCR: &'static str = r"script mode: use first argument as a fift source file and 57 | import remaining arguments as $n"; 58 | } 59 | 60 | fn main() -> Result { 61 | let RestArgs(ArgsOrVersion::(app), rest, ScriptModeDelim) = argh::from_env(); 62 | // Prepare system environment 63 | let mut env = SystemEnvironment::with_include_dirs( 64 | &app.include 65 | .unwrap_or_else(|| std::env::var("FIFTPATH").unwrap_or_default()), 66 | ); 67 | 68 | let is_terminal = std::io::stdin().is_terminal(); 69 | let interactive = app.interactive || rest.is_empty() && app.source_files.is_empty(); 70 | 71 | // Prepare the source block which will be executed 72 | let mut stdout: Box = Box::new(std::io::stdout()); 73 | 74 | let mut source_blocks = Vec::new(); 75 | 76 | let mut stderr: Box; 77 | if interactive { 78 | if is_terminal { 79 | let mut line_reader = LineReader::new()?; 80 | stdout = line_reader.create_external_printer()?; 81 | stderr = Box::new(ColorStderrWriter(std::io::stderr())); 82 | source_blocks.push(SourceBlock::new("", line_reader)); 83 | } else { 84 | stderr = Box::new(PlainStderrWriter(std::io::stderr())); 85 | source_blocks.push(SourceBlock::new("", std::io::stdin().lock())); 86 | } 87 | } else { 88 | stderr = Box::new(ColorStderrWriter(std::io::stderr())); 89 | }; 90 | 91 | if let Some(path) = rest.first() { 92 | source_blocks.push(env.include(path)?); 93 | } 94 | 95 | for path in app.source_files.into_iter().rev() { 96 | source_blocks.push(env.include(&path)?); 97 | } 98 | 99 | // Prepare preamble block 100 | if let Some(lib) = &app.lib { 101 | source_blocks.push(env.include(lib)?); 102 | } else if !app.bare { 103 | source_blocks.push(env.include(fift_libs::base_lib().name)?); 104 | } 105 | 106 | // Prepare Fift context 107 | let mut ctx = fift::Context::new(&mut env, &mut stdout, &mut *stderr) 108 | .with_basic_modules()? 109 | .with_module(CmdArgsUtils::new(rest))? 110 | .with_module(ShellUtils)?; 111 | 112 | for source_block in source_blocks { 113 | ctx.add_source_block(source_block); 114 | } 115 | 116 | // Execute 117 | loop { 118 | let error = match ctx.run() { 119 | Ok(exit_code) => return Ok(ExitCode::from(!exit_code)), 120 | Err(e) => e, 121 | }; 122 | 123 | let terminate = !is_terminal 124 | && error 125 | .downcast_ref::() 126 | .is_some(); 127 | 128 | if interactive && !terminate { 129 | eprintln!("{}", style("!!!").dim()) 130 | } 131 | 132 | if let Some(pos) = ctx.input.get_position() { 133 | eprintln!("{}", Report { pos, error }); 134 | }; 135 | 136 | if let Some(next) = ctx.next.take() { 137 | eprintln!( 138 | "{}\n{}", 139 | style("backtrace:").red(), 140 | style(next.display_backtrace(&ctx.dicts.current)).dim() 141 | ); 142 | } 143 | 144 | if !interactive || terminate { 145 | return Ok(ExitCode::FAILURE); 146 | } 147 | 148 | eprintln!(); 149 | ctx.input.reset_until_base(); 150 | ctx.stack.clear(); 151 | } 152 | } 153 | 154 | struct Report<'a, E> { 155 | pos: LexerPosition<'a>, 156 | error: E, 157 | } 158 | 159 | impl std::fmt::Display for Report<'_, E> 160 | where 161 | E: std::fmt::Debug, 162 | { 163 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 164 | let line_number = self.pos.line_number.to_string(); 165 | let offset_len = line_number.len(); 166 | let offset = format!("{:offset_len$}", ""); 167 | 168 | let arrow = style("-->").blue().bold(); 169 | let block = style("|").blue().bold(); 170 | let line_number = style(line_number).blue().bold(); 171 | 172 | let line = self.pos.line.trim_end(); 173 | let word_start = std::cmp::min(self.pos.word_start, line.len()); 174 | let word_end = std::cmp::min(self.pos.word_end, line.len()); 175 | let (line_start, rest) = line.split_at(word_start); 176 | let (underlined, line_end) = rest.split_at(word_end - word_start); 177 | 178 | let line_start_len = UnicodeWidthStr::width(line_start); 179 | let underlined_len = UnicodeWidthStr::width(underlined); 180 | 181 | write!( 182 | f, 183 | "{}{:?}\n\ 184 | {offset}{arrow} {}:{}:{}\n\ 185 | {offset} {block}\n\ 186 | {line_number} {block} {}{}{}\n\ 187 | {offset} {block} {:line_start_len$}{}\n\ 188 | {offset} {block}", 189 | style("error: ").red(), 190 | style(&self.error).bold(), 191 | self.pos.source_block_name, 192 | self.pos.line_number, 193 | self.pos.word_start + 1, 194 | line_start, 195 | style(underlined).red(), 196 | line_end, 197 | "", 198 | style(format!("{:->1$}", "", underlined_len)).red(), 199 | ) 200 | } 201 | } 202 | 203 | struct PlainStderrWriter(std::io::Stderr); 204 | 205 | impl std::fmt::Write for PlainStderrWriter { 206 | fn write_str(&mut self, s: &str) -> std::fmt::Result { 207 | self.0.write_all(s.as_bytes()).map_err(|_| std::fmt::Error) 208 | } 209 | } 210 | 211 | struct ColorStderrWriter(std::io::Stderr); 212 | 213 | impl std::fmt::Write for ColorStderrWriter { 214 | fn write_str(&mut self, s: &str) -> std::fmt::Result { 215 | self.0 216 | .write_fmt(format_args!("{}", style(s).green())) 217 | .map_err(|_| std::fmt::Error) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/modules/stack_utils.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use anyhow::Result; 4 | use num_traits::Zero; 5 | use tycho_vm::SafeRc; 6 | 7 | use crate::core::*; 8 | 9 | pub struct StackUtils; 10 | 11 | #[fift_module] 12 | impl StackUtils { 13 | #[cmd(name = "drop", stack)] 14 | fn interpret_drop(stack: &mut Stack) -> Result<()> { 15 | stack.pop()?; 16 | Ok(()) 17 | } 18 | 19 | #[cmd(name = "2drop", stack)] 20 | fn interpret_2drop(stack: &mut Stack) -> Result<()> { 21 | stack.pop()?; 22 | stack.pop()?; 23 | Ok(()) 24 | } 25 | 26 | #[cmd(name = "dup", stack)] 27 | fn interpret_dup(stack: &mut Stack) -> Result<()> { 28 | stack.push_raw(stack.fetch(0)?) 29 | } 30 | 31 | #[cmd(name = "2dup", stack)] 32 | fn interpret_2dup(stack: &mut Stack) -> Result<()> { 33 | stack.push_raw(stack.fetch(1)?)?; 34 | stack.push_raw(stack.fetch(1)?) 35 | } 36 | 37 | #[cmd(name = "over", stack)] 38 | fn interpret_over(stack: &mut Stack) -> Result<()> { 39 | stack.push_raw(stack.fetch(1)?) 40 | } 41 | 42 | #[cmd(name = "2over", stack)] 43 | fn interpret_2over(stack: &mut Stack) -> Result<()> { 44 | stack.push_raw(stack.fetch(3)?)?; 45 | stack.push_raw(stack.fetch(3)?) 46 | } 47 | 48 | #[cmd(name = "swap", stack)] 49 | fn interpret_swap(stack: &mut Stack) -> Result<()> { 50 | stack.swap(0, 1) 51 | } 52 | 53 | #[cmd(name = "2swap", stack)] 54 | fn interpret_2swap(stack: &mut Stack) -> Result<()> { 55 | stack.swap(0, 2)?; 56 | stack.swap(1, 3) 57 | } 58 | 59 | #[cmd(name = "tuck", stack)] 60 | fn interpret_tuck(stack: &mut Stack) -> Result<()> { 61 | stack.swap(0, 1)?; 62 | stack.push_raw(stack.fetch(1)?) 63 | } 64 | 65 | #[cmd(name = "nip", stack)] 66 | fn interpret_nip(stack: &mut Stack) -> Result<()> { 67 | stack.swap(0, 1)?; 68 | stack.pop()?; 69 | Ok(()) 70 | } 71 | 72 | #[cmd(name = "rot", stack)] 73 | fn interpret_rot(stack: &mut Stack) -> Result<()> { 74 | stack.swap(1, 2)?; 75 | stack.swap(0, 1) 76 | } 77 | 78 | #[cmd(name = "-rot", stack)] 79 | fn interpret_rot_rev(stack: &mut Stack) -> Result<()> { 80 | stack.swap(0, 1)?; 81 | stack.swap(1, 2) 82 | } 83 | 84 | #[cmd(name = "pick", stack)] 85 | fn interpret_pick(stack: &mut Stack) -> Result<()> { 86 | let n = stack.pop_smallint_range(0, 255)? as usize; 87 | stack.push_raw(stack.fetch(n)?) 88 | } 89 | 90 | #[cmd(name = "roll", stack)] 91 | fn interpret_roll(stack: &mut Stack) -> Result<()> { 92 | let n = stack.pop_smallint_range(0, 255)? as usize; 93 | for i in (1..=n).rev() { 94 | stack.swap(i, i - 1)?; 95 | } 96 | Ok(()) 97 | } 98 | 99 | #[cmd(name = "-roll", stack)] 100 | fn interpret_roll_rev(stack: &mut Stack) -> Result<()> { 101 | let n = stack.pop_smallint_range(0, 255)? as usize; 102 | for i in 0..n { 103 | stack.swap(i, i + 1)?; 104 | } 105 | Ok(()) 106 | } 107 | 108 | #[cmd(name = "reverse", stack)] 109 | fn interpret_reverse(stack: &mut Stack) -> Result<()> { 110 | let m = stack.pop_smallint_range(0, 255)? as usize; 111 | let n = stack.pop_smallint_range(0, 255)? as usize; 112 | if n == 0 { 113 | return Ok(()); 114 | } 115 | 116 | stack.check_underflow(n + m)?; 117 | let s = 2 * m + n - 1; 118 | for i in (m..=(s - 1) >> 1).rev() { 119 | stack.swap(i, s - i)?; 120 | } 121 | Ok(()) 122 | } 123 | 124 | #[cmd(name = "exch", stack)] 125 | fn interpret_exch(stack: &mut Stack) -> Result<()> { 126 | let n = stack.pop_smallint_range(0, 255)? as usize; 127 | stack.swap(0, n) 128 | } 129 | 130 | #[cmd(name = "exch2", stack)] 131 | fn interpret_exch2(stack: &mut Stack) -> Result<()> { 132 | let n = stack.pop_smallint_range(0, 255)? as usize; 133 | let m = stack.pop_smallint_range(0, 255)? as usize; 134 | stack.swap(n, m) 135 | } 136 | 137 | #[cmd(name = "depth", stack)] 138 | fn interpret_depth(stack: &mut Stack) -> Result<()> { 139 | stack.push_int(stack.depth()) 140 | } 141 | 142 | #[cmd(name = "?dup", stack)] 143 | fn interpret_cond_dup(stack: &mut Stack) -> Result<()> { 144 | let item = stack.pop_int()?; 145 | if !item.is_zero() { 146 | stack.push_raw(item.clone().into_dyn_fift_value())?; 147 | } 148 | stack.push_raw(item.into_dyn_fift_value()) 149 | } 150 | 151 | /// === Low-level stack manipulation === 152 | 153 | #[cmd(name = "", stack)] 154 | fn interpret_make_xchg(stack: &mut Stack) -> Result<()> { 155 | let mut y = stack.pop_smallint_range(0, 255)?; 156 | let mut x = stack.pop_smallint_range(0, 255)?; 157 | if x > y { 158 | std::mem::swap(&mut x, &mut y); 159 | } 160 | 161 | match (x, y) { 162 | (0, 0) => stack.push_raw(cont::NopCont::instance().into_dyn_fift_value()), 163 | (0, 1) => stack.push_raw(SafeRc::new_dyn_fift_value( 164 | interpret_swap as cont::StackWordFunc, 165 | )), 166 | _ => stack.push_raw(SafeRc::new_dyn_fift_value(XchgCont { x, y })), 167 | } 168 | } 169 | 170 | #[cmd(name = "", stack)] 171 | fn interpret_make_push(stack: &mut Stack) -> Result<()> { 172 | let x = stack.pop_smallint_range(0, 255)?; 173 | match x { 174 | 0 => stack.push_raw(SafeRc::new_dyn_fift_value( 175 | interpret_dup as cont::StackWordFunc, 176 | )), 177 | 1 => stack.push_raw(SafeRc::new_dyn_fift_value( 178 | interpret_over as cont::StackWordFunc, 179 | )), 180 | _ => stack.push_raw(SafeRc::new_dyn_fift_value(PushCont(x))), 181 | } 182 | } 183 | 184 | #[cmd(name = "", stack)] 185 | fn interpret_make_pop(stack: &mut Stack) -> Result<()> { 186 | let x = stack.pop_smallint_range(0, 255)?; 187 | match x { 188 | 0 => stack.push_raw(SafeRc::new_dyn_fift_value( 189 | interpret_drop as cont::StackWordFunc, 190 | )), 191 | 1 => stack.push_raw(SafeRc::new_dyn_fift_value( 192 | interpret_nip as cont::StackWordFunc, 193 | )), 194 | _ => stack.push_raw(SafeRc::new_dyn_fift_value(PopCont(x))), 195 | } 196 | } 197 | } 198 | 199 | #[derive(Clone, Copy)] 200 | struct XchgCont { 201 | x: u32, 202 | y: u32, 203 | } 204 | 205 | impl cont::FiftCont for XchgCont { 206 | fn run(self: Rc, ctx: &mut Context) -> Result> { 207 | ctx.stack.swap(self.x as usize, self.y as usize)?; 208 | Ok(None) 209 | } 210 | 211 | fn fmt_name(&self, _: &Dictionary, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 212 | write!(f, "", self.x, self.y) 213 | } 214 | } 215 | 216 | #[derive(Clone, Copy)] 217 | struct PushCont(u32); 218 | 219 | impl cont::FiftCont for PushCont { 220 | fn run(self: Rc, ctx: &mut Context) -> Result> { 221 | let item = ctx.stack.fetch(self.0 as usize)?; 222 | ctx.stack.push_raw(item)?; 223 | Ok(None) 224 | } 225 | 226 | fn fmt_name(&self, _: &Dictionary, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 227 | write!(f, "", self.0) 228 | } 229 | } 230 | 231 | #[derive(Clone, Copy)] 232 | struct PopCont(u32); 233 | 234 | impl cont::FiftCont for PopCont { 235 | fn run(self: Rc, ctx: &mut Context) -> Result> { 236 | ctx.stack.swap(0, self.0 as usize)?; 237 | ctx.stack.pop()?; 238 | Ok(None) 239 | } 240 | 241 | fn fmt_name(&self, _: &Dictionary, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 242 | write!(f, "", self.0) 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/core/dictionary.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use tycho_vm::SafeRc; 3 | 4 | use super::cont::{ContextTailWordFunc, ContextWordFunc, FiftCont, RcFiftCont, StackWordFunc}; 5 | use super::stack::{ 6 | DynFiftValue, HashMapTreeKey, HashMapTreeKeyRef, HashMapTreeNode, SharedBox, StackValue, 7 | StackValueType, 8 | }; 9 | use super::{DynFiftCont, IntoDynFiftCont, IntoDynFiftValue}; 10 | 11 | pub struct Dictionaries { 12 | pub current: Dictionary, 13 | pub original: Dictionary, 14 | pub context: Dictionary, 15 | } 16 | 17 | impl Default for Dictionaries { 18 | fn default() -> Self { 19 | let current = Dictionary::default(); 20 | Self { 21 | original: current.clone(), 22 | context: current.clone(), 23 | current, 24 | } 25 | } 26 | } 27 | 28 | impl Dictionaries { 29 | pub fn lookup(&self, word: &String, allow_space: bool) -> Result> { 30 | if allow_space { 31 | let mut entry = self.lookup(word, false)?; 32 | 33 | if entry.is_none() { 34 | entry = self.lookup(&format!("{word} "), false)?; 35 | } 36 | 37 | return Ok(entry); 38 | } 39 | 40 | let mut entry = self.context.lookup(word)?; 41 | 42 | if entry.is_none() && self.current != self.context { 43 | entry = self.current.lookup(word)?; 44 | } 45 | 46 | if entry.is_none() && self.original != self.context && self.original != self.current { 47 | entry = self.original.lookup(word)?; 48 | } 49 | 50 | Ok(entry) 51 | } 52 | } 53 | 54 | #[derive(Default, Clone, Eq, PartialEq)] 55 | pub struct Dictionary { 56 | words: SafeRc, 57 | } 58 | 59 | impl Dictionary { 60 | pub fn set_words_box(&mut self, words: SafeRc) { 61 | self.words = words; 62 | } 63 | 64 | pub fn get_words_box(&self) -> &SafeRc { 65 | &self.words 66 | } 67 | 68 | pub fn clone_words_map(&self) -> Result>> { 69 | let words = self.words.fetch(); 70 | Ok(match words.ty() { 71 | StackValueType::Null => None, 72 | _ => Some(words.into_hashmap()?), 73 | }) 74 | } 75 | 76 | pub fn use_words_map(&mut self) -> Result> { 77 | let words = self.words.take(); 78 | let map = if words.is_null() { 79 | None 80 | } else { 81 | Some(words.into_hashmap()?) 82 | }; 83 | Ok(WordsRefMut { 84 | words_box: &mut self.words, 85 | map, 86 | }) 87 | } 88 | 89 | pub fn lookup(&self, name: &String) -> Result> { 90 | let map = self.clone_words_map()?; 91 | let key = HashMapTreeKeyRef::from(name); 92 | let Some(node) = HashMapTreeNode::lookup(&map, key) else { 93 | return Ok(None); 94 | }; 95 | Ok(DictionaryEntry::try_from_value(node.value.clone())) 96 | } 97 | 98 | pub fn resolve_name(&self, definition: &dyn FiftCont) -> Option> { 99 | let map = self.words.borrow(); 100 | if let Ok(map) = map.as_hashmap() { 101 | for entry in map { 102 | let Some((cont, _)) = DictionaryEntry::cont_ref_from_value(&*entry.value) else { 103 | continue; 104 | }; 105 | if std::ptr::addr_eq(cont, definition) { 106 | return entry.key.stack_value.clone().into_string().ok(); 107 | } 108 | } 109 | } 110 | None 111 | } 112 | 113 | pub fn define_context_word>( 114 | &mut self, 115 | name: T, 116 | f: ContextWordFunc, 117 | ) -> Result<()> { 118 | self.define_word(name, DictionaryEntry { 119 | definition: SafeRc::new_dyn_fift_cont(f), 120 | active: false, 121 | }) 122 | } 123 | 124 | pub fn define_context_tail_word>( 125 | &mut self, 126 | name: T, 127 | f: ContextTailWordFunc, 128 | ) -> Result<()> { 129 | self.define_word(name, DictionaryEntry { 130 | definition: SafeRc::new_dyn_fift_cont(f), 131 | active: false, 132 | }) 133 | } 134 | 135 | pub fn define_active_word>( 136 | &mut self, 137 | name: T, 138 | f: ContextWordFunc, 139 | ) -> Result<()> { 140 | self.define_word(name, DictionaryEntry { 141 | definition: SafeRc::new_dyn_fift_cont(f), 142 | active: true, 143 | }) 144 | } 145 | 146 | pub fn define_stack_word>(&mut self, name: T, f: StackWordFunc) -> Result<()> { 147 | self.define_word(name, DictionaryEntry { 148 | definition: SafeRc::new_dyn_fift_cont(f), 149 | active: false, 150 | }) 151 | } 152 | 153 | pub fn define_word(&mut self, name: T, word: E) -> Result<()> 154 | where 155 | T: Into, 156 | E: Into, 157 | { 158 | fn define_word_impl(d: &mut Dictionary, name: String, word: DictionaryEntry) -> Result<()> { 159 | let mut map = d.use_words_map()?; 160 | 161 | let key = HashMapTreeKey::from(name); 162 | let value = &word.into(); 163 | HashMapTreeNode::set(&mut map, &key, value); 164 | Ok(()) 165 | } 166 | define_word_impl(self, name.into(), word.into()) 167 | } 168 | 169 | pub fn undefine_word(&mut self, name: &String) -> Result { 170 | let mut map = self.use_words_map()?; 171 | 172 | let key = HashMapTreeKeyRef::from(name); 173 | Ok(HashMapTreeNode::remove(&mut map, key).is_some()) 174 | } 175 | } 176 | 177 | pub struct DictionaryEntry { 178 | pub definition: RcFiftCont, 179 | pub active: bool, 180 | } 181 | 182 | impl DictionaryEntry { 183 | fn try_from_value(value: SafeRc) -> Option { 184 | let (definition, active) = Self::cont_from_value(value)?; 185 | Some(Self { definition, active }) 186 | } 187 | 188 | fn cont_from_value(value: SafeRc) -> Option<(RcFiftCont, bool)> { 189 | if value.ty() == StackValueType::Cont { 190 | return Some((value.into_cont().unwrap(), false)); 191 | } else if let Ok(tuple) = value.as_tuple() 192 | && tuple.len() == 1 193 | && let Ok(cont) = tuple.first().cloned()?.into_cont() 194 | { 195 | return Some((cont, true)); 196 | } 197 | None 198 | } 199 | 200 | fn cont_ref_from_value(value: &dyn StackValue) -> Option<(&dyn FiftCont, bool)> { 201 | if let Ok(value) = value.as_cont() { 202 | return Some((value, false)); 203 | } else if let Ok(tuple) = value.as_tuple() 204 | && tuple.len() == 1 205 | && let Ok(cont) = tuple.first()?.as_cont() 206 | { 207 | return Some((cont, true)); 208 | } 209 | None 210 | } 211 | } 212 | 213 | impl From for DictionaryEntry { 214 | fn from(value: RcFiftCont) -> Self { 215 | Self { 216 | definition: value, 217 | active: false, 218 | } 219 | } 220 | } 221 | 222 | impl From> for DictionaryEntry { 223 | fn from(value: SafeRc) -> Self { 224 | Self { 225 | definition: value.into_dyn_fift_cont(), 226 | active: false, 227 | } 228 | } 229 | } 230 | 231 | impl From for SafeRc { 232 | fn from(value: DictionaryEntry) -> Self { 233 | let cont = value.definition.into_dyn_fift_value(); 234 | if value.active { 235 | SafeRc::new_dyn_fift_value(vec![cont]) 236 | } else { 237 | cont 238 | } 239 | } 240 | } 241 | 242 | pub struct WordsRefMut<'a> { 243 | words_box: &'a mut SafeRc, 244 | map: Option>, 245 | } 246 | 247 | impl std::ops::Deref for WordsRefMut<'_> { 248 | type Target = Option>; 249 | 250 | #[inline] 251 | fn deref(&self) -> &Self::Target { 252 | &self.map 253 | } 254 | } 255 | 256 | impl std::ops::DerefMut for WordsRefMut<'_> { 257 | #[inline] 258 | fn deref_mut(&mut self) -> &mut Self::Target { 259 | &mut self.map 260 | } 261 | } 262 | 263 | impl Drop for WordsRefMut<'_> { 264 | fn drop(&mut self) { 265 | self.words_box.store_opt(self.map.take()); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /libs/src/Lists.fif: -------------------------------------------------------------------------------- 1 | library Lists // List utilities 2 | // 3 | { hole dup 1 { @ execute } does create } : recursive 4 | // x x' -- ? recursively compares two S-expressions 5 | recursive equal? { 6 | dup tuple? { 7 | over tuple? { 8 | over count over count over = { // t t' l ? 9 | 0 { dup 0>= { 2dup [] 3 pick 2 pick [] equal? { 1+ } { drop -1 } cond 10 | } if } rot times 11 | nip nip 0>= 12 | } { drop 2drop false } cond 13 | } { 2drop false } cond 14 | } { eqv? } cond 15 | } swap ! 16 | // (a1 .. an) -- (an .. a1) 17 | { null swap { dup null? not } { uncons swap rot cons swap } while drop } : list-reverse 18 | // (a1 .. an) -- an Computes last element of non-empty list l 19 | { { uncons dup null? { drop true } { nip false } cond } until } : list-last 20 | // l l' -- l++l' Concatenates two lists 21 | recursive list+ { 22 | over null? { nip } { swap uncons rot list+ cons } cond 23 | } swap ! 24 | // l l' -- l'' -1 or 0, where l = l' ++ l'' 25 | // Removes prefix from list 26 | { { dup null? { drop true true } { 27 | swap dup null? { 2drop false true } { // l' l 28 | uncons swap rot uncons -rot equal? { false } { 29 | 2drop false true 30 | } cond } cond } cond } until 31 | } : list- 32 | // (a1 .. an) -- a1 .. an n Explodes a list 33 | { 0 { over null? not } { swap uncons rot 1+ } while nip } : explode-list 34 | // (a1 .. an) x -- a1 .. an n x Explodes a list under the topmost element 35 | { swap explode-list dup 1+ roll } : explode-list-1 36 | // l -- t Transforms a list into a tuple with the same elements 37 | { explode-list tuple } : list>tuple 38 | // a1 ... an n x -- (a1 .. an) x 39 | { null swap rot { -rot cons swap } swap times } : mklist-1 40 | // (s1 ... sn) -- s1+...+sn Concatenates a list of strings 41 | { "" { over null? not } { swap uncons -rot $+ } while nip 42 | } : concat-string-list 43 | // (x1 ... xn) -- x1+...+xn Sums a list of integers 44 | { 0 { over null? not } { swap uncons -rot + } while nip 45 | } : sum-list 46 | // (a1 ... an) a e -- e(...e(e(a,a1),a2),...),an) 47 | { -rot { over null? not } { swap uncons -rot 3 pick execute } while nip nip 48 | } : foldl 49 | // (a1 ... an) e -- e(...e(e(a1,a2),a3),...),an) 50 | { swap uncons swap rot foldl } : foldl-ne 51 | // (a1 ... an) a e -- e(a1,e(a2,...,e(an,a)...)) 52 | recursive foldr { 53 | rot dup null? { 2drop } { 54 | uncons -rot 2swap swap 3 pick foldr rot execute 55 | } cond 56 | } swap ! 57 | // (a1 ... an) e -- e(a1,e(a2,...,e(a[n-1],an)...)) 58 | recursive foldr-ne { 59 | over cdr null? { drop car } { 60 | swap uncons 2 pick foldr-ne rot execute 61 | } cond 62 | } swap ! 63 | // (l1 ... ln) -- l1++...++ln Concatenates a list of lists 64 | { dup null? { ' list+ foldr-ne } ifnot } : concat-list-lists 65 | // (a1 .. an . t) n -- t Computes the n-th tail of a list 66 | { ' cdr swap times } : list-tail 67 | // (a0 .. an ..) n -- an Computes the n-th element of a list 68 | { list-tail car } : list-ref 69 | // l -- ? 70 | { { dup null? { drop true true } { 71 | dup pair? { cdr false } { 72 | drop false true 73 | } cond } cond } until 74 | } : list? 75 | // l -- n 76 | { 0 { over null? not } { 1+ swap uncons nip swap } while nip 77 | } : list-length 78 | // l e -- t // returns tail of l after first member that satisfies e 79 | { swap { 80 | dup null? { nip true } { 81 | tuck car over execute { drop true } { 82 | swap cdr false 83 | } cond } cond } until 84 | } : list-tail-from 85 | // a l -- t // tail of l after first occurence of a using eq? 86 | { swap 1 ' eq? does list-tail-from } : list-member-eq 87 | { swap 1 ' eqv? does list-tail-from } : list-member-eqv 88 | { swap 1 ' equal? does list-tail-from } : list-member-equal 89 | // a l -- ? 90 | { list-member-eq null? not } : list-member? 91 | { list-member-eqv null? not } : list-member-eqv? 92 | // l -- a -1 or 0 // returns car l if l is non-empty 93 | { dup null? { drop false } { car true } cond 94 | } : safe-car 95 | { dup null? { drop false } { car second true } cond 96 | } : get-first-value 97 | // l e -- v -1 or 0 98 | { list-tail-from safe-car } : assoc-gen 99 | { list-tail-from get-first-value } : assoc-gen-x 100 | // a l -- (a.v) -1 or 0 -- returns first entry (a . v) in l 101 | { swap 1 { swap first eq? } does assoc-gen } : assq 102 | { swap 1 { swap first eqv? } does assoc-gen } : assv 103 | { swap 1 { swap first equal? } does assoc-gen } : assoc 104 | // a l -- v -1 or 0 -- returns v from first entry (a . v) in l 105 | { swap 1 { swap first eq? } does assoc-gen-x } : assq-val 106 | { swap 1 { swap first eqv? } does assoc-gen-x } : assv-val 107 | { swap 1 { swap first equal? } does assoc-gen-x } : assoc-val 108 | // (a1 .. an) e -- (e(a1) .. e(an)) 109 | recursive list-map { 110 | over null? { drop } { 111 | swap uncons -rot over execute -rot list-map cons 112 | } cond 113 | } swap ! 114 | 115 | variable ctxdump variable curctx 116 | // (a1 .. an) e -- executes e for a1, ..., an 117 | { ctxdump @ curctx @ ctxdump 2! curctx 2! 118 | { curctx 2@ over null? not } { swap uncons rot tuck curctx 2! execute } 119 | while 2drop ctxdump 2@ curctx ! ctxdump ! 120 | } : list-foreach 121 | forget ctxdump forget curctx 122 | 123 | // 124 | // Experimental implementation of `for` loops with index 125 | // 126 | variable loopdump variable curloop 127 | { curloop @ loopdump @ loopdump 2! } : push-loop-ctx 128 | { loopdump 2@ loopdump ! curloop ! } : pop-loop-ctx 129 | // ilast i0 e -- executes e for i=i0,i0+1,...,ilast-1 130 | { -rot 2dup > { 131 | push-loop-ctx { 132 | triple dup curloop ! first execute curloop @ untriple 1+ 2dup <= 133 | } until pop-loop-ctx 134 | } if 2drop drop 135 | } : for 136 | // ilast i0 e -- same as 'for', but pushes current index i before executing e 137 | { -rot 2dup > { 138 | push-loop-ctx { 139 | triple dup curloop ! untriple nip swap execute curloop @ untriple 1+ 2dup <= 140 | } until pop-loop-ctx 141 | } if 2drop drop 142 | } : for-i 143 | // ( -- i ) Returns innermost loop index 144 | { curloop @ third } : i 145 | // ( -- j ) Returns outer loop index 146 | { loopdump @ car third } : j 147 | { loopdump @ cadr third } : k 148 | forget curloop forget loopdump 149 | 150 | // 151 | // create Lisp-style lists using words "(" and ")" 152 | // 153 | variable ') 154 | 'nop box constant ', 155 | { ") without (" abort } ') ! 156 | { ') @ execute } : ) 157 | anon constant dot-marker 158 | // m x1 ... xn t m -- (x1 ... xn . t) 159 | { swap 160 | { -rot 2dup eq? not } 161 | { over dot-marker eq? abort"invalid dotted list" 162 | swap rot cons } while 2drop 163 | } : list-tail-until-marker 164 | // m x1 ... xn m -- (x1 ... xn) 165 | { null swap list-tail-until-marker } : list-until-marker 166 | { over dot-marker eq? { nip 2dup eq? abort"invalid dotted list" } 167 | { null swap } cond 168 | list-tail-until-marker 169 | } : list-until-marker-ext 170 | { ') @ ', @ } : ops-get 171 | { ', ! ') ! } : ops-set 172 | { anon dup ops-get 3 { ops-set list-until-marker-ext } does ') ! 'nop ', ! 173 | } : ( 174 | // test of Lisp-style lists 175 | // ( 42 ( `+ 9 ( `* 3 4 ) ) "test" ) .l cr 176 | // ( `eq? ( `* 3 4 ) 3 4 * ) .l cr 177 | // `alpha ( `beta `gamma `delta ) cons .l cr 178 | // { ( `eq? ( `* 3 5 pick ) 3 4 roll * ) } : 3*sample 179 | // 17 3*sample .l cr 180 | 181 | // similar syntax _( x1 .. xn ) for tuples 182 | { 2 { 1+ 2dup pick eq? } until 3 - nip } : count-to-marker 183 | { count-to-marker tuple nip } : tuple-until-marker 184 | { anon dup ops-get 3 { ops-set tuple-until-marker } does ') ! 'nop ', ! } : _( 185 | // test of tuples 186 | // _( _( 2 "two" ) _( 3 "three" ) _( 4 "four" ) ) .dump cr 187 | 188 | // pseudo-Lisp tokenizer 189 | "()[]'" 34 hold constant lisp-delims 190 | { lisp-delims 11 (word) } : lisp-token 191 | { null cons `quote swap cons } : do-quote 192 | { 1 { ', @ 2 { 2 { ', ! execute ', @ execute } does ', ! } 193 | does ', ! } does 194 | } : postpone-prefix 195 | { ', @ 1 { ', ! } does ', ! } : postpone-', 196 | ( `( ' ( pair 197 | `) ' ) pair 198 | `[ ' _( pair 199 | `] ' ) pair 200 | `' ' do-quote postpone-prefix pair 201 | `. ' dot-marker postpone-prefix pair 202 | `" { char " word } pair 203 | `;; { 0 word drop postpone-', } pair 204 | ) constant lisp-token-dict 205 | variable eol 206 | { eol @ eol 0! anon dup ') @ 'nop 3 207 | { ops-set list-until-marker-ext true eol ! } does ') ! rot ', ! 208 | { lisp-token dup (number) dup { roll drop } { 209 | drop atom dup lisp-token-dict assq { nip second execute } if 210 | } cond 211 | ', @ execute 212 | eol @ 213 | } until 214 | -rot eol ! execute 215 | } :_ List-generic( 216 | { 'nop 'nop List-generic( } :_ LIST( 217 | // LIST((lambda (x) (+ x 1)) (* 3 4)) 218 | // LIST('(+ 3 4)) 219 | // LIST(2 3 "test" . 9) 220 | // LIST((process '[plus 3 4])) 221 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use crc::Crc; 3 | use num_bigint::BigInt; 4 | use num_traits::Num; 5 | use tycho_types::cell::MAX_BIT_LEN; 6 | use tycho_types::prelude::*; 7 | use unicode_segmentation::UnicodeSegmentation; 8 | 9 | pub const CRC_16: Crc = Crc::::new(&crc::CRC_16_XMODEM); 10 | pub const CRC_32: Crc = Crc::::new(&crc::CRC_32_ISO_HDLC); 11 | pub const CRC_32_C: Crc = Crc::::new(&crc::CRC_32_ISCSI); 12 | 13 | pub struct ImmediateInt { 14 | pub num: BigInt, 15 | pub denom: Option, 16 | } 17 | 18 | impl ImmediateInt { 19 | pub fn try_from_str(s: &str) -> Result> { 20 | let (num, denom) = if let Some((left, right)) = s.split_once('.') { 21 | let Some(mut left) = Self::parse_single_number(left)? else { 22 | return Ok(None); 23 | }; 24 | let Ok(denom) = BigInt::from_str_radix(right, left.base) else { 25 | anyhow::bail!("Invalid number"); 26 | }; 27 | 28 | let scale = BigInt::from(left.base).pow(right.len() as u32); 29 | left.num *= &scale; 30 | left.num += denom; 31 | (left.num, Some(scale)) 32 | } else if let Some((left, right)) = s.split_once('/') { 33 | let Some(SingleParsedNumber { num, .. }) = Self::parse_single_number(left)? else { 34 | return Ok(None); 35 | }; 36 | let Some(SingleParsedNumber { num: denom, .. }) = Self::parse_single_number(right)? 37 | else { 38 | anyhow::bail!("Invalid number"); 39 | }; 40 | (num, Some(denom)) 41 | } else { 42 | let Some(SingleParsedNumber { num, .. }) = Self::parse_single_number(s)? else { 43 | return Ok(None); 44 | }; 45 | (num, None) 46 | }; 47 | Ok(Some(ImmediateInt { num, denom })) 48 | } 49 | 50 | // Returns parsed int and its base. 51 | fn parse_single_number(s: &str) -> Result> { 52 | let (neg, mut s) = match s.strip_prefix('-') { 53 | Some(s) => (true, s), 54 | None => (false, s), 55 | }; 56 | 57 | let base = if let Some(stripped) = s.strip_prefix("0x") { 58 | s = stripped; 59 | 16 60 | } else if let Some(stripped) = s.strip_prefix("0b") { 61 | s = stripped; 62 | 2 63 | } else { 64 | if !s.chars().all(|c| c.is_ascii_digit()) { 65 | return Ok(None); 66 | } 67 | 10 68 | }; 69 | 70 | let mut num = BigInt::from_str_radix(s, base)?; 71 | if neg { 72 | num = -num; 73 | } 74 | 75 | Ok(Some(SingleParsedNumber { base, num })) 76 | } 77 | } 78 | 79 | struct SingleParsedNumber { 80 | base: u32, 81 | num: BigInt, 82 | } 83 | 84 | pub(crate) fn reverse_utf8_string_inplace(s: &mut str) { 85 | unsafe { 86 | let v = s.as_bytes_mut(); 87 | 88 | // Reverse the bytes within each grapheme cluster. 89 | // This does not preserve UTF-8 validity. 90 | { 91 | // Invariant: `tail` points to data we have not modified yet, so it is always valid UTF-8. 92 | let mut tail = &mut v[..]; 93 | while let Some(len) = std::str::from_utf8_unchecked(tail) 94 | .graphemes(true) 95 | .next() 96 | .map(str::len) 97 | { 98 | let (grapheme, new_tail) = tail.split_at_mut(len); 99 | grapheme.reverse(); 100 | tail = new_tail; 101 | } 102 | } 103 | 104 | // Reverse all bytes. This restores multi-byte sequences to their original order. 105 | v.reverse(); 106 | 107 | // The string is now valid UTF-8 again. 108 | debug_assert!(std::str::from_utf8(v).is_ok()); 109 | } 110 | } 111 | 112 | #[inline] 113 | pub(crate) fn encode_base64>(data: T) -> String { 114 | use base64::Engine; 115 | fn encode_base64_impl(data: &[u8]) -> String { 116 | base64::engine::general_purpose::STANDARD.encode(data) 117 | } 118 | encode_base64_impl(data.as_ref()) 119 | } 120 | 121 | #[inline] 122 | pub(crate) fn decode_base64>(data: T) -> Result, base64::DecodeError> { 123 | use base64::Engine; 124 | fn decode_base64_impl(data: &[u8]) -> std::result::Result, base64::DecodeError> { 125 | base64::engine::general_purpose::STANDARD.decode(data) 126 | } 127 | decode_base64_impl(data.as_ref()) 128 | } 129 | 130 | #[inline] 131 | pub(crate) fn encode_base64_url>(data: T) -> String { 132 | use base64::Engine; 133 | fn encode_base64_impl(data: &[u8]) -> String { 134 | base64::engine::general_purpose::URL_SAFE.encode(data) 135 | } 136 | encode_base64_impl(data.as_ref()) 137 | } 138 | 139 | #[inline] 140 | pub(crate) fn decode_base64_url>(data: T) -> Result, base64::DecodeError> { 141 | use base64::Engine; 142 | fn decode_base64_impl(data: &[u8]) -> std::result::Result, base64::DecodeError> { 143 | base64::engine::general_purpose::URL_SAFE.decode(data) 144 | } 145 | decode_base64_impl(data.as_ref()) 146 | } 147 | 148 | pub trait DisplaySliceExt<'s> { 149 | fn display_slice_tree<'a: 's>(&'a self, limit: usize) -> DisplayCellSlice<'a, 's>; 150 | 151 | fn display_slice_data<'a: 's>(&'a self) -> DisplaySliceData<'a, 's>; 152 | } 153 | 154 | impl<'s> DisplaySliceExt<'s> for CellSlice<'s> { 155 | fn display_slice_tree<'a: 's>(&'a self, limit: usize) -> DisplayCellSlice<'a, 's> { 156 | DisplayCellSlice { slice: self, limit } 157 | } 158 | 159 | fn display_slice_data<'a: 's>(&'a self) -> DisplaySliceData<'a, 's> { 160 | DisplaySliceData(self) 161 | } 162 | } 163 | 164 | pub struct DisplayCellSlice<'a, 'b> { 165 | slice: &'a CellSlice<'b>, 166 | limit: usize, 167 | } 168 | 169 | impl std::fmt::Display for DisplayCellSlice<'_, '_> { 170 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 171 | let mut stack = vec![(0, *self.slice)]; 172 | 173 | let mut i = 0; 174 | while let Some((indent, cs)) = stack.pop() { 175 | i += 1; 176 | if i > self.limit { 177 | return f.write_str("\n"); 178 | } 179 | 180 | writeln!(f, "{:indent$}{}", "", DisplaySliceData(&cs))?; 181 | 182 | for cell in cs.references().rev() { 183 | let cs = cell.as_slice_allow_exotic(); 184 | stack.push((indent + 1, cs)); 185 | } 186 | } 187 | 188 | Ok(()) 189 | } 190 | } 191 | 192 | pub struct DisplaySliceData<'a, 'b>(&'a CellSlice<'b>); 193 | 194 | impl std::fmt::Display for DisplaySliceData<'_, '_> { 195 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 196 | let mut cs = *self.0; 197 | 198 | if cs.cell_type().is_exotic() { 199 | f.write_str("SPECIAL ")?; 200 | } 201 | 202 | let mut buffer: [u8; 128] = [0; 128]; 203 | 204 | let bits = cs.size_bits(); 205 | cs.load_raw(&mut buffer, bits) 206 | .map_err(|_| std::fmt::Error)?; 207 | append_tag(&mut buffer, bits); 208 | 209 | let mut result = hex::encode(&buffer[..(bits as usize).div_ceil(8)]); 210 | if (1..=4).contains(&(bits % 8)) { 211 | result.pop(); 212 | } 213 | if bits % 4 != 0 { 214 | result.push('_'); 215 | } 216 | 217 | write!(f, "x{{{}}}", result) 218 | } 219 | } 220 | 221 | fn append_tag(data: &mut [u8; 128], bit_len: u16) { 222 | debug_assert!(bit_len < 1024); 223 | 224 | let rem = bit_len % 8; 225 | let last_byte = (bit_len / 8) as usize; 226 | if rem > 0 { 227 | let last_byte = &mut data[last_byte]; 228 | 229 | let tag_mask: u8 = 1 << (7 - rem); 230 | let data_mask = !(tag_mask - 1); 231 | 232 | *last_byte = (*last_byte & data_mask) | tag_mask; 233 | } 234 | } 235 | 236 | pub fn decode_hex_bitstring(s: &str) -> Result { 237 | fn hex_char(c: u8) -> Result { 238 | match c { 239 | b'A'..=b'F' => Ok(c - b'A' + 10), 240 | b'a'..=b'f' => Ok(c - b'a' + 10), 241 | b'0'..=b'9' => Ok(c - b'0'), 242 | _ => anyhow::bail!("Unexpected char `{c}` in hex bistring"), 243 | } 244 | } 245 | 246 | anyhow::ensure!(s.is_ascii(), "Non-ascii characters in bitstring"); 247 | 248 | let s = s.as_bytes(); 249 | let (mut s, with_tag) = match s.strip_suffix(b"_") { 250 | Some(s) => (s, true), 251 | None => (s, false), 252 | }; 253 | 254 | let mut half_byte = None; 255 | if s.len() % 2 != 0 256 | && let Some((last, prefix)) = s.split_last() 257 | { 258 | half_byte = Some(hex_char(*last)?); 259 | s = prefix; 260 | } 261 | 262 | anyhow::ensure!(s.len() <= 128 * 2, "Bitstring is too long"); 263 | 264 | let mut builder = CellBuilder::new(); 265 | 266 | let mut bytes = hex::decode(s)?; 267 | 268 | let mut bits = bytes.len() as u16 * 8; 269 | if let Some(half_byte) = half_byte { 270 | bits += 4; 271 | bytes.push(half_byte << 4); 272 | } 273 | 274 | if with_tag { 275 | bits = bytes.len() as u16 * 8; 276 | for byte in bytes.iter().rev() { 277 | if *byte == 0 { 278 | bits -= 8; 279 | } else { 280 | bits -= 1 + byte.trailing_zeros() as u16; 281 | break; 282 | } 283 | } 284 | } 285 | 286 | builder.store_raw(&bytes, bits)?; 287 | Ok(builder) 288 | } 289 | 290 | pub fn decode_binary_bitstring(s: &str) -> Result { 291 | let mut bits = 0; 292 | let mut buffer = [0; 128]; 293 | 294 | for char in s.chars() { 295 | let value = match char { 296 | '0' => 0u8, 297 | '1' => 1, 298 | c => anyhow::bail!("Unexpected char `{c}` in binary bitstring"), 299 | }; 300 | buffer[bits / 8] |= value << (7 - bits % 8); 301 | 302 | bits += 1; 303 | anyhow::ensure!(bits <= MAX_BIT_LEN as usize, "Bitstring is too long"); 304 | } 305 | 306 | let mut builder = CellBuilder::new(); 307 | builder.store_raw(&buffer, bits as u16)?; 308 | Ok(builder) 309 | } 310 | -------------------------------------------------------------------------------- /libs/src/Stack.fif: -------------------------------------------------------------------------------- 1 | library Stack // advanced stack manipulation library 2 | "Lists.fif" include 3 | // S(a b c - a c 2 a b) would compile to code performing the requested stack manipulation 4 | 5 | // interface to low-level stack manipulation primitives 6 | { (number) 1- abort"index expected" dup 0 < over 255 > or 7 | abort"index 0..255 expected" 8 | } : (idx) 9 | // push(n) : a0 .. an - a0 .. an a0 equivalent to "n pick" 10 | // push(0) = dup, push(1) = over 11 | { 0 char ) word (idx) } ::_ push( 12 | // pop(n) : a0 a1 .. a(n-1) an - an a1 .. a(n-1) 13 | // pop(0) = drop, pop(1) = nip 14 | { 0 char ) word (idx) } ::_ pop( 15 | // xchg(i,j) : equivalent to "i j exch2" 16 | { 0 char , word (idx) char ) word (idx) } ::_ xchg( 17 | // xchg0(i) : equivalent to "i exch" or "xchg(0,i)" 18 | // xchg0(1) = swap 19 | { 0 char ) word (idx) 0 } ::_ xchg0( 20 | forget (idx) 21 | 22 | // parser for stack notation expressions 23 | ")" 34 hold +" -" constant stk-delims 24 | anon constant stk-start 25 | anon constant stk-to 26 | variable stk-mode 27 | { stk-delims 11 (word) } : stk-token 28 | 'nop : mk-lit 29 | // stk-start vn ... v0 -- stk-start ... v0 i where v[i]=v0 30 | { 0 { 31 | 1+ 2dup 2+ pick dup stk-start eq? { 2drop drop 0 true } { eqv? } cond 32 | } until 33 | } : stk-lookup 34 | // stk-start a1 .. an stk-to b1 .. bm -- [a1 .. an] [b1 .. bm] 35 | { stk-mode @ 0= abort"identifier expected" } : chk-lit 36 | { stk-to list-until-marker stk-mode ! 37 | stk-start list-until-marker stk-mode @ 38 | stk-mode 0! 39 | } : build-stk-effect 40 | { stk-start stk-mode 0! { 41 | stk-token dup ")" $= { drop true } { 42 | dup "-" $= { 43 | drop stk-mode @ abort"duplicate -" true stk-mode ! stk-to false } { 44 | dup 34 chr $= { chk-lit drop char " word mk-lit false } { 45 | dup (number) ?dup { chk-lit 1- { swap mk-lit -rot } if mk-lit nip false } { 46 | atom dup `_ eq? { stk-mode @ abort"identifier expected" false } { 47 | stk-lookup 0= stk-mode @ = { 48 | stk-mode @ { atom>$ +" -?" } { atom>$ +" redefined" } cond abort } { 49 | false 50 | } cond } cond } cond } cond } cond } cond } until 51 | stk-mode @ 0= abort"'-' expected" 52 | build-stk-effect 53 | } :_ parse-stk-list( 54 | 55 | // stack operation list construction 56 | variable op-rlist 57 | { op-rlist null! } : clear-op-list 58 | { op-rlist @ list-reverse } : get-op-list 59 | { op-rlist @ cons op-rlist ! } : issue-op 60 | { minmax `xchg -rot triple } : op-xchg 61 | { `push swap pair } : op-push 62 | { `lit swap pair } : op-lit 63 | { `pop swap pair } : op-pop 64 | 0 op-pop constant op-drop 65 | { 2dup <> { op-xchg issue-op } if } : issue-xchg 66 | { op-push issue-op } : issue-push 67 | { op-lit issue-op } : issue-lit 68 | { op-pop issue-op } : issue-pop 69 | { op-drop issue-op } : issue-drop 70 | { ' issue-drop swap times } : issue-drop-# 71 | 72 | // emulated stack contents 73 | variable emul-stk 74 | { emul-stk @ count } : emul-depth 75 | { emul-depth 1- swap - } : adj-i 76 | { emul-depth 1- tuck swap - swap rot - swap } : adj-ij 77 | // i j -- 78 | { adj-ij 2dup emul-stk @ tuck swap [] swap rot [] rot // i sj si j 79 | emul-stk @ -rot []= swap rot []= emul-stk ! 80 | } : emul-xchg 81 | { emul-stk @ tpop drop emul-stk ! } : emul-drop 82 | // i -- 83 | { 0 emul-xchg emul-drop } : emul-pop 84 | // i -- s[i] 85 | { emul-stk @ swap [] } : emul-stk[] 86 | // i -- si 87 | { adj-i emul-stk[] } : emul-get 88 | { 0 emul-get } : emul-tos 89 | // v i -- ? Check whether s[i]=v 90 | { dup emul-depth < { emul-stk[] eqv? } { 2drop false } cond } : emul[]-eq? 91 | // v -- i or -1 Returns maximum i with s[i]=v 92 | { emul-stk @ dup count { // v s i 93 | ?dup 0= { -1 true } { 1- 2dup [] 3 pick eqv? } cond // v s i' ? 94 | } until nip nip 95 | } : emul-stk-lookup-rev 96 | // i -- 97 | { emul-get emul-stk @ swap , emul-stk ! } : emul-push 98 | { emul-stk @ swap , emul-stk ! } : emul-lit 99 | // show emulated stack contents similarly to .s 100 | { emul-stk @ explode dup 1 reverse ' .l swap times cr } : .e 101 | 102 | // both issue an operation and emulate it 103 | { 2dup issue-xchg emul-xchg } : issue-emul-xchg 104 | { dup issue-push emul-push } : issue-emul-push 105 | { dup issue-lit emul-lit } : issue-emul-lit 106 | { dup issue-pop emul-pop } : issue-emul-pop 107 | { issue-drop emul-drop } : issue-emul-drop 108 | { ' issue-emul-drop swap times } : issue-emul-drop-# 109 | 110 | // b.. s -- b.. s moves tos value to stk[s] 111 | { dup emul-stk[] 2 pick cdr list-member-eqv? { 112 | dup adj-i 0 issue-emul-xchg } { dup adj-i issue-emul-pop } cond 113 | } : move-tos-to 114 | 115 | // new s -- ops registered 116 | { { over null? not } { 117 | // .sl .e get-op-list .l cr 118 | // get-op-list list-length 100 > abort"too long" 119 | emul-depth over > 120 | { over emul-tos swap list-member-eqv? not } { false } cond { 121 | // b.. s tos unneeded 122 | issue-emul-drop } { 123 | over car // b.. s b1 124 | 2dup swap emul[]-eq? { drop swap cdr swap 1+ } { 125 | dup emul-stk-lookup-rev // b.. s b1 i 126 | dup 0< { // b.. s b1 i not found, must be a literal 127 | drop dup atom? abort"unavailable value" 128 | issue-emul-lit } { 129 | dup 3 pick < { // b.. s b1 i found in bottom s stack values 130 | nip adj-i issue-emul-push // b.. s 131 | dup emul-depth 1- < { move-tos-to } if 132 | } { 133 | emul-depth 1- over = { // b.. s b1 i found in tos 134 | 2drop move-tos-to 135 | } { // b.. s b1 i 136 | nip over adj-ij issue-emul-xchg 137 | } cond } cond } cond } cond } cond } while 138 | nip emul-depth swap - issue-emul-drop-# 139 | } : generate-reorder-ops 140 | 141 | // old new -- op-list 142 | { emul-stk @ op-rlist @ 2swap 143 | swap list>tuple emul-stk ! clear-op-list 144 | 0 generate-reorder-ops get-op-list 145 | -rot op-rlist ! emul-stk ! 146 | } : generate-reorder 147 | { parse-stk-list( generate-reorder } :_ SG( 148 | 149 | // op-list rewriting according to a ruleset 150 | // l f l1 l2 -- l' -1 or l f with l' = l2 + (l - l1) 151 | { push(3) rot list- { list+ nip nip true } { drop } cond 152 | } : try-rule 153 | // l f ll -- l' -1 or l f 154 | { { dup null? not } { uncons 3 -roll unpair try-rule rot } while drop 155 | } : try-ruleset 156 | // l ll -- l' 157 | { swap { over false swap try-ruleset 0= } until nip 158 | } : try-ruleset* 159 | // l ruleset -- l' 160 | recursive try-ruleset*-everywhere { 161 | tuck try-ruleset* dup null? { nip } { 162 | uncons rot try-ruleset*-everywhere cons } cond 163 | } swap ! 164 | LIST( 165 | [([xchg 0 1] [xchg 0 2]) ([rot])] 166 | [([xchg 0 1] [xchg 1 2]) ([-rot])] 167 | [([xchg 0 2] [xchg 1 2]) ([rot])] 168 | [([xchg 0 2] [xchg 0 1]) ([-rot])] 169 | [([xchg 1 2] [xchg 0 1]) ([rot])] 170 | [([xchg 1 2] [xchg 0 2]) ([-rot])] 171 | [([xchg 0 1] [rot]) ([xchg 0 2])] 172 | [([-rot] [xchg 0 1]) ([xchg 0 2])] 173 | [([xchg 0 2] [xchg 1 3]) ([2swap])] 174 | [([xchg 1 3] [xchg 0 2]) ([2swap])] 175 | [([push 1] [push 1]) ([2dup])] 176 | [([push 3] [push 3]) ([2over])] 177 | [([pop 0] [pop 0]) ([2drop])] 178 | [([pop 1] [pop 0]) ([2drop])] 179 | [([xchg 0 1] [push 1]) ([tuck])] 180 | [([rot] [-rot]) ()] 181 | [([-rot] [rot]) ()] 182 | ) constant fift-stack-ruleset 183 | { fift-stack-ruleset try-ruleset*-everywhere } : fift-ops-rewrite 184 | { SG( fift-ops-rewrite } :_ SGF( 185 | 186 | // helpers for creating Fift source strings for one fift-op 187 | // i j -- s 188 | { minmax over { "xchg(" rot (.) $+ +"," swap (.) $+ +")" } 189 | { nip dup 1 = { drop "swap" } { 190 | ?dup { "xchg0(" swap (.) $+ +")" } { "" } cond 191 | } cond } cond 192 | } : source- 193 | // i -- s 194 | { dup 1 = { drop "over" } { 195 | ?dup { "push(" swap (.) $+ +")" } { "dup" } cond 196 | } cond 197 | } : source- 198 | // i -- s 199 | { dup 1 = { drop "nip" } { 200 | ?dup { "pop(" swap (.) $+ +")" } { "drop" } cond 201 | } cond 202 | } : source- 203 | // lit -- s 204 | { dup string? { char " chr swap $+ char " hold } { (.) } cond 205 | } : source- 206 | 207 | // dictionary with all fift op compilation/source creation 208 | { 0 swap (compile) } : fop-compile 209 | ( _( `xchg 2 { fop-compile } { source- swap cons } ) 210 | _( `push 1 { fop-compile } { source- swap cons } ) 211 | _( `pop 1 { fop-compile } { source- swap cons } ) 212 | _( `lit 1 { 1 'nop (compile) } { source- swap cons } ) 213 | _( `rot 0 { ' rot fop-compile } { "rot" swap cons } ) 214 | _( `-rot 0 { ' -rot fop-compile } { "-rot" swap cons } ) 215 | _( `tuck 0 { ' tuck fop-compile } { "tuck" swap cons } ) 216 | _( `2swap 0 { ' 2swap fop-compile } { "2swap" swap cons } ) 217 | _( `2drop 0 { ' 2drop fop-compile } { "2drop" swap cons } ) 218 | _( `2dup 0 { ' 2dup fop-compile } { "2dup" swap cons } ) 219 | _( `2over 0 { ' 2over fop-compile } { "2over" swap cons } ) 220 | ) box constant fift-op-dict 221 | 222 | { dup atom? { atom>$ } { drop "" } cond 223 | "unknown operation " swap $+ abort 224 | } : report-unknown-op 225 | variable 'fop-entry-exec 226 | // process fift-op according to 'fop-entry-exec 227 | // ... op - ... 228 | { dup first dup fift-op-dict @ assq { report-unknown-op } ifnot 229 | dup second 1+ push(3) count <> abort"incorrect param count" 230 | nip swap explode dup roll drop 1- roll // o2 .. on entry 231 | 'fop-entry-exec @ execute 232 | } : process-fift-op 233 | 234 | // compile op-list into Fift wordlist 235 | // wl op-list -- wl' 236 | { { third execute } 'fop-entry-exec ! 237 | swap ' process-fift-op foldl } : compile-fift-op* 238 | // op-list -- e 239 | { fift-ops-rewrite ({) swap compile-fift-op* (}) } : ops>wdef 240 | 241 | // S( - ) compiles a "word" performing required action 242 | { SG( ops>wdef 0 swap } ::_ S( 243 | // 1 2 3 S(a b c - c a b a) .s would print 3 1 2 1 244 | 245 | // transform op-list into Fift source 246 | // ls op -- ls' 247 | { fift-ops-rewrite 248 | { 3 [] execute } 'fop-entry-exec ! 249 | null ' process-fift-op foldl 250 | dup null? { drop "" } { { +" " swap $+ } foldr-ne } cond 251 | } : ops>$ 252 | { SG( ops>$ 1 'nop } ::_ $S( 253 | { SG( ops>$ type } :_ .$S( 254 | // $S(a b c - b c a c a c) => string "rot 2dup over" 255 | // S(a b c - b c a c a c) => compile/execute block { rot 2dup over } 256 | // $S(_ x y _ - y x) => string "drop pop(2)" 257 | // .$S(x1 x2 - 17 x1) => print string "drop 17 swap" 258 | 259 | // simplify/transform sequences of stack manipulation operations 260 | LIST(. [a b c d e f g h i j]) constant std-stack 261 | { stk-start std-stack explode drop stk-to std-stack explode drop 262 | } : simplify<{ 263 | { build-stk-effect generate-reorder ops>$ } : }>stack 264 | // simplify<{ drop drop over over -13 }>stack => string "2drop 2dup -13" 265 | // simplify<{ 17 rot }>stack => string "swap 17 swap" 266 | // simplify<{ 5 1 reverse }>stack => string "xchg(1,5) xchg(2,4)" 267 | -------------------------------------------------------------------------------- /src/core/lexer.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | 3 | use super::env::SourceBlock; 4 | use crate::error::UnexpectedEof; 5 | 6 | #[derive(Default)] 7 | pub struct Lexer { 8 | blocks: Vec, 9 | } 10 | 11 | impl Lexer { 12 | pub fn push_source_block(&mut self, block: SourceBlock) { 13 | self.blocks.push(SourceBlockState::from(block)); 14 | } 15 | 16 | pub fn pop_source_block(&mut self) -> bool { 17 | self.blocks.pop().is_some() 18 | } 19 | 20 | pub fn reset_until_base(&mut self) { 21 | self.blocks.truncate(1); 22 | } 23 | 24 | pub fn get_position(&self) -> Option> { 25 | let offset = self.blocks.len(); 26 | let input = self.blocks.last()?; 27 | Some(LexerPosition { 28 | offset, 29 | source_block_name: input.block.name(), 30 | line: &input.line, 31 | word_start: input.prev_word_start, 32 | word_end: input.prev_word_end, 33 | line_number: input.line_number, 34 | }) 35 | } 36 | 37 | pub fn depth(&self) -> i32 { 38 | (self.blocks.len() as i32) - 1 39 | } 40 | 41 | pub fn scan_word(&mut self) -> Result> { 42 | let Some(input) = self.blocks.last_mut() else { 43 | return Ok(None); 44 | }; 45 | input.scan_word() 46 | } 47 | 48 | pub fn scan_until_space_or_eof(&mut self) -> Result<&str> { 49 | if let Some(input) = self.blocks.last_mut() 50 | && let Some(word) = input.scan_word()? 51 | { 52 | return Ok(word); 53 | } 54 | Ok("") 55 | } 56 | 57 | pub fn scan_until_delimiter(&mut self, delimiter: char) -> Result<&str> { 58 | self.use_last_block()?.scan_until(delimiter) 59 | } 60 | 61 | pub fn scan_classify(&mut self, delims: &str, space_class: u8) -> Result<&str> { 62 | let Some(input) = self.blocks.last_mut() else { 63 | return Ok(""); 64 | }; 65 | let classifier = AsciiCharClassifier::with_delims(delims, space_class)?; 66 | input.scan_classify(&classifier) 67 | } 68 | 69 | pub fn rewind(&mut self, last_word_len: usize) { 70 | if let Some(input) = self.blocks.last_mut() { 71 | input.rewind(last_word_len) 72 | } 73 | } 74 | 75 | pub fn scan_skip_whitespace(&mut self) -> Result { 76 | if let Some(input) = self.blocks.last_mut() { 77 | input.scan_skip_whitespace() 78 | } else { 79 | Ok(false) 80 | } 81 | } 82 | 83 | pub fn skip_line_whitespace(&mut self) { 84 | if let Some(input) = self.blocks.last_mut() { 85 | input.skip_line_whitespace(); 86 | } 87 | } 88 | 89 | pub fn skip_symbol(&mut self) { 90 | if let Some(input) = self.blocks.last_mut() { 91 | input.skip_symbol(); 92 | } 93 | } 94 | 95 | fn use_last_block(&mut self) -> Result<&mut SourceBlockState> { 96 | self.blocks.last_mut().ok_or_else(|| UnexpectedEof.into()) 97 | } 98 | } 99 | 100 | #[derive(Debug, Clone, Copy)] 101 | pub struct LexerPosition<'a> { 102 | pub offset: usize, 103 | pub source_block_name: &'a str, 104 | pub line: &'a str, 105 | pub word_start: usize, 106 | pub word_end: usize, 107 | pub line_number: usize, 108 | } 109 | 110 | pub trait Delimiter { 111 | fn delim(&mut self, c: char) -> bool; 112 | } 113 | 114 | impl bool> Delimiter for T { 115 | fn delim(&mut self, c: char) -> bool { 116 | (self)(c) 117 | } 118 | } 119 | 120 | impl Delimiter for char { 121 | #[inline] 122 | fn delim(&mut self, c: char) -> bool { 123 | *self == c 124 | } 125 | } 126 | 127 | struct SourceBlockState { 128 | block: SourceBlock, 129 | line: String, 130 | require_next_line: bool, 131 | line_offset: usize, 132 | prev_word_start: usize, 133 | prev_word_end: usize, 134 | line_number: usize, 135 | } 136 | 137 | impl From for SourceBlockState { 138 | fn from(block: SourceBlock) -> Self { 139 | Self { 140 | block, 141 | line: Default::default(), 142 | require_next_line: true, 143 | line_offset: 0, 144 | prev_word_start: 0, 145 | prev_word_end: 0, 146 | line_number: 0, 147 | } 148 | } 149 | } 150 | 151 | impl SourceBlockState { 152 | fn scan_word(&mut self) -> Result> { 153 | loop { 154 | if !self.scan_skip_whitespace()? { 155 | return Ok(None); 156 | } 157 | 158 | let start = self.line_offset; 159 | self.prev_word_start = start; 160 | 161 | self.skip_until(char::is_whitespace); 162 | let end = self.line_offset; 163 | self.prev_word_end = end; 164 | 165 | self.skip_line_whitespace(); 166 | 167 | if start != end { 168 | return Ok(Some(&self.line[start..end])); 169 | } 170 | } 171 | } 172 | 173 | fn scan_until(&mut self, c: char) -> Result<&str> { 174 | if self.require_next_line { 175 | self.read_line()?; 176 | } 177 | 178 | let start = self.line_offset; 179 | self.prev_word_start = start; 180 | 181 | let mut found = false; 182 | self.skip_until(|x| { 183 | found |= x == c; 184 | found 185 | }); 186 | 187 | let end = self.line_offset; 188 | self.prev_word_end = self.line_offset; 189 | 190 | anyhow::ensure!(found || c as u32 == 0, "End delimiter `{c}` not found"); 191 | 192 | if found { 193 | self.skip_symbol(); 194 | } else { 195 | self.require_next_line = true; 196 | } 197 | 198 | Ok(&self.line[start..end]) 199 | } 200 | 201 | fn scan_classify(&mut self, classifier: &AsciiCharClassifier) -> Result<&str> { 202 | self.scan_skip_whitespace()?; 203 | 204 | let start = self.line_offset; 205 | self.prev_word_start = start; 206 | 207 | let mut skip = false; 208 | let mut empty = true; 209 | self.skip_until(|c| { 210 | if c == '\n' || c == '\r' { 211 | return true; 212 | } 213 | 214 | let class = classifier.classify(c); 215 | if class & 0b01 != 0 && !empty { 216 | return true; 217 | } else if class & 0b10 != 0 { 218 | skip = true; 219 | return true; 220 | } 221 | 222 | empty = false; 223 | false 224 | }); 225 | 226 | if skip { 227 | self.skip_symbol(); 228 | } 229 | 230 | self.prev_word_end = self.line_offset; 231 | 232 | Ok(&self.line[start..self.line_offset]) 233 | } 234 | 235 | fn rewind(&mut self, last_word_len: usize) { 236 | self.line_offset = self.prev_word_start + last_word_len; 237 | self.prev_word_end = self.line_offset; 238 | } 239 | 240 | fn scan_skip_whitespace(&mut self) -> Result { 241 | loop { 242 | self.skip_line_whitespace(); 243 | 244 | if self.line_offset < self.line.len() { 245 | return Ok(true); 246 | } 247 | 248 | if (self.line.is_empty() || self.line_offset >= self.line.len()) && !self.read_line()? { 249 | return Ok(false); 250 | } 251 | } 252 | } 253 | 254 | fn skip_line_whitespace(&mut self) { 255 | self.skip_while(char::is_whitespace) 256 | } 257 | 258 | fn skip_until(&mut self, mut p: P) { 259 | self.skip_while(|c| !p.delim(c)); 260 | } 261 | 262 | fn skip_symbol(&mut self) { 263 | let mut first = true; 264 | self.skip_while(|_| std::mem::take(&mut first)) 265 | } 266 | 267 | fn skip_while(&mut self, mut p: P) { 268 | let prev_offset = self.line_offset; 269 | for (offset, c) in self.line[self.line_offset..].char_indices() { 270 | if !p.delim(c) { 271 | self.line_offset = prev_offset + offset; 272 | return; 273 | } 274 | } 275 | self.line_offset = self.line.len(); 276 | } 277 | 278 | fn read_line(&mut self) -> Result { 279 | const SKIP_PREFIX: &str = "#!"; 280 | 281 | self.require_next_line = false; 282 | self.prev_word_start = 0; 283 | self.prev_word_end = 0; 284 | self.line_offset = 0; 285 | self.line_number += 1; 286 | self.line.clear(); 287 | let not_eof = self.block.buffer_mut().read_line(&mut self.line)? > 0; 288 | if not_eof && self.line_number == 1 && self.line.starts_with(SKIP_PREFIX) { 289 | self.read_line() 290 | } else { 291 | Ok(not_eof) 292 | } 293 | } 294 | } 295 | 296 | struct AsciiCharClassifier { 297 | /// A native representation of `[u2; 256]` 298 | data: [u8; 64], 299 | } 300 | 301 | impl AsciiCharClassifier { 302 | fn with_delims(delims: &str, space_class: u8) -> Result { 303 | anyhow::ensure!( 304 | delims.is_ascii(), 305 | "Non-ascii symbols are not supported by character classifier" 306 | ); 307 | 308 | let mut data = [0u8; 64]; 309 | let mut set_char_class = |c: u8, mut class: u8| { 310 | // Ensure that class is in range 0..=3 311 | class &= 0b11; 312 | 313 | let offset = (c & 0b11) * 2; 314 | 315 | // Each byte stores classes (0..=3) for 4 characters. 316 | // 0: 00 00 00 11 317 | // 1: 00 00 11 00 318 | // 2: 00 11 00 00 319 | // 3: 11 00 00 00 320 | let mask = 0b11 << offset; 321 | class <<= offset; 322 | 323 | // Find a byte for the character 324 | let p = &mut data[(c >> 2) as usize]; 325 | // Set character class whithin this byte 326 | *p = (*p & !mask) | class; 327 | }; 328 | 329 | set_char_class(b' ', space_class); 330 | set_char_class(b'\t', space_class); 331 | 332 | let mut class = 0b11u8; 333 | for &c in delims.as_bytes() { 334 | if c == b' ' { 335 | class = class.checked_sub(1).context("Too many classes")?; 336 | } else { 337 | set_char_class(c, class); 338 | } 339 | } 340 | 341 | Ok(Self { data }) 342 | } 343 | 344 | fn classify(&self, c: char) -> u8 { 345 | if c.is_ascii() { 346 | let c = c as u8; 347 | let offset = (c & 0b11) * 2; 348 | (self.data[(c >> 2) as usize] >> offset) & 0b11 349 | } else { 350 | 0 351 | } 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/modules/arithmetic.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use num_bigint::{BigInt, Sign}; 3 | use num_integer::Integer; 4 | use num_traits::{One, Signed, Zero}; 5 | use tycho_types::prelude::*; 6 | use tycho_vm::SafeRc; 7 | 8 | use crate::core::*; 9 | 10 | pub struct Arithmetic; 11 | 12 | #[fift_module] 13 | impl Arithmetic { 14 | #[init] 15 | fn init(&self, d: &mut Dictionary) -> Result<()> { 16 | let mut make_int_lit = |name: &str, value: i32| { 17 | d.define_word(name, SafeRc::new(cont::IntLitCont::from(value))) 18 | }; 19 | 20 | make_int_lit("false ", 0)?; 21 | make_int_lit("true ", -1)?; 22 | make_int_lit("0 ", 0)?; 23 | make_int_lit("1 ", 1)?; 24 | make_int_lit("2 ", 2)?; 25 | make_int_lit("-1 ", -1)?; 26 | make_int_lit("bl ", 32) 27 | } 28 | 29 | // === Basic === 30 | 31 | #[cmd(name = "+", stack)] 32 | fn interpret_plus(stack: &mut Stack) -> Result<()> { 33 | let y = stack.pop_int()?; 34 | let mut x = stack.pop_int()?; 35 | *SafeRc::make_mut(&mut x) += y.as_ref(); 36 | stack.push_raw(x.into_dyn_fift_value()) 37 | } 38 | 39 | #[cmd(name = "-", stack)] 40 | fn interpret_minus(stack: &mut Stack) -> Result<()> { 41 | let y = stack.pop_int()?; 42 | let mut x = stack.pop_int()?; 43 | *SafeRc::make_mut(&mut x) -= y.as_ref(); 44 | stack.push_raw(x.into_dyn_fift_value()) 45 | } 46 | 47 | #[cmd(name = "1+", stack, args(rhs = 1))] 48 | #[cmd(name = "1-", stack, args(rhs = -1))] 49 | #[cmd(name = "2+", stack, args(rhs = 2))] 50 | #[cmd(name = "2-", stack, args(rhs = -2))] 51 | fn interpret_plus_const(stack: &mut Stack, rhs: i32) -> Result<()> { 52 | let mut x = stack.pop_int()?; 53 | *SafeRc::make_mut(&mut x) += rhs; 54 | stack.push_raw(x.into_dyn_fift_value()) 55 | } 56 | 57 | #[cmd(name = "negate", stack)] 58 | fn interpret_negate(stack: &mut Stack) -> Result<()> { 59 | let mut x = stack.pop_int()?; 60 | { 61 | let x = SafeRc::make_mut(&mut x); 62 | *x = -std::mem::take(x); 63 | } 64 | stack.push_raw(x.into_dyn_fift_value()) 65 | } 66 | 67 | #[cmd(name = "*", stack)] 68 | fn interpret_mul(stack: &mut Stack) -> Result<()> { 69 | let y = stack.pop_int()?; 70 | let mut x = stack.pop_int()?; 71 | *SafeRc::make_mut(&mut x) *= y.as_ref(); 72 | stack.push_raw(x.into_dyn_fift_value()) 73 | } 74 | 75 | #[cmd(name = "/", stack, args(r = Rounding::Floor))] 76 | #[cmd(name = "/r", stack, args(r = Rounding::Nearest))] 77 | #[cmd(name = "/c", stack, args(r = Rounding::Ceil))] 78 | fn interpret_div(stack: &mut Stack, r: Rounding) -> Result<()> { 79 | let y = stack.pop_int()?; 80 | let x = stack.pop_int()?; 81 | stack.push(divmod(&x, &y, r)?.0) 82 | } 83 | 84 | #[cmd(name = "mod", stack, args(r = Rounding::Floor))] 85 | #[cmd(name = "rmod", stack, args(r = Rounding::Nearest))] 86 | #[cmd(name = "cmod", stack, args(r = Rounding::Ceil))] 87 | fn interpret_mod(stack: &mut Stack, r: Rounding) -> Result<()> { 88 | let y = stack.pop_int()?; 89 | let x = stack.pop_int()?; 90 | stack.push(divmod(&x, &y, r)?.1) 91 | } 92 | 93 | #[cmd(name = "/mod", stack, args(r = Rounding::Floor))] 94 | #[cmd(name = "/rmod", stack, args(r = Rounding::Nearest))] 95 | #[cmd(name = "/cmod", stack, args(r = Rounding::Ceil))] 96 | fn interpret_divmod(stack: &mut Stack, r: Rounding) -> Result<()> { 97 | let y = stack.pop_int()?; 98 | let x = stack.pop_int()?; 99 | let (q, r) = divmod(&x, &y, r)?; 100 | stack.push(q)?; 101 | stack.push(r) 102 | } 103 | 104 | #[cmd(name = "*/", stack, args(r = Rounding::Floor))] 105 | #[cmd(name = "*/r", stack, args(r = Rounding::Nearest))] 106 | #[cmd(name = "*/c", stack, args(r = Rounding::Ceil))] 107 | fn interpret_times_div(stack: &mut Stack, r: Rounding) -> Result<()> { 108 | let z = stack.pop_int()?; 109 | let y = stack.pop_int()?; 110 | let mut x = stack.pop_int()?; 111 | *SafeRc::make_mut(&mut x) *= y.as_ref(); 112 | stack.push(divmod(&x, &z, r)?.0) 113 | } 114 | 115 | #[cmd(name = "*/mod", stack, args(r = Rounding::Floor))] 116 | #[cmd(name = "*/rmod", stack, args(r = Rounding::Nearest))] 117 | #[cmd(name = "*/cmod", stack, args(r = Rounding::Ceil))] 118 | fn interpret_times_divmod(stack: &mut Stack, r: Rounding) -> Result<()> { 119 | let z = stack.pop_int()?; 120 | let y = stack.pop_int()?; 121 | let mut x = stack.pop_int()?; 122 | *SafeRc::make_mut(&mut x) *= y.as_ref(); 123 | let (q, r) = divmod(&x, &z, r)?; 124 | stack.push(q)?; 125 | stack.push(r) 126 | } 127 | 128 | #[cmd(name = "*mod", stack, args(r = Rounding::Floor))] 129 | fn interpret_times_mod(stack: &mut Stack, r: Rounding) -> Result<()> { 130 | let z = stack.pop_int()?; 131 | let y = stack.pop_int()?; 132 | let mut x = stack.pop_int()?; 133 | *SafeRc::make_mut(&mut x) *= y.as_ref(); 134 | stack.push(divmod(&x, &z, r)?.1) 135 | } 136 | 137 | #[cmd(name = "1<<", stack, args(negate = false, minus_one = false))] 138 | #[cmd(name = "-1<<", stack, args(negate = true, minus_one = false))] 139 | #[cmd(name = "1<<1-", stack, args(negate = false, minus_one = true))] 140 | fn interpret_pow2(stack: &mut Stack, negate: bool, minus_one: bool) -> Result<()> { 141 | let x = stack.pop_smallint_range(0, 255 + (negate || minus_one) as u32)? as u16; 142 | let mut res = BigInt::one(); 143 | res <<= x; 144 | if minus_one { 145 | res -= 1; 146 | } 147 | if negate { 148 | res = -res; 149 | } 150 | stack.push(res) 151 | } 152 | 153 | #[cmd(name = "%1<<", stack)] 154 | fn interpret_mod_pow2(stack: &mut Stack) -> Result<()> { 155 | let y = stack.pop_smallint_range(0, 256)? as u16; 156 | let mut x = stack.pop_int()?; 157 | let mut mask = BigInt::one(); 158 | mask <<= y; 159 | mask -= 1; 160 | *SafeRc::make_mut(&mut x) &= mask; 161 | stack.push_raw(x.into_dyn_fift_value()) 162 | } 163 | 164 | #[cmd(name = "<<", stack)] 165 | fn interpret_lshift(stack: &mut Stack) -> Result<()> { 166 | let y = stack.pop_smallint_range(0, 256)? as u16; 167 | let mut x = stack.pop_int()?; 168 | *SafeRc::make_mut(&mut x) <<= y; 169 | stack.push_raw(x.into_dyn_fift_value()) 170 | } 171 | 172 | #[cmd(name = ">>", stack, args(r = Rounding::Floor))] 173 | #[cmd(name = ">>r", stack, args(r = Rounding::Nearest))] 174 | #[cmd(name = ">>c", stack, args(r = Rounding::Ceil))] 175 | fn interpret_rshift(stack: &mut Stack, r: Rounding) -> Result<()> { 176 | let y = stack.pop_smallint_range(0, 256)? as u16; 177 | let mut x = stack.pop_int()?; 178 | match r { 179 | Rounding::Floor => *SafeRc::make_mut(&mut x) >>= y, 180 | // TODO 181 | _ => anyhow::bail!("Unimplemented"), 182 | } 183 | stack.push_raw(x.into_dyn_fift_value()) 184 | } 185 | 186 | #[cmd(name = "2*", stack, args(y = 1))] 187 | fn interpret_lshift_const(stack: &mut Stack, y: u8) -> Result<()> { 188 | let mut x = stack.pop_int()?; 189 | *SafeRc::make_mut(&mut x) <<= y; 190 | stack.push_raw(x.into_dyn_fift_value()) 191 | } 192 | 193 | #[cmd(name = "2/", stack, args(y = 1))] 194 | fn interpret_rshift_const(stack: &mut Stack, y: u8) -> Result<()> { 195 | let mut x = stack.pop_int()?; 196 | *SafeRc::make_mut(&mut x) >>= y; 197 | stack.push_raw(x.into_dyn_fift_value()) 198 | } 199 | 200 | #[cmd(name = "< Result<()> { 204 | let z = stack.pop_smallint_range(0, 256)?; 205 | let y = stack.pop_int()?; 206 | let mut x = stack.pop_int()?; 207 | *SafeRc::make_mut(&mut x) <<= z; 208 | stack.push(divmod(&x, &y, r)?.0) 209 | } 210 | 211 | // TODO: mul shift, shift div 212 | 213 | // === Logical === 214 | 215 | #[cmd(name = "not", stack)] 216 | fn interpret_not(stack: &mut Stack) -> Result<()> { 217 | let mut x = stack.pop_int()?; 218 | { 219 | let lhs = SafeRc::make_mut(&mut x); 220 | *lhs = !std::mem::take(lhs); 221 | } 222 | stack.push_raw(x.into_dyn_fift_value()) 223 | } 224 | 225 | #[cmd(name = "and", stack)] 226 | fn interpret_and(stack: &mut Stack) -> Result<()> { 227 | let y = stack.pop_int()?; 228 | let mut x = stack.pop_int()?; 229 | *SafeRc::make_mut(&mut x) &= y.as_ref(); 230 | stack.push_raw(x.into_dyn_fift_value()) 231 | } 232 | 233 | #[cmd(name = "or", stack)] 234 | fn interpret_or(stack: &mut Stack) -> Result<()> { 235 | let y = stack.pop_int()?; 236 | let mut x = stack.pop_int()?; 237 | *SafeRc::make_mut(&mut x) |= y.as_ref(); 238 | stack.push_raw(x.into_dyn_fift_value()) 239 | } 240 | 241 | #[cmd(name = "xor", stack)] 242 | fn interpret_xor(stack: &mut Stack) -> Result<()> { 243 | let y = stack.pop_int()?; 244 | let mut x = stack.pop_int()?; 245 | *SafeRc::make_mut(&mut x) ^= y.as_ref(); 246 | stack.push_raw(x.into_dyn_fift_value()) 247 | } 248 | 249 | // === Integer comparison === 250 | 251 | #[cmd(name = "cmp", stack, args(map = [-1, 0, 1]))] 252 | #[cmd(name = "=", stack, args(map = [0, -1, 0]))] 253 | #[cmd(name = "<>", stack, args(map = [-1, 0, -1]))] 254 | #[cmd(name = "<=", stack, args(map = [-1, -1, 0]))] 255 | #[cmd(name = ">=", stack, args(map = [0, -1, -1]))] 256 | #[cmd(name = "<", stack, args(map = [-1, 0, 0]))] 257 | #[cmd(name = ">", stack, args(map = [0, 0, -1]))] 258 | fn interpret_cmp(stack: &mut Stack, map: [i8; 3]) -> Result<()> { 259 | let y = stack.pop_int()?; 260 | let x = stack.pop_int()?; 261 | let map_index = x.cmp(&y) as i8 + 1; 262 | stack.push_int(map[map_index as usize]) 263 | } 264 | 265 | #[cmd(name = "sgn", stack, args(map = [-1, 0, 1]))] 266 | #[cmd(name = "0=", stack, args(map = [0, -1, 0]))] 267 | #[cmd(name = "0<>", stack, args(map = [-1, 0, -1]))] 268 | #[cmd(name = "0<=", stack, args(map = [-1, -1, 0]))] 269 | #[cmd(name = "0>=", stack, args(map = [0, -1, -1]))] 270 | #[cmd(name = "0<", stack, args(map = [-1, 0, 0]))] 271 | #[cmd(name = "0>", stack, args(map = [0, 0, -1]))] 272 | fn interpret_sgn(stack: &mut Stack, map: [i8; 3]) -> Result<()> { 273 | let x = stack.pop_int()?; 274 | let map_index = match x.sign() { 275 | Sign::Minus => 0, 276 | Sign::NoSign => 1, 277 | Sign::Plus => 2, 278 | }; 279 | stack.push_int(map[map_index as usize]) 280 | } 281 | 282 | #[cmd(name = "fits", stack, args(signed = true))] 283 | #[cmd(name = "ufits", stack, args(signed = false))] 284 | fn interpret_fits(stack: &mut Stack, signed: bool) -> Result<()> { 285 | let y = stack.pop_smallint_range(0, 1023)? as u16; 286 | let x = stack.pop_int()?; 287 | let bits = x.bitsize(signed); 288 | stack.push_bool(bits <= y) 289 | } 290 | } 291 | 292 | enum Rounding { 293 | Floor, 294 | Nearest, 295 | Ceil, 296 | } 297 | 298 | // Math code from: 299 | // https://github.com/tonlabs/ever-vm/blob/master/src/stack/integer/math.rs 300 | 301 | #[inline] 302 | fn divmod(lhs: &BigInt, rhs: &BigInt, rounding: Rounding) -> Result<(BigInt, BigInt)> { 303 | anyhow::ensure!(!rhs.is_zero(), "Division by zero"); 304 | Ok(match rounding { 305 | Rounding::Floor => lhs.div_mod_floor(rhs), 306 | Rounding::Nearest => { 307 | let (mut q, mut r) = lhs.div_rem(rhs); 308 | round_nearest(&mut q, &mut r, lhs, rhs); 309 | (q, r) 310 | } 311 | Rounding::Ceil => { 312 | let (mut q, mut r) = lhs.div_rem(rhs); 313 | round_ceil(&mut q, &mut r, lhs, rhs); 314 | (q, r) 315 | } 316 | }) 317 | } 318 | 319 | #[inline] 320 | fn round_ceil(q: &mut BigInt, r: &mut BigInt, lhs: &BigInt, rhs: &BigInt) { 321 | if r.is_zero() || r.sign() != rhs.sign() { 322 | return; 323 | } 324 | *r -= rhs; 325 | if lhs.sign() == rhs.sign() { 326 | *q += 1; 327 | } else { 328 | *q -= 1; 329 | } 330 | } 331 | 332 | #[inline] 333 | fn round_nearest(q: &mut BigInt, r: &mut BigInt, lhs: &BigInt, rhs: &BigInt) { 334 | if r.is_zero() { 335 | return; 336 | } 337 | // 5 / 2 -> 2, 1 -> 3, -1 338 | // -5 / 2 -> -2, -1 -> -2, -1 339 | // 5 /-2 -> -2, 1 -> -2, 1 340 | // -5 /-2 -> 2, -1 -> 3, 1 341 | let r_x2: BigInt = r.clone() << 1; 342 | let cmp_result = r_x2.abs().cmp(&rhs.abs()); 343 | let is_not_negative = lhs.sign() == rhs.sign(); 344 | if cmp_result == std::cmp::Ordering::Greater 345 | || (cmp_result == std::cmp::Ordering::Equal && is_not_negative) 346 | { 347 | if rhs.sign() == r.sign() { 348 | *r -= rhs; 349 | } else { 350 | *r += rhs; 351 | } 352 | if is_not_negative { 353 | *q += 1; 354 | } else { 355 | *q -= 1; 356 | } 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /libs/src/Lisp.fif: -------------------------------------------------------------------------------- 1 | library Lisp // tiny Lisp (or rather Scheme) interpreter 2 | "Lists.fif" include 3 | variable lisp-dict 4 | { hole dup 1 { @ execute } does create } : recursive 5 | { atom>$ +" undefined" abort } : report-not-found 6 | // a l -- d -1 or a 0 Look up definition d of atom a in dictionary l 7 | { { dup null? { drop false true } 8 | { uncons -rot unpair -rot over eq? 9 | { drop nip true true } { nip swap false } cond 10 | } cond 11 | } until 12 | } : lookup-in 13 | // a dict -- def 14 | { lookup-in ' report-not-found ifnot } : lookup-or-fail 15 | { lisp-dict @ lookup-or-fail } : lisp-dict-lookup 16 | // a d -- Defines a with definition d in dictionary lisp-dict 17 | { pair lisp-dict @ cons lisp-dict ! } : lisp-dict-int-define 18 | { box lisp-dict-int-define } : lisp-dict-define 19 | // a d -- Defines new a with defininition d 20 | { over lisp-dict @ lookup-in { 2drop atom>$ +" already defined" abort } 21 | { drop lisp-dict-int-define } cond 22 | } : lisp-dict-int-define-new 23 | { box lisp-dict-int-define-new } : lisp-dict-define-new 24 | // a e -- Defines a with executable definition given by e 25 | { single lisp-dict-define-new } : lisp-dict-define-exec 26 | // expr ctx def -- val 27 | { dup first execute } : run-definition 28 | // expr ctx -- val 29 | recursive lisp-ctx-eval { 30 | over tuple? 31 | { over first over lisp-ctx-eval run-definition } 32 | { over atom? { lookup-or-fail @ } { drop } cond } 33 | cond 34 | } swap ! 35 | // exp -- value 36 | { lisp-dict @ lisp-ctx-eval } : lisp-eval 37 | // (exprs) ctx -- (vals) 38 | recursive lisp-ctx-eval-list 39 | { over null? { drop } { 40 | swap uncons -rot over lisp-ctx-eval -rot lisp-ctx-eval-list cons 41 | } cond 42 | } swap ! 43 | // (exprs) ctx -- val 44 | { null rot { 45 | dup null? { drop nip true } { 46 | nip uncons swap 2 pick lisp-ctx-eval swap false 47 | } cond } until 48 | } : lisp-ctx-eval-list-last 49 | // l c -- (args) 50 | { swap uncons nip swap lisp-ctx-eval-list } : extract-eval-arg-list 51 | { drop uncons nip } : extract-arg-list 52 | // (x1 .. xn) e n -- x1 .. xn e 53 | { { swap uncons rot } swap times 54 | swap null? not abort"invalid number of arguments" 55 | } : unpack-list 56 | // l c n e -- v 57 | { swap 2swap extract-eval-arg-list // e n (args) 58 | -rot unpack-list execute 59 | } : eval-exec-fixed 60 | // l c n e -- v 61 | { 2 pick pair 62 | swap 2swap extract-arg-list // [e c] n (args) 63 | -rot unpack-list unpair swap execute 64 | } : exec-fixed 65 | // l c e -- v 66 | { -rot extract-eval-arg-list // e (args) 67 | swap execute 68 | } : eval-exec-list 69 | { -rot tuck extract-arg-list // e c (args) 70 | swap rot execute 71 | } : exec-list 72 | // e a n -- 73 | { rot 2 { // expr ctx def n e 74 | rot drop eval-exec-fixed } does 75 | lisp-dict-define-exec 76 | } : lisp-fixed-primitive 77 | { rot 2 { rot drop exec-fixed } does lisp-dict-define-exec 78 | } : lisp-fixed-lazy-primitive 79 | // e a -- 80 | { swap 1 { nip eval-exec-list } does lisp-dict-define-exec 81 | } : lisp-primitive 82 | { swap 1 { nip exec-list } does lisp-dict-define-exec 83 | } : lisp-lazy-primitive 84 | 85 | // Uncomment next line for Fift booleans 86 | // false constant #f true constant #t null constant no-answer 87 | // Uncomment next line for Scheme booleans 88 | `#f constant #f `#t constant #t #f constant no-answer 89 | { #f eq? } : lisp-false? 90 | { lisp-false? 0= } : lisp-true? 91 | { ' #t ' #f cond } : lisp-bool 92 | 93 | // temp for defining a lot of primitives 94 | { bl word atom lisp-primitive } : L: 95 | { bl word atom swap lisp-dict-define } : L=: 96 | { bl word atom swap lisp-fixed-primitive } : #L: 97 | { 0 #L: } : 0L: 98 | { 1 #L: } : 1L: 99 | { 2 #L: } : 2L: 100 | 101 | // basic primitives 102 | { sum-list } L: + 103 | { - } 2L: - 104 | { dup null? { drop 1 } { ' * foldl-ne } cond } L: * 105 | { / } 2L: / 106 | { mod } 2L: modulo 107 | { abs } 1L: abs 108 | { ' min foldl-ne } L: min 109 | { ' max foldl-ne } L: max 110 | { true ' and foldl } L: integer-and 111 | { false ' or foldl } L: integer-or 112 | { 0 ' xor foldl } L: integer-xor 113 | { not } 1L: integer-not 114 | { = lisp-bool } 2L: = 115 | { <> lisp-bool } 2L: <> 116 | { < lisp-bool } 2L: < 117 | { <= lisp-bool } 2L: <= 118 | { > lisp-bool } 2L: > 119 | { >= lisp-bool } 2L: >= 120 | { eq? lisp-bool } 2L: eq? 121 | { eqv? lisp-bool } 2L: eqv? 122 | { equal? lisp-bool } 2L: equal? 123 | { cons } 2L: cons 124 | { car } 1L: car 125 | { cdr } 1L: cdr 126 | { cadr } 1L: cadr 127 | { cddr } 1L: cddr 128 | { caddr } 1L: caddr 129 | { cdr cddr } 1L: cdddr 130 | { concat-list-lists } L: append 131 | { list-reverse } 1L: reverse 132 | { list-tail } 2L: list-tail 133 | { list-ref } 2L: list-ref 134 | { list-member-eq } 2L: memq 135 | { list-member-eqv } 2L: memv 136 | { list-member-equal } 2L: member 137 | { assq ' #f ifnot } 2L: assq 138 | { assv ' #f ifnot } 2L: assv 139 | { assoc ' #f ifnot } 2L: assoc 140 | { list? lisp-bool } 1L: list? 141 | { pair? lisp-bool } 1L: pair? 142 | { tuple? lisp-bool } 1L: tuple? 143 | { string? lisp-bool } 1L: string? 144 | { integer? lisp-bool } 1L: integer? 145 | { integer? lisp-bool } 1L: number? 146 | { count } 1L: width 147 | { list-length } 1L: length 148 | { [] } 2L: tuple-ref 149 | { first } 1L: first 150 | { second } 1L: second 151 | { third } 1L: third 152 | { 3 [] } 1L: fourth 153 | { list>tuple } 1L: list->tuple 154 | { explode list } 1L: tuple->list 155 | null L=: null 156 | { atom? lisp-bool } 1L: symbol? 157 | { atom } 1L: string->symbol 158 | { atom>$ } 1L: symbol->string 159 | { dup #f eq? swap #t eq? or lisp-bool } 1L: boolean? 160 | #t L=: else 161 | #f L=: #f 162 | #t L=: #t 163 | { null? lisp-bool } 1L: null? 164 | { 0= lisp-bool } 1L: zero? 165 | { 0> lisp-bool } 1L: positive? 166 | { 0< lisp-bool } 1L: negative? 167 | { 1 and 0= lisp-bool } 1L: even? 168 | { 1 and 0<> lisp-bool } 1L: odd? 169 | { bye } 0L: exit 170 | { .l null } 1L: write 171 | { lisp-eval } 1L: eval 172 | { drop } `quote 1 lisp-fixed-lazy-primitive 173 | 'nop L: list 174 | { list>tuple } L: tuple 175 | { list-last } L: begin 176 | { $len } 1L: string-length 177 | { concat-string-list } L: string-append 178 | { $= lisp-bool } 2L: string=? 179 | { $cmp 0< lisp-bool } 2L: string lisp-bool } 2L: string>? 182 | { $cmp 0>= lisp-bool } 2L: string>=? 183 | { (number) dup 1 = { drop } { ' 2drop if no-answer } cond 184 | } 1L: string->number 185 | { (.) } 1L: number->string 186 | { box? lisp-bool } 1L: box? 187 | { box } 1L: box 188 | { hole } 0L: new-box 189 | { @ } 1L: unbox 190 | { tuck swap ! } 2L: set-box! 191 | { abort } 1L: error 192 | { dup find { nip execute } { +" -?" abort } cond } : find-execute 193 | { explode-list 1- roll find-execute } L: fift-exec 194 | { explode-list dup 1- swap roll find-execute } L: fift-exec-cnt 195 | { uncons swap find-execute } L: fift-exec-list 196 | // end of basic primitives 197 | forget L: forget #L: forget L=: 198 | forget 0L: forget 1L: forget 2L: 199 | 200 | { { dup tuple? ' do-quote if } list-map } : map-quote 201 | { uncons ' cons foldr-ne map-quote 202 | null swap cons lisp-dict @ rot run-definition 203 | } `apply lisp-primitive // bad: should have preserved original context 204 | // e1 e2 e3 ctx 205 | { 3 exch 3 pick lisp-ctx-eval lisp-true? ' swap if nip swap lisp-ctx-eval } 206 | `if 3 lisp-fixed-lazy-primitive 207 | // (e) ctx 208 | { #t -rot 209 | { over null? { 2drop true } { 210 | swap uncons swap 2 pick lisp-ctx-eval dup lisp-true? // v' c t v ? 211 | { swap 2swap nip false } { -rot 2drop nip true } cond 212 | } cond } until 213 | } `and lisp-lazy-primitive 214 | { #f -rot 215 | { over null? { 2drop true } { 216 | swap uncons swap 2 pick lisp-ctx-eval dup lisp-false? // v' c t v ? 217 | { swap 2swap nip false } { -rot 2drop nip true } cond 218 | } cond } until 219 | } `or lisp-lazy-primitive 220 | { lisp-false? lisp-bool } `not 1 lisp-fixed-primitive 221 | // cond-clause ctx -- v -1 or 0 222 | { swap uncons -rot dup `else eq? { 223 | drop lisp-ctx-eval-list-last true } { 224 | over lisp-ctx-eval lisp-true? { 225 | lisp-ctx-eval-list-last true } { 226 | 2drop false 227 | } cond } cond 228 | } : eval-cond-clause 229 | // (clauses) ctx -- v 230 | { { over null? { no-answer true } { 231 | swap uncons -rot over eval-cond-clause } cond 232 | } until -rot 2drop 233 | } `cond lisp-lazy-primitive 234 | { lisp-dict @ lookup-in { hole tuck lisp-dict-int-define } ifnot 235 | } : lisp-create-global-var 236 | // a e ctx -- old (simple) define 237 | { drop over atom? not abort"only a variable can be define'd" 238 | over lisp-create-global-var swap lisp-eval swap ! 239 | } drop // `define 2 lisp-fixed-lazy-primitive 240 | { tuck lisp-ctx-eval rot dup atom? not abort"only a variable can be set" 241 | rot lookup-or-fail dup @ -rot ! 242 | } `set! 2 lisp-fixed-lazy-primitive 243 | // define lambda 244 | { { dup null? { drop true true } 245 | { uncons swap atom? { false } { drop false true } cond } cond 246 | } until 247 | } : var-list? 248 | { { dup null? over atom? or { drop true true } 249 | { uncons swap atom? { false } { drop false true } cond } cond 250 | } until 251 | } : lambda-var-list? 252 | // (quote x) -- x -1 ; else 0 253 | { dup pair? { uncons swap `quote eq? { car true } { drop false } cond } 254 | { drop false } cond 255 | } : is-quote? 256 | recursive match-arg-list-acc 257 | // l (vars) (args) -- ((var . arg) ...)+l -1 or ? 0 258 | { over atom? { over `_ eq? { 2drop } { pair swap cons } cond true } { 259 | over null? { nip null? } { // (vars) (args) 260 | over tuple? not { 2drop false } { 261 | over is-quote? { eq? nip } { // (v) (a) 262 | dup tuple? not { 2drop false } { 263 | over count over count over <> { drop 2drop false } { // l [v] [a] n 264 | 3 roll 0 rot { // [v] [a] l i 265 | dup 0< { 266 | 3 pick over [] swap // [v] [a] l vi i 267 | 3 pick over [] 2swap rot // [v] [a] i l vi ai 268 | match-arg-list-acc { // [v] [a] i l' 269 | swap 1+ } { nip -1 } cond 270 | } ifnot 271 | } swap times 272 | 2swap 2drop 0>= 273 | } cond } cond } cond } cond } cond } cond 274 | } swap ! 275 | { null -rot match-arg-list-acc } : match-arg-list 276 | // ((var . arg)...) ctx -- ctx' 277 | { { over null? not } 278 | { swap uncons swap unpair box pair rot cons } while 279 | nip 280 | } : extend-ctx-by-list 281 | // ((vars) body) ctx 282 | { swap uncons -rot 283 | dup lambda-var-list? not abort"invalid formal parameter list" 284 | { // l-expr ctx' [_ body ctx (vars)] 285 | -rot 2 pick 3 [] swap rot // [_ body ...] (vars) ctx' l-expr 286 | uncons nip swap lisp-ctx-eval-list // [_ body ...] (vars) (arg-vals) 287 | match-arg-list not abort"invalid arguments to lambda" // [_ body ...] ((var arg)...) 288 | over third extend-ctx-by-list // [_ body ctx (vars)] ctx'' 289 | swap second swap lisp-ctx-eval-list-last 290 | } 3 -roll 4 tuple 291 | } : make-lambda 292 | { make-lambda } `lambda lisp-lazy-primitive 293 | // (a e) ctx -- more sophisticated (define a e) 294 | { drop uncons swap dup atom? { // (e) a 295 | tuck lisp-create-global-var 296 | swap lisp-dict @ lisp-ctx-eval-list-last swap ! 297 | } { // (e) (a v..) 298 | uncons over atom? not abort"only variables can be define'd" // (e) a (v..) 299 | rot cons over lisp-create-global-var // a ((v..) (e)) h 300 | swap lisp-dict @ make-lambda swap ! 301 | } cond 302 | } `define lisp-lazy-primitive 303 | // ((x e) ..) ctx -- ((x.v) ..) 304 | recursive eval-assign-list 305 | { over null? { drop } { 306 | swap uncons swap uncons // ctx t x (e) 307 | over atom? not abort"invalid variable name in assignment list" 308 | 3 pick lisp-ctx-eval-list-last // ctx t x v 309 | pair swap rot eval-assign-list cons 310 | } cond 311 | } swap ! 312 | // (((x v) ..) body) ctx -- let construct 313 | { swap uncons swap 2 pick eval-assign-list // ctx body ((x v)...) 314 | rot extend-ctx-by-list lisp-ctx-eval-list-last 315 | } `let lisp-lazy-primitive 316 | // ((x e) ..) ctx -- ctx' 317 | { swap { 318 | dup null? { drop true } { 319 | uncons swap uncons // ctx t x (e) 320 | over atom? not abort"invalid variable name in assignment list" 321 | 3 pick lisp-ctx-eval-list-last // ctx t x v 322 | box pair rot cons swap false 323 | } cond } until 324 | } : compute-let*-ctx 325 | // (((x v) ..) body) ctx -- let* construct 326 | { swap uncons swap rot compute-let*-ctx lisp-ctx-eval-list-last 327 | } `let* lisp-lazy-primitive 328 | // ((x e) ..) ctx -- ((h e) ..) ctx' , with x bound to h in ctx' 329 | recursive prepare-letrec-ctx { 330 | over null? { 331 | swap uncons swap uncons swap // ctx t (e) x 332 | hole tuck pair swap rot cons // ctx t (x.h) (h e) 333 | 3 -roll rot cons prepare-letrec-ctx // (h e) t ctx' 334 | -rot cons swap 335 | } ifnot 336 | } swap ! 337 | // (((x v) ..) body) ctx -- letrec construct 338 | { swap uncons swap rot prepare-letrec-ctx swap { // body ctx' ((h e)..) 339 | dup null? { drop true } { 340 | uncons -rot uncons 2 pick lisp-ctx-eval-list-last // body t ctx' h v 341 | swap ! swap false 342 | } cond } until 343 | lisp-ctx-eval-list-last 344 | } `letrec lisp-lazy-primitive 345 | // (e (p e)...) ctx -- match construct 346 | { swap uncons swap 2 pick lisp-ctx-eval swap { // ctx v ((p e)..) 347 | dup null? { drop 2drop no-answer true } { 348 | uncons swap uncons swap 3 pick // ctx v t e p v 349 | match-arg-list { // ctx v t e ((x' . v')...) 350 | 2swap 2drop rot extend-ctx-by-list lisp-ctx-eval-list-last true } { 351 | 2drop false 352 | } cond } cond } until 353 | } `match lisp-lazy-primitive 354 | // 355 | lisp-dict @ constant original-lisp-dict 356 | { original-lisp-dict lisp-dict ! } : reset-lisp 357 | { ' drop { lisp-eval .l cr } List-generic( } :_ LISP-EVAL-PRINT( 358 | // LISP-EVAL-PRINT((+ 3 4) (* 5 6)) computes and prints 12 and 30 359 | { hole dup 1 { @ nip } does swap 360 | 1 { swap lisp-eval swap ! } does 361 | List-generic( 362 | } :_ LISP-EVAL( 363 | // LISP-EVAL((+ 3 4) (* 5 6)) computes 12 and 30, returns only 30 364 | 365 | // words for invoking Lisp definitions from Fift 366 | // (args) def -- val 367 | { null rot map-quote cons lisp-dict @ rot run-definition 368 | } : invoke-lisp-definition 369 | { atom lisp-dict-lookup 1 { @ invoke-lisp-definition } 370 | } : (invoke-lisp) 371 | { bl word (invoke-lisp) } :: invoke-lisp 372 | // ( 2 3 ) invoke-lisp compare .l 373 | { atom lisp-dict-lookup 2 { @ mklist-1 invoke-lisp-definition } 374 | } : (invoke-lisp-fixed) 375 | { bl word (invoke-lisp-fixed) } :: invoke-lisp-fixed 376 | // 9 8 2 invoke-lisp-fixed compare .l 377 | { bl word (invoke-lisp) does } : make-lisp-invoker 378 | { bl word (invoke-lisp-fixed) does } : make-lisp-fixed-invoker 379 | // 2 make-lisp-fixed-invoker compare : compare 380 | // 3 9 compare 381 | // import Lisp definitions as Fift words 382 | { bl word dup (invoke-lisp) does swap 0 (create) } : import-lisp 383 | { bl word tuck (invoke-lisp-fixed) does swap 0 (create) } : import-lisp-fixed 384 | // 1 import-lisp-fixed fact 385 | // 7 fact . 386 | -------------------------------------------------------------------------------- /libs/src/TonUtil.fif: -------------------------------------------------------------------------------- 1 | library TonUtil // TON Blockchain Fift Library 2 | "Lists.fif" include 3 | 4 | -1 constant Masterchain 5 | 0 constant Basechain 6 | 7 | // parse workchain id 8 | // ( S -- workchain ) 9 | { (number) 1- abort"workchain id must be an integer" 10 | dup 32 fits not abort"workchain id must fit in 32 bits" 11 | } : parse-workchain-id 12 | 13 | { (number) 1- abort"integer expected" } : parse-int 14 | 15 | { over null? ' swap if drop } : replace-if-null 16 | 17 | // Private key load/generate 18 | // ( fname -- pubkey privkey ) 19 | { dup ."Loading private key from file " type cr 20 | file>B dup Blen 32 <> abort"Private key must be exactly 32 bytes long" 21 | dup priv>pub swap 22 | } : load-keypair 23 | // ( fname -- pubkey privkey ) 24 | { dup file-exists? 25 | { load-keypair } 26 | { dup newkeypair swap rot over swap B>file 27 | rot ."Saved new private key to file " type cr 28 | } cond 29 | } : load-generate-keypair 30 | 31 | // Parse smart-contract address 32 | // ( S -- workchain addr bounce? ) 33 | { $>smca not abort"invalid smart-contract address" 34 | 1 and 0= 35 | } : parse-smc-addr 36 | 37 | // ( x -- ) Displays a 64-digit hex number 38 | { 64 0x. } : 64x. 39 | { 64 0X. } : 64X. 40 | // ( wc addr -- ) Show address in : form 41 | { swap ._ .":" 64x. } : .addr 42 | // ( wc addr flags -- ) Show address in base64url form 43 | { smca>$ type } : .Addr 44 | // ( wc addr fname -- ) Save address to file in 36-byte format 45 | { -rot 256 u>B swap 32 i>B B+ swap B>file } : save-address 46 | // ( wc addr fname -- ) Save address and print message 47 | { dup ."(Saving address to file " type .")" cr save-address 48 | } : save-address-verbose 49 | 50 | // ( fname -- wc addr ) Load address from file 51 | { file>B 32 B| 52 | dup Blen { 32 B>i@ } { drop Basechain } cond 53 | swap 256 B>u@ 54 | } : load-address 55 | // ( fname -- wc addr ) Load address from file and print message 56 | { dup ."(Loading address from file " type .")" cr load-address 57 | } : load-address-verbose 58 | // Parse string as address or load address from file (if string is prefixed by @) 59 | // ( S default-bounce -- workchain addr bounce? ) 60 | { over $len 0= abort"empty smart-contract address" 61 | swap dup 1 $| swap "@" $= 62 | { nip load-address rot } { drop nip parse-smc-addr } cond 63 | } : parse-load-address 64 | 65 | // ( hex-str -- addr ) Parses ADNL address 66 | { dup $len 64 <> abort"ADNL address must consist of exactly 64 hexadecimal characters" 67 | (hex-number) 1 <> abort"ADNL address must consist of 64 hexadecimal characters" 68 | dup 256 ufits not abort"invalid ADNL address" 69 | } : parse-adnl-address 70 | 71 | // ( b wc addr -- b' ) Serializes address into Builder b 72 | { -rot 8 i, swap 256 u, } : addr, 73 | { over 8 fits { rot b{100} s, -rot addr, } { 74 | rot b{110} s, 256 9 u, rot 32 i, swap 256 u, } cond 75 | } : Addr, 76 | 77 | // Gram utilities 78 | 1000000000 constant Gram 79 | { Gram swap */r } : Gram*/ 80 | { Gram * } : Gram* 81 | { (number) dup { 1- ' Gram*/ ' Gram* cond true } if 82 | } : $>GR? 83 | // ( S -- nanograms ) 84 | { $>GR? not abort"not a valid Gram amount" 85 | } : $>GR 86 | { bl word $>GR 1 'nop } ::_ GR$ 87 | // ( nanograms -- S ) 88 | { dup abs <# ' # 9 times char . hold #s rot sign #> 89 | nip -trailing0 } : (.GR) 90 | { (.GR) ."GR$" type } : .GR_ 91 | { .GR_ space } : .GR 92 | 93 | // b x -- b' ( serializes a Gram amount ) 94 | { -1 { 1+ 2dup 8 * ufits } until 95 | rot over 4 u, -rot 8 * u, } : Gram, 96 | // s -- x s' ( deserializes a Gram amount ) 97 | { 4 u@+ swap 8 * u@+ } : Gram@+ 98 | // s -- x 99 | { 4 u@+ swap 8 * u@ } : Gram@ 100 | 101 | // currency collections 102 | // b x --> b' ( serializes a VarUInteger32 ) 103 | { -1 { 1+ 2dup 8 * ufits } until 104 | rot over 5 u, -rot 8 * u, } : VarUInt32, 105 | // s --> x ( deserializes a VarUInteger32 ) 106 | { 5 u@+ swap 8 * u@ } : VarUInt32@ 107 | 32 constant cc-key-bits 108 | ' VarUInt32, : val, 109 | ' VarUInt32@ : val@ 110 | // d k v -- d' 111 | { cc 120 | { dup null? { ."(null)" drop } { val@ ._ } cond } dup : .maybeVarUInt32 : .val 121 | { swap cc-key-bits { rot { ."+" } if .val ."*$" ._ true true } idictforeach drop } : (.cc) 122 | { false (.cc) { ."0" } ifnot } : .cc_ 123 | { .cc_ space } : .cc 124 | { true (.cc) drop } : .+cc_ 125 | { .+cc_ space } : .+cc 126 | { cc-key-bits { rot . ."-> " swap .val .val ."; " true } dictdiff drop cr } : show-cc-diff 127 | { cc-key-bits { val@ swap val@ + val, true } dictmerge } : cc+ 128 | { null swap cc-key-bits { val@ pair swap cons true } idictforeach drop } : cc>list-rev 129 | { cc>list-rev list-reverse } : cc>list 130 | forget val, forget val@ forget .val 131 | 132 | // ( S -- x -1 or 0 ) 133 | { (number) dup 2 = { -rot 2drop } if 1 = } : int? 134 | { int? dup { drop dup 0< { drop false } { true } cond } if } : pos-int? 135 | // ( S -- k v -1 or 0 ) Parses expression * or *$ 136 | { dup "*" $pos dup 0< { 2drop false } { 137 | $| dup $len 2 < { 2drop false } { 138 | 1 $| nip dup 1 $| swap "$" $= { swap } if drop 139 | int? dup { over 32 fits { 2drop false } ifnot } if 140 | not { drop false } { 141 | swap pos-int? not { drop false } { 142 | true 143 | } cond } cond } cond } cond 144 | } : cc-key-value? 145 | // ( S -- D -1 or 0 ) Parses an extra currency collection 146 | // e.g. "10000*$3+7777*$-11" means "10000 units of currency #3 and 7777 units of currency #-11" 147 | { dictnew { // S D 148 | swap dup "+" $pos dup 0< { drop null -rot } { $| 1 $| nip -rot } cond 149 | cc-key-value? { +ccpair over null? dup { rot drop true } if } { 2drop false true } cond 150 | } until 151 | } : $>xcc? 152 | { $>xcc? not abort"invalid extra currency collection" } : $>xcc 153 | { char } word dup $len { $>xcc } { drop dictnew } cond 1 'nop } ::_ CX{ 154 | 155 | // complete currency collections 156 | { $>xcc? { true } { drop false } cond } : end-parse-cc 157 | // ( S -- x D -1 or 0 ) Parses a currency collection 158 | // e.g. "1.2+300*$2" means "1200000000ng plus 300 units of currency #2" 159 | { 0 swap dup "+" $pos dup 0< { drop dup 160 | $>GR? { nip nip dictnew true } { end-parse-cc } cond 161 | } { over swap $| swap $>GR? { 2swap 2drop swap 1 $| nip } { drop 162 | } cond end-parse-cc } cond 163 | } : $>cc? 164 | { $>cc? not abort"invalid currency collection" } : $>cc 165 | { char } word dup $len { $>cc } { drop 0 dictnew } cond 2 'nop } ::_ CC{ 166 | // ( x D -- ) 167 | { swap ?dup { .GR_ .+cc_ } { .cc_ } cond } : .GR+cc_ 168 | { .GR+cc_ space } : .GR+cc 169 | { -rot Gram, swap dict, } : Gram+cc, 170 | 171 | // Libraries 172 | // ( -- D ) New empty library collection 173 | ' dictnew : Libs{ 174 | // ( D -- D ) Return library collection as dictionary 175 | 'nop : }Libs 176 | // ( D c x -- D' ) Add a public/private library c to collection D 177 | { -rot B, swap ref, 189 | } cond 190 | } swap ! 191 | // b S n -- b' 192 | { swap $>B swap append-long-bytes } : append-long-string 193 | // S -- c 194 | { 195 | } : simple-transfer-body 196 | 197 | // ( S -- x ) parse public key 198 | { dup $len 48 <> abort"public key must be 48 characters long" 199 | base64url>B dup Blen 36 <> abort"public key must be 48 characters long" 200 | 34 B| 16 B>u@ over crc16 <> abort"crc16 mismatch in public key" 201 | 16 B>u@+ 0x3ee6 <> abort"invalid tag in public key" 202 | 256 B>u@ 203 | } : parse-pubkey 204 | { bl word parse-pubkey 1 'nop } ::_ PK' 205 | // ( x -- S ) serialize public key 206 | { 256 u>B B{3ee6} swap B+ dup crc16 16 u>B B+ B>base64 } : pubkey>$ 207 | { pubkey>$ type } : .pubkey 208 | 209 | // ( S -- x ) parse validator-encoded public key 210 | { base64>B dup Blen 36 <> abort"public key with magic must be 36 bytes long" 211 | 4 B| swap 32 B>u@ 0xC6B41348 <> abort"unknown magic for public key (not Ed25519)" 212 | } : parse-val-pubkey 213 | { bl word parse-val-pubkey 1 'nop } ::_ VPK' 214 | { char } word base64>B 1 'nop } ::_ B64{ 215 | 216 | // adnl address parser 217 | { 256 u>B B{2D} swap B+ dup crc16 16 u>B B+ } : adnl-preconv 218 | { swap 32 /mod dup 26 < { 65 } { 24 } cond + rot swap hold } : Base32# 219 | { <# ' Base32# 8 times #> } : Base32#*8 220 | { "" over Blen 5 / { swap 40 B>u@+ Base32#*8 nip rot swap $+ } swap times nip } : B>Base32 221 | 222 | // ( x -- S ) Converts an adnl-address from a 256-bit integer to a string 223 | { adnl-preconv B>Base32 1 $| nip } : adnl>$ 224 | 225 | { 65 - dup 0>= { -33 and dup 26 < } { 41 + dup 25 > over 32 < and } cond ?dup nip } : Base32-digit? 226 | { Base32-digit? not abort"not a Base32 digit" } : Base32-digit 227 | { 0 { over $len } { swap 1 $| -rot (char) Base32-digit swap 5 << + } while nip } : Base32-number 228 | { B{} { over $len } { swap 8 $| -rot Base32-number 40 u>B B+ } while nip } : Base32>B 229 | 230 | // ( S -- x ) Converts an adnl address from a string to 256-bit integer 231 | { dup $len 55 <> abort"not 55 alphanumeric characters" "F" swap $+ Base32>B 232 | 33 B| 16 B>u@ over crc16 <> abort"crc16 checksum mismatch" 233 | 8 B>u@+ 0x2D <> abort"not a valid adnl address" 256 B>u@ } : $>adnl 234 | 235 | { 65 - dup 0>= { -33 and 10 + dup 16 < } { 17 + dup 0>= over 10 < and } cond ?dup nip } : hex-digit? 236 | // ( S -- x -1 or 0 ) Parses a hexadecimal integer 237 | { dup $len { 238 | 0 { 239 | 4 << swap 1 $| -rot (char) hex-digit? // S a d -1 or S a 0 240 | { + over $len 0= } { drop -1 true } cond 241 | } until 242 | dup 0< { 2drop false } { nip true } cond 243 | } { drop false } cond 244 | } : hex$>u? 245 | // ( S -- x ) 246 | { hex$>u? not abort"not a hexadecimal number" } : hex$>u 247 | 248 | { dup $len 64 = { hex$>u } { 249 | dup $len 55 = { $>adnl } { 250 | true abort"invalid adnl address" 251 | } cond } cond 252 | } : parse-adnl-addr 253 | { adnl>$ type } : .adnl 254 | { bl word parse-adnl-addr 1 'nop } ::_ adnl: 255 | 256 | // ( x a b -- a<=x<=b ) 257 | { 2 pick >= -rot >= and } : in-range? 258 | 259 | // ( c i -- ? ) Checks whether c is a valid value for config param #i 260 | def? config-valid? { 261 | { nip 0>= { ."warning: cannot check validity of configuration parameter value, use create-state instead of fift to check validity" cr } if 262 | true } : config-valid? 263 | } ifnot 264 | 265 | { dup -1000 = { drop 278 | { 279 | // anycast_info$_ depth:(#<= 30) { depth >= 1 } 280 | // rewrite_pfx:(bits depth) = Anycast; 281 | 30 u@+ swap // get depth 282 | 283 | dup 1 > { 284 | dup 2 roll swap u@+ // get rewrite_pfx 285 | // return depth, rewrite_pfx, slice 286 | } 287 | { 288 | drop // drop depth (<=1) 289 | 0 0 2 roll // set anycast to none 290 | } cond 291 | } 292 | { 293 | 0 0 2 roll // set anycast to none 294 | } cond 295 | } : maybe-anycast 296 | 297 | // Rewrite first bits of addr with anycast info 298 | { // input: anycast depth, rewrite_pfx, workchain, slice, address length 299 | 4 -roll 300 | 3 roll dup dup 0 = { 2drop 2 roll drop } 301 | { 302 | rot swap u@+ swap drop 303 | 3 roll 304 | // Get addr: addr_none$00 / addr_extern$01 / addr_std$10 / addr_var$11 317 | { // if greater that zero 318 | dup 1 > 319 | { 320 | 2 = 321 | { 322 | // if addr_std$10 323 | // anycast:(Maybe Anycast) 324 | // workchain_id:int8 325 | // address:bits256 = MsgAddressInt; 326 | maybe-anycast // get anycast depth, bits, slice 327 | 8 i@+ // get workchain 328 | 256 parse-address-with-anycast 329 | `addr-std swap 330 | } 331 | 332 | { 333 | // if addr_var$11 334 | // anycast:(Maybe Anycast) 335 | // addr_len:(## 9) 336 | // workchain_id:int32 337 | // address:(bits addr_len) = MsgAddressInt; 338 | maybe-anycast // get anycast depth, bits, slice 339 | 9 u@+ // get addr_len 340 | 32 i@+ // get workchain 341 | swap 2 -roll // move workchain to neede position 342 | swap parse-address-with-anycast 343 | `addr-var swap 344 | } cond 345 | 346 | } 347 | { 348 | drop // drop header (dup for statment upper) 349 | // if addr_extern$01 350 | // addr_extern$01 len:(## 9) 351 | // external_address:(bits len) 352 | 9 u@+ swap // bit len 353 | u@+ // external_address 354 | `addr-extern swap 355 | } cond 356 | } 357 | { 358 | swap 359 | // if addr_none$00 360 | `addr-none swap 361 | } cond 362 | } : addr@+ 363 | 364 | { addr@+ drop } : addr@ 365 | 366 | // User-friendly prints output of addr@ 367 | // (0 A or addr A or wc addr A -- ) 368 | { 369 | dup `addr-none eq? 370 | { 2drop ."addr_none" } 371 | { 372 | `addr-extern eq? 373 | { (dump) type } 374 | { (x.) swap (dump) ":" $+ swap $+ type } 375 | cond 376 | } 377 | cond 378 | } : print-addr // print addr with workchain 379 | 380 | forget maybe-anycast 381 | forget parse-address-with-anycast 382 | -------------------------------------------------------------------------------- /src/modules/mod.rs: -------------------------------------------------------------------------------- 1 | use std::iter::Peekable; 2 | 3 | use anyhow::{Context as _, Result}; 4 | use tycho_vm::SafeRc; 5 | 6 | pub use self::arithmetic::Arithmetic; 7 | pub use self::cell_utils::CellUtils; 8 | pub use self::control::Control; 9 | pub use self::crypto::Crypto; 10 | pub use self::debug_utils::DebugUtils; 11 | pub use self::dict_utils::DictUtils; 12 | pub use self::stack_utils::StackUtils; 13 | pub use self::string_utils::StringUtils; 14 | pub use self::vm_utils::VmUtils; 15 | use crate::core::*; 16 | 17 | mod arithmetic; 18 | mod cell_utils; 19 | mod control; 20 | mod crypto; 21 | mod debug_utils; 22 | mod dict_utils; 23 | mod stack_utils; 24 | mod string_utils; 25 | mod vm_utils; 26 | 27 | pub struct BaseModule; 28 | 29 | #[fift_module] 30 | impl FiftModule for BaseModule { 31 | #[init] 32 | fn init(&self, d: &mut Dictionary) -> Result<()> { 33 | d.define_word("nop", cont::NopCont::instance()) 34 | } 35 | 36 | #[cmd(name = "null", stack)] 37 | fn interpret_push_null(stack: &mut Stack) -> Result<()> { 38 | stack.push_null() 39 | } 40 | 41 | #[cmd(name = "null?", stack, args(ty = StackValueType::Null))] 42 | #[cmd(name = "integer?", stack, args(ty = StackValueType::Int))] 43 | #[cmd(name = "string?", stack, args(ty = StackValueType::String))] 44 | #[cmd(name = "tuple?", stack, args(ty = StackValueType::Tuple))] 45 | #[cmd(name = "box?", stack, args(ty = StackValueType::SharedBox))] 46 | #[cmd(name = "atom?", stack, args(ty = StackValueType::Atom))] 47 | fn interpret_is_type(stack: &mut Stack, ty: StackValueType) -> Result<()> { 48 | let is_ty = stack.pop()?.ty() == ty; 49 | stack.push_bool(is_ty) 50 | } 51 | 52 | #[cmd(name = "hole", stack)] 53 | fn interpret_hole(stack: &mut Stack) -> Result<()> { 54 | stack.push(SharedBox::default()) 55 | } 56 | 57 | #[cmd(name = "box", stack)] 58 | fn interpret_box(stack: &mut Stack) -> Result<()> { 59 | let value = stack.pop()?; 60 | stack.push(SharedBox::new(value)) 61 | } 62 | 63 | #[cmd(name = "@", stack)] 64 | fn interpret_box_fetch(stack: &mut Stack) -> Result<()> { 65 | let value = stack.pop_shared_box()?; 66 | stack.push_raw(value.fetch()) 67 | } 68 | 69 | #[cmd(name = "!", stack)] 70 | fn interpret_box_store(stack: &mut Stack) -> Result<()> { 71 | let value = stack.pop_shared_box()?; 72 | value.store(stack.pop()?); 73 | Ok(()) 74 | } 75 | 76 | #[cmd(name = "anon", stack)] 77 | fn interpret_atom_anon(stack: &mut Stack) -> Result<()> { 78 | let anon = stack.atoms_mut().create_anon(); 79 | stack.push(anon) 80 | } 81 | 82 | #[cmd(name = "(atom)", stack)] 83 | fn interpret_atom(stack: &mut Stack) -> Result<()> { 84 | let create = stack.pop_bool()?; 85 | let name = stack.pop_string()?; 86 | let mut atom = stack.atoms().get(&*name); 87 | if create && atom.is_none() { 88 | atom = Some(stack.atoms_mut().create_named(&*name)); 89 | } 90 | let exists = atom.is_some(); 91 | if let Some(atom) = atom { 92 | stack.push(atom)?; 93 | } 94 | stack.push_bool(exists) 95 | } 96 | 97 | #[cmd(name = "atom>$", stack)] 98 | fn interpret_atom_name(stack: &mut Stack) -> Result<()> { 99 | let atom = stack.pop_atom()?; 100 | stack.push(atom.to_string()) 101 | } 102 | 103 | #[cmd(name = "eq?", stack)] 104 | fn interpret_is_eq(stack: &mut Stack) -> Result<()> { 105 | let y = stack.pop()?; 106 | let x = stack.pop()?; 107 | stack.push_bool(x.is_equal(&*y)) 108 | } 109 | 110 | #[cmd(name = "eqv?", stack)] 111 | fn interpret_is_eqv(stack: &mut Stack) -> Result<()> { 112 | let y = stack.pop()?; 113 | let x = stack.pop()?; 114 | let ty = x.ty(); 115 | 116 | stack.push_bool(if ty == y.ty() { 117 | match ty { 118 | StackValueType::Null => true, 119 | StackValueType::Atom => *x.as_atom()? == *y.as_atom()?, 120 | StackValueType::Int => *x.as_int()? == *y.as_int()?, 121 | StackValueType::String => x.as_string()? == y.as_string()?, 122 | _ => false, 123 | } 124 | } else { 125 | false 126 | }) 127 | } 128 | 129 | #[cmd(name = "|", stack)] 130 | fn interpret_empty_tuple(stack: &mut Stack) -> Result<()> { 131 | stack.push(StackTuple::new()) 132 | } 133 | 134 | #[cmd(name = ",", stack)] 135 | fn interpret_tuple_push(stack: &mut Stack) -> Result<()> { 136 | let value = stack.pop()?; 137 | let mut tuple = stack.pop_tuple()?; 138 | SafeRc::make_mut(&mut tuple).push(value); 139 | stack.push_raw(tuple.into_dyn_fift_value()) 140 | } 141 | 142 | #[cmd(name = "tpop", stack)] 143 | fn interpret_tuple_pop(stack: &mut Stack) -> Result<()> { 144 | let mut tuple = stack.pop_tuple()?; 145 | let last = SafeRc::make_mut(&mut tuple) 146 | .pop() 147 | .context("Tuple underflow")?; 148 | stack.push_raw(tuple.into_dyn_fift_value())?; 149 | stack.push_raw(last) 150 | } 151 | 152 | #[cmd(name = "[]popn", stack)] 153 | fn interpret_tuple_popn(stack: &mut Stack) -> Result<()> { 154 | let n = stack.pop_usize()?; 155 | let mut tuple = stack.pop_tuple()?; 156 | 157 | let moved: Vec<_> = { 158 | let tuple = SafeRc::make_mut(&mut tuple); 159 | (0..n) 160 | .map(|_| tuple.pop().context("Tuple underflow")) 161 | .collect::>()? 162 | }; 163 | 164 | stack.push_raw(tuple.into_dyn_fift_value())?; 165 | stack.extend_raw(moved.iter().rev().cloned()) 166 | } 167 | 168 | #[cmd(name = "[]", stack)] 169 | fn interpret_tuple_index(stack: &mut Stack) -> Result<()> { 170 | let idx = stack.pop_usize()?; 171 | let tuple = stack.pop_tuple()?; 172 | let value = tuple 173 | .get(idx) 174 | .with_context(|| format!("Index {idx} is out of the tuple range"))? 175 | .clone(); 176 | stack.push_raw(value) 177 | } 178 | 179 | #[cmd(name = "[]=", stack)] 180 | fn interpret_tuple_set(stack: &mut Stack) -> Result<()> { 181 | let idx = stack.pop_usize()?; 182 | let value = stack.pop()?; 183 | let mut tuple = stack.pop_tuple()?; 184 | *SafeRc::make_mut(&mut tuple) 185 | .get_mut(idx) 186 | .with_context(|| format!("Index {idx} is out of the tuple range"))? = value; 187 | stack.push_raw(tuple.into_dyn_fift_value()) 188 | } 189 | 190 | #[cmd(name = "[]!", stack)] // []! (t v i -- t') 191 | fn interpret_tuple_insert(stack: &mut Stack) -> Result<()> { 192 | let idx = stack.pop_usize()?; 193 | let value = stack.pop()?; 194 | let mut tuple = stack.pop_tuple()?; 195 | 196 | let l = tuple.len(); 197 | anyhow::ensure!( 198 | idx <= l, 199 | format!("insertion index (is {idx}) should be <= len (is {l})") 200 | ); 201 | 202 | SafeRc::make_mut(&mut tuple).insert(idx, value); 203 | stack.push_raw(tuple.into_dyn_fift_value()) 204 | } 205 | 206 | #[cmd(name = "[]>$", stack, args(pop_sep = false))] // []>$ (t[S0, S1, ..., Sn] -- S) 207 | #[cmd(name = "[]>$by", stack, args(pop_sep = true))] // []>$by (t[S0, S1, ..., Sn] s -- S) 208 | fn interpret_tuple_strings_join(stack: &mut Stack, pop_sep: bool) -> Result<()> { 209 | let sep = if pop_sep { 210 | Some(stack.pop_string()?) 211 | } else { 212 | None 213 | }; 214 | let tuple = stack.pop_tuple_owned()?; 215 | 216 | let mut result = String::new(); 217 | 218 | let mut first = true; 219 | for item in tuple { 220 | if let Some(sep) = sep.as_deref() 221 | && !std::mem::take(&mut first) 222 | { 223 | result.push_str(sep); 224 | } 225 | result.push_str(item.as_string()?); 226 | } 227 | 228 | stack.push(result) 229 | } 230 | 231 | #[cmd(name = "count", stack)] 232 | fn interpret_tuple_len(stack: &mut Stack) -> Result<()> { 233 | let len = stack.pop_tuple()?.len(); 234 | stack.push_int(len) 235 | } 236 | 237 | #[cmd(name = "tuple", stack)] 238 | fn interpret_make_tuple(stack: &mut Stack) -> Result<()> { 239 | let n = stack.pop_smallint_range(0, 255)? as usize; 240 | let mut tuple = Vec::with_capacity(n); 241 | for _ in 0..n { 242 | tuple.push(stack.pop()?); 243 | } 244 | tuple.reverse(); 245 | stack.push(tuple) 246 | } 247 | 248 | #[cmd(name = "untuple", stack, args(pop_count = true))] 249 | #[cmd(name = "explode", stack, args(pop_count = false))] 250 | fn interpret_tuple_explode(stack: &mut Stack, pop_count: bool) -> Result<()> { 251 | let mut n = if pop_count { 252 | stack.pop_smallint_range(0, 255)? as usize 253 | } else { 254 | 0 255 | }; 256 | let tuple = stack.pop_tuple_owned()?; 257 | if !pop_count { 258 | n = tuple.len(); 259 | anyhow::ensure!(n <= 255, "Cannot untuple a tuple with {n} items"); 260 | } else { 261 | anyhow::ensure!( 262 | tuple.len() == n, 263 | "Tuple size mismatch. Expected: {n}, actual: {}", 264 | tuple.len() 265 | ); 266 | } 267 | 268 | for item in tuple { 269 | stack.push_raw(item)?; 270 | } 271 | 272 | if !pop_count { 273 | stack.push_int(n)?; 274 | } 275 | 276 | Ok(()) 277 | } 278 | 279 | #[cmd(name = "allot", stack)] 280 | fn interpret_allot(stack: &mut Stack) -> Result<()> { 281 | let n = stack.pop_smallint_range(0, u32::MAX)?; 282 | let mut tuple = Vec::>::new(); 283 | tuple.resize_with(n as usize, || { 284 | SafeRc::new_dyn_fift_value(SharedBox::default()) 285 | }); 286 | stack.push(tuple) 287 | } 288 | 289 | // === Hashmaps === 290 | 291 | #[cmd(name = "hmapnew", stack)] 292 | fn interpret_hmap_new(stack: &mut Stack) -> Result<()> { 293 | stack.push_null() 294 | } 295 | 296 | #[cmd(name = "hmap@", stack, args(chk = false))] 297 | #[cmd(name = "hmap@?", stack, args(chk = true))] 298 | fn interpret_hmap_fetch(stack: &mut Stack, chk: bool) -> Result<()> { 299 | let map = stack.pop_hashmap()?; 300 | let key = HashMapTreeKey::new(stack.pop()?)?; 301 | let value = HashMapTreeNode::lookup(&map, key).map(|node| node.value.clone()); 302 | 303 | let found = value.is_some(); 304 | match value { 305 | Some(value) => stack.push_raw(value)?, 306 | None if !chk => stack.push_null()?, 307 | _ => {} 308 | } 309 | if chk { 310 | stack.push_bool(found)?; 311 | } 312 | Ok(()) 313 | } 314 | 315 | #[cmd(name = "hmap-", stack, args(chk = false, read = false))] 316 | #[cmd(name = "hmap-?", stack, args(chk = true, read = false))] 317 | #[cmd(name = "hmap@-", stack, args(chk = false, read = true))] 318 | fn interpret_hmap_delete(stack: &mut Stack, chk: bool, read: bool) -> Result<()> { 319 | let mut map = stack.pop_hashmap()?; 320 | let key = HashMapTreeKey::new(stack.pop()?)?; 321 | let value = HashMapTreeNode::remove(&mut map, key); 322 | stack.push_opt_raw(map)?; 323 | 324 | let exists = value.is_some(); 325 | match value { 326 | Some(value) if read => stack.push_raw(value)?, 327 | None if read && !chk => stack.push_null()?, 328 | _ => {} 329 | } 330 | if chk { 331 | stack.push_bool(exists)?; 332 | } 333 | Ok(()) 334 | } 335 | 336 | #[cmd(name = "hmap!", stack, args(add = false))] 337 | #[cmd(name = "hmap!+", stack, args(add = true))] 338 | fn interpret_hmap_store(stack: &mut Stack, add: bool) -> Result<()> { 339 | let mut map = stack.pop_hashmap()?; 340 | let key = HashMapTreeKey::new(stack.pop()?)?; 341 | let value = stack.pop()?; 342 | 343 | if add { 344 | HashMapTreeNode::set(&mut map, &key, &value); 345 | } else { 346 | HashMapTreeNode::replace(&mut map, key, &value); 347 | } 348 | stack.push_opt_raw(map) 349 | } 350 | 351 | #[cmd(name = "hmapempty?", stack)] 352 | fn interpret_hmap_is_empty(stack: &mut Stack) -> Result<()> { 353 | let map = stack.pop_hashmap()?; 354 | stack.push_bool(map.is_none()) 355 | } 356 | 357 | #[cmd(name = "hmapunpack", stack)] 358 | fn interpret_hmap_decompose(stack: &mut Stack) -> Result<()> { 359 | let map = stack.pop_hashmap()?; 360 | let not_empty = map.is_some(); 361 | 362 | if let Some(map) = map { 363 | stack.push_raw(map.key.stack_value.clone())?; 364 | stack.push_raw(map.value.clone())?; 365 | stack.push_opt_raw(map.left.clone())?; 366 | stack.push_opt_raw(map.right.clone())?; 367 | } 368 | 369 | stack.push_bool(not_empty) 370 | } 371 | 372 | #[cmd(name = "hmapforeach", tail)] 373 | fn interpret_hmap_foreach(ctx: &mut Context) -> Result> { 374 | let func = ctx.stack.pop_cont()?; 375 | let Some(map) = ctx.stack.pop_hashmap()? else { 376 | return Ok(None); 377 | }; 378 | Ok(Some(SafeRc::new_dyn_fift_cont(cont::LoopCont::new( 379 | HmapIterCont { 380 | iter: HashMapTreeNode::owned_iter(map).peekable(), 381 | ok: true, 382 | }, 383 | func, 384 | ctx.next.take(), 385 | )))) 386 | } 387 | 388 | // === Environment === 389 | 390 | #[cmd(name = "now")] 391 | fn interpret_now(ctx: &mut Context) -> Result<()> { 392 | ctx.stack.push_int(ctx.env.now_ms() / 1000) 393 | } 394 | 395 | #[cmd(name = "nowms")] 396 | fn interpret_now_ms(ctx: &mut Context) -> Result<()> { 397 | ctx.stack.push_int(ctx.env.now_ms()) 398 | } 399 | 400 | #[cmd(name = "getenv")] 401 | fn interpret_getenv(ctx: &mut Context) -> Result<()> { 402 | let name = ctx.stack.pop_string()?; 403 | let value = ctx.env.get_env(&name).unwrap_or_default(); 404 | ctx.stack.push(value) 405 | } 406 | 407 | #[cmd(name = "getenv?")] 408 | fn interpret_getenv_exists(ctx: &mut Context) -> Result<()> { 409 | let name = ctx.stack.pop_string()?; 410 | let exists = match ctx.env.get_env(&name) { 411 | Some(value) => { 412 | ctx.stack.push(value)?; 413 | true 414 | } 415 | None => false, 416 | }; 417 | ctx.stack.push_bool(exists) 418 | } 419 | 420 | #[cmd(name = "file>B")] 421 | fn interpret_read_file(ctx: &mut Context) -> Result<()> { 422 | let name = ctx.stack.pop_string()?; 423 | let data = ctx.env.read_file(name.as_str())?; 424 | ctx.stack.push(data) 425 | } 426 | 427 | #[cmd(name = "filepart>B")] 428 | fn interpret_read_file_part(ctx: &mut Context) -> Result<()> { 429 | let size = ctx.stack.pop_usize()? as u64; 430 | let offset = ctx.stack.pop_usize()? as u64; 431 | let name = ctx.stack.pop_string()?; 432 | let data = ctx.env.read_file_part(name.as_str(), offset, size)?; 433 | ctx.stack.push(data) 434 | } 435 | 436 | #[cmd(name = "B>file")] 437 | fn interpret_write_file(ctx: &mut Context) -> Result<()> { 438 | let name = ctx.stack.pop_string()?; 439 | let data = ctx.stack.pop_bytes()?; 440 | ctx.env.write_file(name.as_str(), data.as_slice())?; 441 | Ok(()) 442 | } 443 | 444 | #[cmd(name = "file-exists?")] 445 | fn interpret_file_exists(ctx: &mut Context) -> Result<()> { 446 | let name = ctx.stack.pop_string()?; 447 | let exists = ctx.env.file_exists(&name); 448 | ctx.stack.push_bool(exists) 449 | } 450 | } 451 | 452 | #[derive(Clone)] 453 | struct HmapIterCont { 454 | iter: Peekable, 455 | ok: bool, 456 | } 457 | 458 | impl cont::LoopContImpl for HmapIterCont { 459 | fn pre_exec(&mut self, ctx: &mut Context) -> Result { 460 | let entry = match self.iter.next() { 461 | Some(entry) => entry, 462 | None => return Ok(false), 463 | }; 464 | 465 | ctx.stack.push_raw(entry.key.stack_value.clone())?; 466 | ctx.stack.push_raw(entry.value.clone())?; 467 | Ok(true) 468 | } 469 | 470 | fn post_exec(&mut self, ctx: &mut Context) -> Result { 471 | self.ok = ctx.stack.pop_bool()?; 472 | Ok(self.ok && self.iter.peek().is_some()) 473 | } 474 | 475 | fn finalize(&mut self, ctx: &mut Context) -> Result { 476 | ctx.stack.push_bool(self.ok)?; 477 | Ok(true) 478 | } 479 | } 480 | --------------------------------------------------------------------------------