Compare commits

...

36 Commits

Author SHA1 Message Date
imxyy1soope1 dde3052e2d chore 2026-06-30 18:49:45 +08:00
imxyy1soope1 f0e3f1eeca fix-vm: use Machine trait exclusively 2026-06-19 22:37:29 +08:00
imxyy1soope1 afbc471e40 *.toml: reformat using tombi 2026-06-19 21:17:10 +08:00
imxyy1soope1 c1b4ac4d8f flake.lock: update 2026-06-13 23:24:08 +08:00
imxyy1soope1 81ac08fb5a refactor: reorganize crate hierarchy 2026-06-06 22:02:31 +08:00
imxyy1soope1 9412c319f9 implement any & all 2026-05-20 21:05:36 +08:00
imxyy1soope1 b420a950a3 chore: flake.nix 2026-05-20 21:04:41 +08:00
imxyy1soope1 4aa694aa3a STW 2026-05-17 17:55:37 +08:00
imxyy1soope1 d98e389606 implement string context 2026-05-17 17:04:32 +08:00
imxyy1soope1 9a17990d5e deep equal 2026-05-17 15:30:25 +08:00
imxyy1soope1 29fab93cd1 refactor: abstract VM 2026-05-16 19:51:38 +08:00
imxyy1soope1 21899f7380 implement Path type 2026-05-16 19:51:00 +08:00
imxyy1soope1 3d07f89afe implement foldl' 2026-05-08 21:09:54 +08:00
imxyy1soope1 cfd2df5d0e implement import & scopedImport (WIP, ResolvePath resolves to string) 2026-05-08 18:52:18 +08:00
imxyy1soope1 49392f66f8 fix: null dynamic attrs 2026-05-08 17:46:23 +08:00
imxyy1soope1 4aff27142c implement __functor 2026-05-08 17:46:23 +08:00
imxyy1soope1 62d65b2e5f refactor primops 2026-05-08 17:46:23 +08:00
imxyy1soope1 b3e6591809 refactor with 2026-05-08 17:46:23 +08:00
imxyy1soope1 fca00b04ba implement Assert 2026-05-08 17:46:23 +08:00
imxyy1soope1 88a205f419 document trying mechanism 2026-05-08 17:46:23 +08:00
imxyy1soope1 7401f1ba5e chore: update flake.lock 2026-05-08 17:46:23 +08:00
imxyy1soope1 9d10fa7da3 ConcatStrings 2026-05-08 17:46:23 +08:00
imxyy1soope1 1550868e90 implement dynamic key; implement __curPos; other small changes 2026-05-08 17:46:23 +08:00
imxyy1soope1 035ebd3808 implement |> and <| 2026-05-08 17:46:23 +08:00
imxyy1soope1 06e73fc9be avoid thunking trivial values 2026-05-08 17:46:23 +08:00
imxyy1soope1 47b1344ebe refactor: use GAT in enum Ir 2026-05-08 17:46:23 +08:00
imxyy1soope1 6659b22dce layer: usize -> u8 2026-05-08 17:46:22 +08:00
imxyy1soope1 045f0bd6de temp 2026-05-08 17:46:22 +08:00
imxyy1soope1 fe96f6d9c5 implement pattern calling 2026-05-08 17:46:22 +08:00
imxyy1soope1 a28dfada30 implement unary operations 2026-05-08 17:46:22 +08:00
imxyy1soope1 103928779f ForceMode 2026-05-08 17:46:22 +08:00
imxyy1soope1 c08e0b81c4 implement __seq and __deepSeq 2026-05-08 17:46:22 +08:00
imxyy1soope1 26717a8184 implement primop (filter) 2026-05-08 17:46:22 +08:00
imxyy1soope1 4f3cd0ef4c refactor: split VmContext 2026-04-26 15:24:40 +08:00
imxyy1soope1 468269c20d chore: update flake.lock 2026-04-26 15:24:33 +08:00
imxyy1soope1 21036aba46 implement Select and HasAttr 2026-04-24 21:09:12 +08:00
65 changed files with 6628 additions and 3514 deletions
Generated
+185 -57
View File
@@ -445,20 +445,18 @@ name = "fix"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bumpalo",
"clap", "clap",
"criterion", "criterion",
"ere", "ere",
"fix-codegen", "fix-bytecode",
"fix-common", "fix-compiler",
"fix-error", "fix-error",
"fix-ir", "fix-lang",
"fix-runtime",
"fix-vm", "fix-vm",
"ghost-cell",
"hashbrown 0.16.1", "hashbrown 0.16.1",
"miette", "miette",
"mimalloc", "mimalloc",
"rnix",
"rustyline", "rustyline",
"string-interner", "string-interner",
"tempfile", "tempfile",
@@ -469,34 +467,31 @@ dependencies = [
] ]
[[package]] [[package]]
name = "fix-builtins" name = "fix-bytecode"
version = "0.1.0"
dependencies = [
"gc-arena",
"num_enum",
]
[[package]]
name = "fix-codegen"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"colored", "colored",
"fix-builtins", "fix-lang",
"fix-common",
"fix-ir",
"hashbrown 0.16.1",
"num_enum", "num_enum",
"rnix",
"string-interner", "string-interner",
] ]
[[package]] [[package]]
name = "fix-common" name = "fix-compiler"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"ere", "bumpalo",
"gc-arena", "colored",
"fix-bytecode",
"fix-error",
"fix-lang",
"fix-runtime",
"ghost-cell",
"hashbrown 0.16.1",
"rnix",
"rowan",
"string-interner", "string-interner",
"tracing",
] ]
[[package]] [[package]]
@@ -509,18 +504,26 @@ dependencies = [
] ]
[[package]] [[package]]
name = "fix-ir" name = "fix-lang"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bumpalo", "ere",
"fix-builtins", "gc-arena",
"fix-common",
"fix-error",
"ghost-cell",
"hashbrown 0.16.1",
"num_enum", "num_enum",
"rnix", "string-interner",
"rowan", ]
[[package]]
name = "fix-runtime"
version = "0.1.0"
dependencies = [
"fix-bytecode",
"fix-error",
"fix-lang",
"gc-arena",
"hashbrown 0.16.1",
"smallvec",
"sptr",
"string-interner", "string-interner",
] ]
@@ -528,17 +531,14 @@ dependencies = [
name = "fix-vm" name = "fix-vm"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"fix-builtins", "fix-bytecode",
"fix-codegen",
"fix-common",
"fix-error", "fix-error",
"fix-lang",
"fix-runtime",
"gc-arena", "gc-arena",
"hashbrown 0.16.1", "hashbrown 0.16.1",
"likely_stable",
"num_enum",
"smallvec", "smallvec",
"sptr", "sysinfo",
"string-interner",
] ]
[[package]] [[package]]
@@ -735,15 +735,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "likely_stable"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d61f7017d8abea1fc23ff7f01a8147b2656dea3aeb24d519aab6e2177eaf671c"
dependencies = [
"rustc_version",
]
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.12.1" version = "0.12.1"
@@ -840,6 +831,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "ntapi"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.50.3" version = "0.50.3"
@@ -880,6 +880,25 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "objc2-core-foundation"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags",
]
[[package]]
name = "objc2-io-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15"
dependencies = [
"libc",
"objc2-core-foundation",
]
[[package]] [[package]]
name = "object" name = "object"
version = "0.37.3" version = "0.37.3"
@@ -1092,15 +1111,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "1.1.4" version = "1.1.4"
@@ -1285,6 +1295,20 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "sysinfo"
version = "0.38.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f"
dependencies = [
"libc",
"memchr",
"ntapi",
"objc2-core-foundation",
"objc2-io-kit",
"windows",
]
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.27.0" version = "3.27.0"
@@ -1678,12 +1702,107 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-future"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core",
"windows-link",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "windows-link" name = "windows-link"
version = "0.2.1" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core",
"windows-link",
]
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.61.2" version = "0.61.2"
@@ -1693,6 +1812,15 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link",
]
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "1.0.1" version = "1.0.1"
+20 -18
View File
@@ -2,39 +2,41 @@
resolver = "3" resolver = "3"
members = [ members = [
"fix", "fix",
"fix-builtins", "fix-bytecode",
"fix-codegen", "fix-compiler",
"fix-common",
"fix-error", "fix-error",
"fix-ir", "fix-lang",
"fix-runtime",
"fix-vm", "fix-vm",
] ]
[profile.profiling]
inherits = "release"
debug = true
[profile.lto]
inherits = "release"
lto = true
[workspace.dependencies] [workspace.dependencies]
bumpalo = { version = "3.20", features = [ bumpalo = {
version = "3.20",
features = [
"allocator-api2", "allocator-api2",
"boxed", "boxed",
"collections", "collections",
] } ]
}
ere = "0.2"
ghost-cell = "0.2" ghost-cell = "0.2"
hashbrown = "0.16" hashbrown = "0.16"
num_enum = "0.7.5" num_enum = "0.7.5"
smallvec = "1.15"
ere = "0.2"
string-interner = "0.19"
rnix = "0.14" rnix = "0.14"
rowan = "0.16" rowan = "0.16"
likely_stable = "0.1" smallvec = { version = "1.15", features = ["const_generics", "const_new"] }
string-interner = "0.19"
[workspace.dependencies.gc-arena] [workspace.dependencies.gc-arena]
git = "https://github.com/kyren/gc-arena" git = "https://github.com/kyren/gc-arena"
rev = "75671ae03f53718357b741ed4027560f14e90836" rev = "75671ae03f53718357b741ed4027560f14e90836"
features = ["allocator-api2", "hashbrown", "smallvec"] features = ["allocator-api2", "hashbrown", "smallvec"]
[profile.lto]
inherits = "release"
lto = true
[profile.profiling]
inherits = "release"
debug = true
-70
View File
@@ -1,70 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.14/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"includes": ["**", "!!**/dist"]
},
"formatter": {
"enabled": true,
"formatWithErrors": true,
"attributePosition": "auto",
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 110,
"lineEnding": "lf"
},
"linter": {
"rules": {
"style": {
"useNamingConvention": {
"level": "warn",
"options": {
"strictCase": false,
"conventions": [
{
"selector": { "kind": "objectLiteralProperty" },
"formats": ["camelCase", "PascalCase", "CONSTANT_CASE"]
},
{
"selector": { "kind": "typeProperty" },
"formats": ["camelCase", "snake_case"]
}
]
}
}
}
}
},
"overrides": [
{
"includes": ["**/global.d.ts"],
"linter": {
"rules": {
"style": {
"useNamingConvention": "off"
}
}
}
}
],
"javascript": {
"formatter": {
"arrowParentheses": "always",
"bracketSameLine": false,
"bracketSpacing": true,
"jsxQuoteStyle": "double",
"quoteProperties": "asNeeded",
"semicolons": "always",
"trailingCommas": "all"
}
},
"json": {
"formatter": {
"trailingCommas": "none"
}
}
}
-8
View File
@@ -1,8 +0,0 @@
[package]
name = "fix-builtins"
version = "0.1.0"
edition = "2024"
[dependencies]
num_enum = { workspace = true }
gc-arena = { workspace = true }
-125
View File
@@ -1,125 +0,0 @@
use gc_arena::Collect;
use num_enum::TryFromPrimitive;
macro_rules! define_builtins {
($(($name:literal, $variant:ident, $arity:expr)),* $(,)?) => {
/// Builtin function registry.
/// Array index IS the PrimOp id. (name, arity) pairs.
pub const BUILTINS: &[(&str, u8)] = &[
$(($name, $arity),)*
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive, Collect)]
#[repr(u8)]
#[collect(require_static)]
pub enum BuiltinId {
$($variant,)*
}
};
}
define_builtins! {
("abort", Abort, 1),
("__add", Add, 2),
("__addErrorContext", AddErrorContext, 2),
("__all", All, 2),
("__any", Any, 2),
("__appendContext", AppendContext, 2),
("__attrNames", AttrNames, 1),
("__attrValues", AttrValues, 1),
("baseNameOf", BaseNameOf, 1),
("__bitAnd", BitAnd, 2),
("__bitOr", BitOr, 2),
("__bitXor", BitXor, 2),
("break", Break, 1),
("__catAttrs", CatAttrs, 2),
("__ceil", Ceil, 1),
("__compareVersions", CompareVersions, 2),
("__concatLists", ConcatLists, 1),
("__concatMap", ConcatMap, 2),
("__concatStringsSep", ConcatStringsSep, 2),
("__convertHash", ConvertHash, 1),
("__deepSeq", DeepSeq, 2),
("derivation", Derivation, 1),
("derivationStrict", DerivationStrict, 1),
("dirOf", DirOf, 1),
("__div", Div, 2),
("__elem", Elem, 2),
("__elemAt", ElemAt, 2),
("fetchGit", FetchGit, 1),
("fetchMercurial", FetchMercurial, 1),
("fetchTarball", FetchTarball, 1),
("fetchTree", FetchTree, 1),
("__fetchurl", FetchUrl, 1),
("__filter", Filter, 2),
("__filterSource", FilterSource, 2),
("__findFile", FindFile, 2),
("__floor", Floor, 1),
("__foldl'", FoldlStrict, 3),
("__fromJSON", FromJSON, 1),
("fromTOML", FromTOML, 1),
("__functionArgs", FunctionArgs, 1),
("__genList", GenList, 2),
("__genericClosure", GenericClosure, 1),
("__getAttr", GetAttr, 2),
("__getContext", GetContext, 1),
("__getEnv", GetEnv, 1),
("__groupBy", GroupBy, 2),
("__hasAttr", HasAttr, 2),
("__hasContext", HasContext, 1),
("__hashFile", HashFile, 2),
("__hashString", HashString, 2),
("__head", Head, 1),
("import", Import, 1),
("__intersectAttrs", IntersectAttrs, 2),
("__isAttrs", IsAttrs, 1),
("__isBool", IsBool, 1),
("__isFloat", IsFloat, 1),
("__isFunction", IsFunction, 1),
("__isInt", IsInt, 1),
("__isList", IsList, 1),
("isNull", IsNull, 1),
("__isPath", IsPath, 1),
("__isString", IsString, 1),
("__length", Length, 1),
("__lessThan", LessThan, 2),
("__listToAttrs", ListToAttrs, 1),
("map", Map, 2),
("__mapAttrs", MapAttrs, 2),
("__match", Match, 2),
("__mul", Mul, 2),
("null", Null, 0), // constant, not a function
("__parseDrvName", ParseDrvName, 1),
("__partition", Partition, 2),
("__path", Path, 1),
("__pathExists", PathExists, 1),
("placeholder", Placeholder, 1),
("__readDir", ReadDir, 1),
("__readFile", ReadFile, 1),
("__readFileType", ReadFileType, 1),
("removeAttrs", RemoveAttrs, 2),
("__replaceStrings", ReplaceStrings, 3),
("scopedImport", ScopedImport, 2),
("__seq", Seq, 2),
("__sort", Sort, 2),
("__split", Split, 2),
("__splitVersion", SplitVersion, 1),
("__storePath", StorePath, 1),
("__stringLength", StringLength, 1),
("__sub", Sub, 2),
("__substring", Substring, 3),
("__tail", Tail, 1),
("throw", Throw, 1),
("__toFile", ToFile, 2),
("__toJSON", ToJSON, 1),
("__toPath", ToPath, 1),
("toString", ToString, 1),
("__toXML", ToXML, 1),
("__trace", Trace, 2),
("__tryEval", TryEval, 1),
("__typeOf", TypeOf, 1),
("__unsafeDiscardStringContext", UnsafeDiscardStringContext, 1),
("__unsafeGetAttrPos", UnsafeGetAttrPos, 2),
("__warn", Warn, 2),
("__zipAttrsWith", ZipAttrsWith, 2),
}
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "fix-bytecode"
version = "0.1.0"
edition = "2024"
[dependencies]
colored = "3.1.1"
num_enum = { workspace = true }
string-interner = { workspace = true }
fix-lang = { path = "../fix-lang" }
@@ -1,9 +1,8 @@
use std::fmt::Write; use std::fmt::Write;
use colored::Colorize; use colored::Colorize as _;
use num_enum::TryFromPrimitive;
use crate::{AttrKeyType, InstructionPtr, Op, OperandType}; use crate::{InstructionPtr, Op, OperandType, PrimOpPhase};
pub trait DisassemblerContext { pub trait DisassemblerContext {
fn resolve_string(&self, id: u32) -> &str; fn resolve_string(&self, id: u32) -> &str;
@@ -79,19 +78,30 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
#[inline(always)] #[inline(always)]
fn read_operand_data(&mut self) { fn read_operand_data(&mut self) {
use OperandType::*;
let tag = self.read_u8(); let tag = self.read_u8();
let ty = OperandType::try_from_primitive(tag).expect("invalid operand type"); let ty = OperandType::try_from(tag).expect("invalid operand type");
match ty { match ty {
OperandType::Const => { Const => {
self.read_u32(); self.read_u32();
} }
OperandType::Local => { BigInt => {
self.read_i64();
}
Local => {
self.read_u8(); self.read_u8();
self.read_u32(); self.read_u32();
} }
OperandType::Builtins => {} BuiltinConst => {
OperandType::BigInt => { self.read_u32();
self.read_i64(); }
Builtins => {}
ReplBinding => {
self.read_u32();
}
ScopedImportBinding => {
self.read_u32();
self.read_u32();
} }
} }
} }
@@ -190,7 +200,7 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
} }
fn decode_instruction(&mut self, op_byte: u8, current_pc: usize) -> (&'static str, String) { fn decode_instruction(&mut self, op_byte: u8, current_pc: usize) -> (&'static str, String) {
let op = Op::try_from_primitive(op_byte).expect("invalid op code"); let op = Op::try_from(op_byte).expect("invalid op code");
match op { match op {
Op::PushSmi => { Op::PushSmi => {
@@ -280,30 +290,35 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
("MakePatternClosure", arg_str) ("MakePatternClosure", arg_str)
} }
Op::Call => ("Call", String::new()), Op::Call => {
self.read_operand_data();
("Call", "arg=?".into())
}
Op::DispatchPrimOp => {
let phase = PrimOpPhase::try_from(self.read_u8()).expect("invalid primop phase");
("DispatchPrimOp", format!("phase={phase:?}"))
}
Op::MakeAttrs => { Op::MakeAttrs => {
let count = self.read_u32(); let static_count = self.read_u32();
let mut args = format!("size={}", count); let dynamic_count = self.read_u32();
for _ in 0..count { let mut args = format!("static={} dynamic={}", static_count, dynamic_count);
let key_tag = self.read_u8();
let key_ty = for _ in 0..static_count {
AttrKeyType::try_from_primitive(key_tag).expect("invalid attr key type");
match key_ty {
AttrKeyType::Static => {
let key_id = self.read_u32(); let key_id = self.read_u32();
let _ = let _ = write!(args, " [{}={}", self.ctx.resolve_string(key_id), key_id);
write!(args, " [{}={}", self.ctx.resolve_string(key_id), key_id);
}
AttrKeyType::Dynamic => {
let _ = write!(args, " [dyn");
self.read_operand_data();
}
}
self.read_operand_data(); self.read_operand_data();
let _span_id = self.read_u32(); let _span_id = self.read_u32();
args.push(']'); args.push(']');
} }
for _ in 0..dynamic_count {
let _ = write!(args, " [dyn");
self.read_operand_data();
let _span_id = self.read_u32();
args.push(']');
}
("MakeAttrs", args) ("MakeAttrs", args)
} }
Op::MakeEmptyAttrs => ("MakeEmptyAttrs", String::new()), Op::MakeEmptyAttrs => ("MakeEmptyAttrs", String::new()),
@@ -320,6 +335,27 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
let span_id = self.read_u32(); let span_id = self.read_u32();
("SelectDynamic", format!("span={}", span_id)) ("SelectDynamic", format!("span={}", span_id))
} }
Op::HasAttrPathStatic => {
let span_id = self.read_u32();
let key_id = self.read_u32();
(
"HasAttrPathStatic",
format!("key={} span={}", self.ctx.resolve_string(key_id), span_id),
)
}
Op::HasAttrPathDynamic => {
let span_id = self.read_u32();
("HasAttrPathDynamic", format!("span={}", span_id))
}
Op::HasAttrStatic => {
let key_id = self.read_u32();
(
"HasAttrStatic",
format!("key={}", self.ctx.resolve_string(key_id)),
)
}
Op::HasAttrDynamic => ("HasAttrDynamic", String::new()),
Op::HasAttrResolve => ("HasAttrResolve", String::new()),
Op::JumpIfSelectSucceeded => { Op::JumpIfSelectSucceeded => {
let offset = self.read_i32(); let offset = self.read_i32();
let target = (current_pc as isize + 1 + 4 + offset as isize) as usize; let target = (current_pc as isize + 1 + 4 + offset as isize) as usize;
@@ -328,9 +364,13 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
format!("-> {:04x} offset={}", target, offset), format!("-> {:04x} offset={}", target, offset),
) )
} }
Op::HasAttr => { Op::JumpIfSelectFailed => {
let path_len = self.read_u16(); let offset = self.read_i32();
("HasAttr", format!("path_len={}", path_len)) let target = (current_pc as isize + 1 + 4 + offset as isize) as usize;
(
"JumpIfSelectFailed",
format!("-> {:04x} offset={}", target, offset),
)
} }
Op::MakeList => { Op::MakeList => {
@@ -381,19 +421,25 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
let force = self.read_u8(); let force = self.read_u8();
("ConcatStrings", format!("count={} force={}", count, force)) ("ConcatStrings", format!("count={} force={}", count, force))
} }
Op::ResolvePath => ("ResolvePath", String::new()), Op::CoerceToString => ("CoerceToString", String::new()),
Op::ResolvePath => {
let dir_id = self.read_u32();
let dir = self.ctx.resolve_string(dir_id);
("ResolvePath", format!("dir={:?}", dir))
}
Op::Assert => { Op::Assert => {
let raw_idx = self.read_u32(); let raw_idx = self.read_u32();
let span_id = self.read_u32(); let span_id = self.read_u32();
("Assert", format!("text_id={} span={}", raw_idx, span_id)) ("Assert", format!("text_id={} span={}", raw_idx, span_id))
} }
Op::PushWith => ("PushWith", String::new()),
Op::PopWith => ("PopWith", String::new()),
Op::PrepareWith => ("PrepareWith", String::new()),
Op::LookupWith => { Op::LookupWith => {
let idx = self.read_u32(); let idx = self.read_u32();
let name = self.ctx.resolve_string(idx); let name = self.ctx.resolve_string(idx);
("LookupWith", format!("{:?}", name)) let n = self.read_u8();
for _ in 0..n {
self.read_operand_data();
}
("LookupWith", format!("sym={:?} n={}", name, n))
} }
Op::LoadBuiltins => ("LoadBuiltins", String::new()), Op::LoadBuiltins => ("LoadBuiltins", String::new()),
@@ -401,19 +447,16 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
let id = self.read_u8(); let id = self.read_u8();
("LoadBuiltin", format!("id={}", id)) ("LoadBuiltin", format!("id={}", id))
} }
Op::MkPos => {
let span_id = self.read_u32();
("MkPos", format!("id={}", span_id))
}
Op::LoadReplBinding => { Op::LoadReplBinding => {
let idx = self.read_u32(); let idx = self.read_u32();
let name = self.ctx.resolve_string(idx); let name = self.ctx.resolve_string(idx);
("LoadReplBinding", format!("{:?}", name)) ("LoadReplBinding", format!("{:?}", name))
} }
Op::LoadScopedBinding => { Op::LoadScopedBinding => {
let slot = self.read_u32();
let idx = self.read_u32(); let idx = self.read_u32();
let name = self.ctx.resolve_string(idx); let name = self.ctx.resolve_string(idx);
("LoadScopedBinding", format!("{:?}", name)) ("LoadScopedBinding", format!("slot={} {:?}", slot, name))
} }
Op::Return => ("Return", String::new()), Op::Return => ("Return", String::new()),
Op::Illegal => ("Illegal", String::new()), Op::Illegal => ("Illegal", String::new()),
+532
View File
@@ -0,0 +1,532 @@
#![allow(dead_code)]
use fix_lang::{BuiltinId, StringId};
use num_enum::TryFromPrimitive;
use string_interner::Symbol as _;
pub mod disassembler;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct InstructionPtr(pub usize);
#[repr(u8)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
#[allow(clippy::enum_variant_names)]
pub enum Op {
PushSmi,
PushBigInt,
PushFloat,
PushString,
PushNull,
PushTrue,
PushFalse,
LoadLocal,
LoadOuter,
StoreLocal,
AllocLocals,
MakeThunk,
MakeClosure,
MakePatternClosure,
Call,
DispatchPrimOp,
MakeAttrs,
MakeEmptyAttrs,
SelectStatic,
SelectDynamic,
HasAttrPathStatic,
HasAttrPathDynamic,
HasAttrStatic,
HasAttrDynamic,
HasAttrResolve,
JumpIfSelectSucceeded,
JumpIfSelectFailed,
MakeList,
MakeEmptyList,
OpAdd,
OpSub,
OpMul,
OpDiv,
OpEq,
OpNeq,
OpLt,
OpGt,
OpLeq,
OpGeq,
OpConcat,
OpUpdate,
OpNeg,
OpNot,
JumpIfFalse,
JumpIfTrue,
Jump,
CoerceToString,
ConcatStrings,
ResolvePath,
Assert,
LookupWith,
LoadBuiltins,
LoadBuiltin,
LoadReplBinding,
LoadScopedBinding,
Return,
Illegal,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
pub enum OperandType {
Const,
BigInt,
Local,
BuiltinConst,
Builtins,
ReplBinding,
ScopedImportBinding,
}
pub enum Const {
Smi(i32),
Float(f64),
Bool(bool),
String(StringId),
Path(StringId),
PrimOp {
id: BuiltinId,
arity: u8,
dispatch_ip: u32,
},
Null,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
pub enum AttrKeyType {
Static,
Dynamic,
}
pub enum OperandData {
Const(u32),
BigInt(i64),
Local { layer: u8, idx: u32 },
BuiltinConst(StringId),
Builtins,
ReplBinding(StringId),
ScopedImportBinding { slot_id: u32, name: StringId },
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum PrimOpPhase {
Abort,
Add,
AddErrorContext,
All,
AllCallPred,
AllCheck,
Any,
AnyCallPred,
AnyCheck,
AppendContext,
AttrNames,
AttrValues,
BaseNameOf,
BitAnd,
BitOr,
BitXor,
Break,
CatAttrs,
Ceil,
CompareVersions,
ConcatLists,
ConcatMap,
ConcatStringsSep,
ConvertHash,
DeepSeq,
DeepSeqPush,
DeepSeqLoop,
Derivation,
DerivationStrict,
DirOf,
Div,
Elem,
ElemAt,
FetchGit,
FetchMercurial,
FetchTarball,
FetchTree,
FetchUrl,
FilterForceList,
FilterCallPred,
FilterCheck,
FilterSource,
FindFile,
Floor,
FoldlStrict,
FoldlStrictEmpty,
FoldlStrictCall1,
FoldlStrictCall2,
FoldlStrictUpdate,
FromJSON,
FromTOML,
FunctionArgs,
GenList,
GenericClosure,
GetAttr,
GetContext,
GetEnv,
GroupBy,
HasAttr,
HasContext,
HashFile,
HashString,
Head,
Import,
IntersectAttrs,
IsAttrs,
IsBool,
IsFloat,
IsFunction,
IsInt,
IsList,
IsNull,
IsPath,
IsString,
Length,
LessThan,
ListToAttrs,
Map,
MapAttrs,
Match,
Mul,
ParseDrvName,
Partition,
Path,
PathExists,
Placeholder,
ReadDir,
ReadFile,
ReadFileType,
RemoveAttrs,
ReplaceStrings,
ScopedImport,
Seq,
Sort,
Split,
SplitVersion,
StorePath,
StringLength,
Sub,
Substring,
Tail,
Throw,
ToFile,
ToJSON,
ToPath,
ToString,
ToXML,
Trace,
TryEval,
TypeOf,
UnsafeDiscardStringContext,
UnsafeGetAttrPos,
Warn,
ZipAttrsWith,
ForceResultShallow,
ForceResultShallowPush,
ForceResultShallowLoop,
ForceResultDeepFinish,
EqStep,
EqForce,
CallPattern,
CallFunctor1,
CallFunctor2,
ImportFinalize,
ScopedImportFinalize,
AppendContextLoop,
AppendContextEntryForced,
AppendContextOutputsForced,
AppendContextOutputElementLoop,
AppendContextOutputElementForced,
UnsafeDiscardOutputDependency,
Illegal,
}
impl TryFrom<u8> for PrimOpPhase {
type Error = u8;
fn try_from(value: u8) -> Result<Self, Self::Error> {
if (0..Self::Illegal as u8).contains(&value) {
Ok(unsafe { std::mem::transmute::<u8, Self>(value) })
} else {
Err(value)
}
}
}
impl PrimOpPhase {
pub fn entry_for_builtin(id: BuiltinId) -> Self {
use BuiltinId::*;
match id {
Abort => Self::Abort,
Add => Self::Add,
AddErrorContext => Self::AddErrorContext,
All => Self::All,
Any => Self::Any,
AppendContext => Self::AppendContext,
AttrNames => Self::AttrNames,
AttrValues => Self::AttrValues,
BaseNameOf => Self::BaseNameOf,
BitAnd => Self::BitAnd,
BitOr => Self::BitOr,
BitXor => Self::BitXor,
Break => Self::Break,
CatAttrs => Self::CatAttrs,
Ceil => Self::Ceil,
CompareVersions => Self::CompareVersions,
ConcatLists => Self::ConcatLists,
ConcatMap => Self::ConcatMap,
ConcatStringsSep => Self::ConcatStringsSep,
ConvertHash => Self::ConvertHash,
DeepSeq => Self::DeepSeq,
Derivation => Self::Derivation,
DerivationStrict => Self::DerivationStrict,
DirOf => Self::DirOf,
Div => Self::Div,
Elem => Self::Elem,
ElemAt => Self::ElemAt,
FetchGit => Self::FetchGit,
FetchMercurial => Self::FetchMercurial,
FetchTarball => Self::FetchTarball,
FetchTree => Self::FetchTree,
FetchUrl => Self::FetchUrl,
Filter => Self::FilterForceList,
FilterSource => Self::FilterSource,
FindFile => Self::FindFile,
Floor => Self::Floor,
FoldlStrict => Self::FoldlStrict,
FromJSON => Self::FromJSON,
FromTOML => Self::FromTOML,
FunctionArgs => Self::FunctionArgs,
GenList => Self::GenList,
GenericClosure => Self::GenericClosure,
GetAttr => Self::GetAttr,
GetContext => Self::GetContext,
GetEnv => Self::GetEnv,
GroupBy => Self::GroupBy,
HasAttr => Self::HasAttr,
HasContext => Self::HasContext,
HashFile => Self::HashFile,
HashString => Self::HashString,
Head => Self::Head,
Import => Self::Import,
IntersectAttrs => Self::IntersectAttrs,
IsAttrs => Self::IsAttrs,
IsBool => Self::IsBool,
IsFloat => Self::IsFloat,
IsFunction => Self::IsFunction,
IsInt => Self::IsInt,
IsList => Self::IsList,
IsNull => Self::IsNull,
IsPath => Self::IsPath,
IsString => Self::IsString,
Length => Self::Length,
LessThan => Self::LessThan,
ListToAttrs => Self::ListToAttrs,
Map => Self::Map,
MapAttrs => Self::MapAttrs,
Match => Self::Match,
Mul => Self::Mul,
ParseDrvName => Self::ParseDrvName,
Partition => Self::Partition,
Path => Self::Path,
PathExists => Self::PathExists,
Placeholder => Self::Placeholder,
ReadDir => Self::ReadDir,
ReadFile => Self::ReadFile,
ReadFileType => Self::ReadFileType,
RemoveAttrs => Self::RemoveAttrs,
ReplaceStrings => Self::ReplaceStrings,
ScopedImport => Self::ScopedImport,
Seq => Self::Seq,
Sort => Self::Sort,
Split => Self::Split,
SplitVersion => Self::SplitVersion,
StorePath => Self::StorePath,
StringLength => Self::StringLength,
Sub => Self::Sub,
Substring => Self::Substring,
Tail => Self::Tail,
Throw => Self::Throw,
ToFile => Self::ToFile,
ToJSON => Self::ToJSON,
ToPath => Self::ToPath,
ToString => Self::ToString,
ToXML => Self::ToXML,
Trace => Self::Trace,
TryEval => Self::TryEval,
TypeOf => Self::TypeOf,
UnsafeDiscardStringContext => Self::UnsafeDiscardStringContext,
UnsafeDiscardOutputDependency => Self::UnsafeDiscardOutputDependency,
UnsafeGetAttrPos => Self::UnsafeGetAttrPos,
Warn => Self::Warn,
ZipAttrsWith => Self::ZipAttrsWith,
}
}
pub fn ip(self) -> u32 {
self as u32 * 2
}
}
pub struct BytecodeReader<'a> {
bytecode: &'a [u8],
pc: usize,
inst_start_pc: usize,
}
impl<'a> BytecodeReader<'a> {
pub fn new(bytecode: &'a [u8], pc: usize) -> Self {
Self {
bytecode,
pc,
inst_start_pc: pc,
}
}
#[inline(always)]
pub fn from_after_op(bytecode: &'a [u8], inst_start_pc: usize) -> Self {
Self {
bytecode,
pc: inst_start_pc + 1,
inst_start_pc,
}
}
#[inline(always)]
#[cfg_attr(debug_assertions, track_caller)]
fn read_array<const N: usize>(&mut self) -> [u8; N] {
let ret = self.bytecode[self.pc..self.pc + N]
.try_into()
.expect("read_array failed");
self.pc += N;
ret
}
#[inline(always)]
pub fn read_op(&mut self) -> Op {
self.inst_start_pc = self.pc;
let byte = self.bytecode[self.pc];
if !(0..Op::Illegal as u8).contains(&byte) {
std::hint::cold_path();
panic!("unknown opcode: {byte:#04x}")
}
self.pc += 1;
unsafe { std::mem::transmute::<u8, Op>(byte) }
}
#[inline(always)]
pub fn read_u8(&mut self) -> u8 {
let val = self.bytecode[self.pc];
self.pc += 1;
val
}
#[inline(always)]
pub fn read_u16(&mut self) -> u16 {
u16::from_le_bytes(self.read_array())
}
#[inline(always)]
pub fn read_u32(&mut self) -> u32 {
u32::from_le_bytes(self.read_array())
}
#[inline(always)]
pub fn read_i32(&mut self) -> i32 {
i32::from_le_bytes(self.read_array())
}
#[inline(always)]
pub fn read_i64(&mut self) -> i64 {
i64::from_le_bytes(self.read_array())
}
#[inline(always)]
pub fn read_f64(&mut self) -> f64 {
f64::from_le_bytes(self.read_array())
}
#[inline(always)]
pub fn read_string_id(&mut self) -> StringId {
let raw = self.read_u32();
#[allow(clippy::unwrap_used)]
StringId(string_interner::symbol::SymbolU32::try_from_usize(raw as usize).unwrap())
}
#[inline(always)]
pub fn read_operand_data(&mut self) -> OperandData {
let tag = self.read_u8();
let Ok(ty) = OperandType::try_from_primitive(tag)
.map_err(|err| panic!("unknown operand tag: {:#04x}", err.number));
match ty {
OperandType::Const => OperandData::Const(self.read_u32()),
OperandType::BigInt => OperandData::BigInt(self.read_i64()),
OperandType::Local => {
let layer = self.read_u8();
let idx = self.read_u32();
OperandData::Local { layer, idx }
}
OperandType::BuiltinConst => OperandData::BuiltinConst(self.read_string_id()),
OperandType::Builtins => OperandData::Builtins,
OperandType::ReplBinding => OperandData::ReplBinding(self.read_string_id()),
OperandType::ScopedImportBinding => {
let slot_id = self.read_u32();
let name = self.read_string_id();
OperandData::ScopedImportBinding { slot_id, name }
}
}
}
pub fn pc(&self) -> usize {
self.pc
}
pub fn set_pc(&mut self, pc: usize) {
self.pc = pc;
}
pub fn inst_start_pc(&self) -> usize {
self.inst_start_pc
}
}
-15
View File
@@ -1,15 +0,0 @@
[package]
name = "fix-codegen"
version = "0.1.0"
edition = "2024"
[dependencies]
hashbrown = { workspace = true }
num_enum = { workspace = true }
rnix = { workspace = true }
string-interner = { workspace = true }
colored = "3.1.1"
fix-builtins = { path = "../fix-builtins" }
fix-common = { path = "../fix-common" }
fix-ir = { path = "../fix-ir" }
@@ -1,17 +1,19 @@
[package] [package]
name = "fix-ir" name = "fix-compiler"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
bumpalo = { workspace = true } bumpalo = { workspace = true }
colored = "3.1.1"
ghost-cell = { workspace = true } ghost-cell = { workspace = true }
hashbrown = { workspace = true }
rnix = { workspace = true } rnix = { workspace = true }
rowan = { workspace = true } rowan = { workspace = true }
string-interner = { workspace = true } string-interner = { workspace = true }
hashbrown = { workspace = true }
num_enum = { workspace = true }
fix-builtins = { path = "../fix-builtins" } fix-bytecode = { path = "../fix-bytecode" }
fix-common = { path = "../fix-common" }
fix-error = { path = "../fix-error" } fix-error = { path = "../fix-error" }
fix-lang = { path = "../fix-lang" }
fix-runtime = { path = "../fix-runtime" }
tracing = "0.1"
+542
View File
@@ -0,0 +1,542 @@
use bumpalo::Bump;
use fix_bytecode::{Const, InstructionPtr, Op, PrimOpPhase};
use fix_error::{Error, Result, Source};
use fix_lang::{StringId, Symbol};
use fix_runtime::{StaticValue, VmCode, VmRuntimeCtx};
use ghost_cell::{GhostCell, GhostToken};
use hashbrown::{HashMap, HashSet};
use string_interner::DefaultStringInterner;
use crate::BytecodeContext;
use crate::ir::downgrade::{Downgrade as _, DowngradeContext};
use crate::ir::{
GhostMaybeThunkRef, GhostRoIrRef, GhostRoMaybeThunkRef, GhostRoRef, Ir, MaybeThunk, RawIrRef,
ThunkId,
};
pub struct CodeState {
pub bytecode: Vec<u8>,
pub sources: Vec<Source>,
pub spans: Vec<(usize, rnix::TextRange)>,
pub thunk_count: usize,
pub global_env: HashMap<StringId, MaybeThunk>,
}
impl CodeState {
pub fn new(strings: &mut DefaultStringInterner) -> Self {
let global_env = crate::ir::new_global_env(strings);
let mut bytecode = Vec::with_capacity(PrimOpPhase::Illegal as usize * 2);
for phase in 0..=PrimOpPhase::Illegal as u8 {
bytecode.push(Op::DispatchPrimOp as u8);
bytecode.push(phase);
}
Self {
sources: Vec::new(),
spans: Vec::new(),
thunk_count: 0,
bytecode,
global_env,
}
}
pub fn compile_bytecode<'ctx>(
&'ctx mut self,
source: Source,
extra_scope: Option<ExtraScope<'ctx>>,
runtime: &'ctx mut impl VmRuntimeCtx,
) -> Result<InstructionPtr> {
let mut compiler = CompilerCtx {
code: self,
runtime,
};
compiler.compile_bytecode(source, extra_scope)
}
}
impl VmCode for CodeState {
fn bytecode(&self) -> &[u8] {
&self.bytecode
}
fn compile_with_scope(
&mut self,
source: Source,
extra_scope: Option<fix_runtime::ExtraScope>,
runtime: &mut impl VmRuntimeCtx,
) -> Result<InstructionPtr> {
let extra = extra_scope.map(|s| match s {
fix_runtime::ExtraScope::ScopedImport { keys, slot_id } => {
ExtraScope::ScopedImport { keys, slot_id }
}
});
CodeState::compile_bytecode(self, source, extra, runtime)
}
}
struct CompilerCtx<'a, R: VmRuntimeCtx> {
code: &'a mut CodeState,
runtime: &'a mut R,
}
impl<'a, R: VmRuntimeCtx> CompilerCtx<'a, R> {
fn compile_bytecode(
&mut self,
source: Source,
extra_scope: Option<ExtraScope>,
) -> Result<InstructionPtr> {
let root = self.downgrade(source, extra_scope)?;
let ip = crate::compile_bytecode(root.as_ref(), self);
Ok(ip)
}
fn downgrade(&mut self, source: Source, extra_scope: Option<ExtraScope>) -> Result<OwnedIr> {
tracing::debug!("Parsing Nix expression");
self.code.sources.push(source.clone());
let root = rnix::Root::parse(&source.src);
handle_parse_error(root.errors(), source.clone()).map_or(Ok(()), Err)?;
tracing::debug!("Downgrading Nix expression");
let expr = root
.tree()
.expr()
.ok_or_else(|| Error::parse_error("unexpected EOF".into()))?;
let bump = Bump::new();
GhostToken::new(|token| {
let downgrade_ctx = DowngradeCtx::new(
&bump,
token,
self.runtime,
&self.code.global_env,
extra_scope.map(Into::into),
&mut self.code.thunk_count,
source,
);
let ir = downgrade_ctx.downgrade_toplevel(expr)?;
let ir = unsafe { std::mem::transmute::<RawIrRef<'_>, RawIrRef<'static>>(ir) };
Ok(OwnedIr { _bump: bump, ir })
})
}
}
impl<'a, R: VmRuntimeCtx> BytecodeContext for CompilerCtx<'a, R> {
fn intern_string(&mut self, s: &str) -> StringId {
self.runtime.intern_string(s)
}
fn register_span(&mut self, range: rnix::TextRange) -> u32 {
let id = self.code.spans.len();
let source_id = self
.code
.sources
.len()
.checked_sub(1)
.expect("current_source not set");
self.code.spans.push((source_id, range));
id as u32
}
fn get_code(&self) -> &[u8] {
&self.code.bytecode
}
fn get_code_mut(&mut self) -> &mut Vec<u8> {
&mut self.code.bytecode
}
fn add_constant(&mut self, val: Const) -> u32 {
use Const::*;
let val = match val {
Smi(x) => StaticValue::new_inline(x),
Float(x) => StaticValue::new_float(x),
Bool(x) => StaticValue::new_inline(x),
String(x) => StaticValue::new_inline(x),
Path(x) => StaticValue::new_inline(fix_runtime::Path(x)),
PrimOp {
id,
arity,
dispatch_ip,
} => StaticValue::new_primop(id, arity, dispatch_ip),
Null => StaticValue::default(),
};
self.runtime.add_const(val)
}
fn current_source_dir(&mut self) -> StringId {
let dir = self
.code
.sources
.last()
.expect("current_source not set")
.get_dir()
.to_string_lossy()
.into_owned();
self.runtime.intern_string(dir)
}
}
fn parse_error_span(error: &rnix::ParseError) -> Option<rnix::TextRange> {
use rnix::ParseError::*;
match error {
Unexpected(range)
| UnexpectedExtra(range)
| UnexpectedWanted(_, range, _)
| UnexpectedDoubleBind(range)
| DuplicatedArgs(range, _) => Some(*range),
_ => None,
}
}
fn handle_parse_error<'a>(
errors: impl IntoIterator<Item = &'a rnix::ParseError>,
source: Source,
) -> Option<Box<Error>> {
for err in errors {
if let Some(span) = parse_error_span(err) {
return Some(
Error::parse_error(err.to_string())
.with_source(source)
.with_span(span),
);
}
}
None
}
struct DowngradeCtx<'ctx, 'id, 'ir, R: VmRuntimeCtx> {
bump: &'ir Bump,
token: GhostToken<'id>,
runtime: &'ctx mut R,
source: Source,
scopes: Vec<Scope<'ctx, 'id, 'ir>>,
with_stack: Vec<GhostRoMaybeThunkRef<'id, 'ir>>,
thunk_count: &'ctx mut usize,
thunk_scopes: Vec<ThunkScope<'id, 'ir>>,
}
impl<'ctx, 'id, 'ir, R: VmRuntimeCtx> DowngradeCtx<'ctx, 'id, 'ir, R> {
fn new(
bump: &'ir Bump,
token: GhostToken<'id>,
runtime: &'ctx mut R,
global: &'ctx HashMap<StringId, MaybeThunk>,
extra_scope: Option<Scope<'ctx, 'id, 'ir>>,
thunk_count: &'ctx mut usize,
source: Source,
) -> Self {
Self {
bump,
token,
runtime,
source,
scopes: std::iter::once(Scope::Global(global))
.chain(extra_scope)
.collect(),
thunk_count,
with_stack: Vec::new(),
thunk_scopes: vec![ThunkScope::new_in(bump)],
}
}
}
impl<'ctx: 'ir, 'id, 'ir, R: VmRuntimeCtx> DowngradeContext<'id, 'ir>
for DowngradeCtx<'ctx, 'id, 'ir, R>
{
fn new_expr(&self, expr: Ir<'ir, GhostRoRef<'id, 'ir>>) -> GhostRoIrRef<'id, 'ir> {
self.bump.alloc(GhostCell::new(expr).into())
}
fn maybe_thunk(&mut self, ir: GhostRoIrRef<'id, 'ir>) -> GhostRoMaybeThunkRef<'id, 'ir> {
use MaybeThunk::*;
let expr = (|| {
let expr = match *ir.borrow(&self.token) {
Ir::Builtin(x) => Builtin(x),
Ir::Int(x) => Int(x),
Ir::Float(x) => Float(x),
Ir::Bool(x) => Bool(x),
Ir::Str(x) => Str(x),
Ir::Arg { layer } => Arg { layer },
Ir::Builtins => Builtins,
Ir::Null => Null,
Ir::MaybeThunk(thunk) => return Some(thunk),
_ => return None,
};
Some(self.bump.alloc(GhostCell::new(expr).into()))
})();
if let Some(thunk) = expr {
return thunk;
}
let id = ThunkId(*self.thunk_count);
*self.thunk_count = self.thunk_count.checked_add(1).expect("thunk id overflow");
self.thunk_scopes
.last_mut()
.expect("no active cache scope")
.add_binding(id, ir);
self.bump.alloc(GhostCell::new(Thunk(id)).into())
}
fn intern_string(&mut self, sym: impl AsRef<str>) -> StringId {
self.runtime.intern_string(sym)
}
fn resolve_sym(&self, id: StringId) -> Symbol<'_> {
self.runtime.resolve_string(id).into()
}
fn lookup(
&mut self,
sym: StringId,
span: rnix::TextRange,
) -> Result<GhostRoMaybeThunkRef<'id, 'ir>> {
for scope in self.scopes.iter().rev() {
match scope {
&Scope::Global(global_scope) => {
if let Some(expr) = global_scope.get(&sym) {
return Ok(expr.into());
}
}
&Scope::Repl(repl_bindings) => {
if repl_bindings.contains(&sym) {
return Ok(self
.bump
.alloc(GhostCell::new(MaybeThunk::ReplBinding(sym)).into()));
}
}
&Scope::ScopedImport { ref keys, slot_id } => {
if keys.contains(&sym) {
return Ok(self.bump.alloc(
GhostCell::new(MaybeThunk::ScopedImportBinding { sym, slot_id }).into(),
));
}
}
Scope::Let(let_scope) => {
if let Some(&expr) = let_scope.get(&sym) {
return Ok(expr.into());
}
}
&Scope::Param {
sym: param_sym,
abs_layer,
} => {
if param_sym == sym {
let layers: u8 =
self.thunk_scopes.len().try_into().expect("scope too deep!");
let layer = layers - abs_layer;
return Ok(self
.bump
.alloc(GhostCell::new(MaybeThunk::Arg { layer }).into()));
}
}
}
}
if !self.with_stack.is_empty() {
let id = ThunkId(*self.thunk_count);
*self.thunk_count = self.thunk_count.checked_add(1).expect("thunk id overflow");
let mut namespaces =
bumpalo::collections::Vec::with_capacity_in(self.with_stack.len(), self.bump);
namespaces.extend(self.with_stack.iter().rev().copied());
let body = self
.bump
.alloc(GhostCell::new(Ir::WithLookup { sym, namespaces }).into());
self.thunk_scopes
.last_mut()
.expect("no active thunk scope")
.add_binding(id, body);
Ok(self
.bump
.alloc(GhostCell::new(MaybeThunk::Thunk(id)).into()))
} else {
Err(Error::downgrade_error(
format!("'{}' not found", self.resolve_sym(sym)),
self.get_current_source(),
span,
))
}
}
fn get_current_source(&self) -> Source {
self.source.clone()
}
fn with_let_scope<F, Ret>(&mut self, keys: &[StringId], f: F) -> Result<Ret>
where
F: FnOnce(
&mut Self,
) -> Result<(
bumpalo::collections::Vec<'ir, GhostRoMaybeThunkRef<'id, 'ir>>,
Ret,
)>,
{
let base = *self.thunk_count;
*self.thunk_count = self
.thunk_count
.checked_add(keys.len())
.expect("thunk id overflow");
let handles = (base..base + keys.len())
.map(|id| {
&*self
.bump
.alloc(GhostCell::new(MaybeThunk::Thunk(ThunkId(id))))
})
.collect::<Vec<_>>();
let scope = keys.iter().copied().zip(handles.iter().copied()).collect();
self.scopes.push(Scope::Let(scope));
let (vals, ret) = { f(self)? };
self.scopes.pop();
assert_eq!(keys.len(), vals.len());
let scope = self.thunk_scopes.last_mut().expect("no active thunk scope");
for (i, (val, handle)) in vals.into_iter().zip(handles).enumerate() {
let thunk = *val.borrow(&self.token);
*handle.borrow_mut(&mut self.token) = thunk;
let id = ThunkId(base + i);
let ir_ref = self
.bump
.alloc(GhostCell::new(Ir::MaybeThunk(handle.into())).into());
scope.add_binding(id, ir_ref);
}
Ok(ret)
}
fn with_param_scope<F, Ret>(&mut self, sym: StringId, f: F) -> Ret
where
F: FnOnce(&mut Self) -> Ret,
{
self.scopes.push(Scope::Param {
sym,
abs_layer: self.thunk_scopes.len().try_into().expect("scope too deep!"),
});
let mut guard = ScopeGuard { ctx: self };
f(guard.as_ctx())
}
fn with_with_scope<F, Ret>(&mut self, namespace: GhostRoMaybeThunkRef<'id, 'ir>, f: F) -> Ret
where
F: FnOnce(&mut Self) -> Ret,
{
self.with_stack.push(namespace);
let ret = f(self);
self.with_stack.pop();
ret
}
fn with_thunk_scope<F, Ret>(
&mut self,
f: F,
) -> (
Ret,
bumpalo::collections::Vec<'ir, (ThunkId, GhostRoIrRef<'id, 'ir>)>,
)
where
F: FnOnce(&mut Self) -> Ret,
{
if self.thunk_scopes.len() == u8::MAX as usize {
panic!("scope too deep!");
}
self.thunk_scopes.push(ThunkScope::new_in(self.bump));
let ret = f(self);
(
ret,
self.thunk_scopes
.pop()
.expect("no thunk scope left???")
.bindings,
)
}
fn bump(&self) -> &'ir bumpalo::Bump {
self.bump
}
}
impl<'id, 'ir, 'ctx: 'ir, R: VmRuntimeCtx> DowngradeCtx<'ctx, 'id, 'ir, R> {
fn downgrade_toplevel(mut self, root: rnix::ast::Expr) -> Result<RawIrRef<'ir>> {
let body = root.downgrade(&mut self)?;
let thunks = self
.thunk_scopes
.pop()
.expect("no thunk scope left???")
.bindings;
Ok(Ir::freeze(
self.new_expr(Ir::TopLevel { body, thunks }),
self.token,
))
}
}
struct ThunkScope<'id, 'ir> {
bindings: bumpalo::collections::Vec<'ir, (ThunkId, GhostRoIrRef<'id, 'ir>)>,
}
impl<'id, 'ir> ThunkScope<'id, 'ir> {
fn new_in(bump: &'ir Bump) -> Self {
Self {
bindings: bumpalo::collections::Vec::new_in(bump),
}
}
fn add_binding(&mut self, id: ThunkId, ir: GhostRoIrRef<'id, 'ir>) {
self.bindings.push((id, ir));
}
}
enum Scope<'ctx, 'id, 'ir> {
Global(&'ctx HashMap<StringId, MaybeThunk>),
Repl(&'ctx HashSet<StringId>),
ScopedImport {
keys: HashSet<StringId>,
#[allow(dead_code)]
slot_id: u32,
},
Let(HashMap<StringId, GhostMaybeThunkRef<'id, 'ir>>),
Param {
sym: StringId,
abs_layer: u8,
},
}
pub enum ExtraScope<'ctx> {
Repl(&'ctx HashSet<StringId>),
ScopedImport {
keys: HashSet<StringId>,
slot_id: u32,
},
}
impl<'ctx> From<ExtraScope<'ctx>> for Scope<'ctx, '_, '_> {
fn from(value: ExtraScope<'ctx>) -> Self {
use ExtraScope::*;
match value {
ScopedImport { keys, slot_id } => Scope::ScopedImport { keys, slot_id },
Repl(scope) => Scope::Repl(scope),
}
}
}
struct ScopeGuard<'a, 'ctx, 'id, 'ir, R: VmRuntimeCtx> {
ctx: &'a mut DowngradeCtx<'ctx, 'id, 'ir, R>,
}
impl<'a, 'ctx, 'id, 'ir, R: VmRuntimeCtx> Drop for ScopeGuard<'a, 'ctx, 'id, 'ir, R> {
fn drop(&mut self) {
self.ctx.scopes.pop();
}
}
impl<'a, 'ctx, 'id, 'ir, R: VmRuntimeCtx> ScopeGuard<'a, 'ctx, 'id, 'ir, R> {
fn as_ctx(&mut self) -> &mut DowngradeCtx<'ctx, 'id, 'ir, R> {
self.ctx
}
}
struct OwnedIr {
_bump: Bump,
ir: RawIrRef<'static>,
}
impl OwnedIr {
fn as_ref<'ir>(&'ir self) -> RawIrRef<'ir> {
unsafe { std::mem::transmute::<RawIrRef<'static>, RawIrRef<'ir>>(self.ir) }
}
}
+329
View File
@@ -0,0 +1,329 @@
use std::hash::Hash;
use std::marker::PhantomData;
use bumpalo::Bump;
use bumpalo::collections::Vec;
use fix_lang::{BUILTINS, BuiltinId, StringId};
use ghost_cell::{GhostCell, GhostToken};
use rnix::{TextRange, ast};
use string_interner::DefaultStringInterner;
pub mod downgrade;
pub type HashMap<'ir, K, V> = hashbrown::HashMap<K, V, hashbrown::DefaultHashBuilder, &'ir Bump>;
pub type GhostIrRef<'id, 'ir> = <GhostRef<'id, 'ir> as RefExt<'ir>>::IrRef;
pub type GhostRoIrRef<'id, 'ir> = <GhostRoRef<'id, 'ir> as RefExt<'ir>>::IrRef;
pub type RawIrRef<'ir> = <RawRef<'ir> as RefExt<'ir>>::IrRef;
pub type GhostMaybeThunkRef<'id, 'ir> = <GhostRef<'id, 'ir> as RefExt<'ir>>::MaybeThunkRef;
pub type GhostRoMaybeThunkRef<'id, 'ir> = <GhostRoRef<'id, 'ir> as RefExt<'ir>>::MaybeThunkRef;
impl<'id, 'ir> Ir<'ir, GhostRoRef<'id, 'ir>> {
/// Freeze a mutable IR reference into a read-only one, consuming the
/// `GhostToken` to prevent any further mutation.
pub fn freeze(this: GhostRoIrRef<'id, 'ir>, _: GhostToken<'id>) -> RawIrRef<'ir> {
// SAFETY: The transmute is sound because:
// - `GhostCell<'id, T>` is `#[repr(transparent)]` over `T`, so
// `&'ir GhostCell<'id, T>` and `&'ir T` have identical layout.
// - `Ir<'ir, R>` is `#[repr(C)]`, and for every field that depends on
// `R`, instantiating `R = GhostRef<'id, 'ir>` vs `R = RawRef<'ir>`
// produces types of identical layout:
// - `R::IrRef` becomes `&'ir GhostCell<'id, Ir<...>>` vs `&'ir Ir<...>`
// - `R::MaybeThunkRef` becomes `&'ir GhostCell<'id, MaybeThunk>`
// vs `&'ir MaybeThunk`
// - `R::Ref<Ir<'ir, R>>` (used in `ConcatStrings::parts`) reduces
// to the same case as `R::IrRef`
// - Therefore `IrRef<'id, 'ir>` and `RawIrRef<'ir>` are both
// pointer-sized references with the same layout.
//
// Consuming the `GhostToken` guarantees no `borrow_mut` calls can
// occur afterwards, so the shared `&Ir` references reachable from a
// `RawIrRef<'ir>` can never alias with mutable references.
unsafe { std::mem::transmute::<GhostRoIrRef<'id, 'ir>, RawIrRef<'ir>>(this) }
}
}
#[repr(transparent)]
pub struct GhostRoCell<'id, T: ?Sized>(GhostCell<'id, T>);
impl<'id, T> From<GhostCell<'id, T>> for GhostRoCell<'id, T> {
fn from(value: GhostCell<'id, T>) -> Self {
Self(value)
}
}
impl<'id, T: ?Sized> From<&GhostCell<'id, T>> for &GhostRoCell<'id, T> {
fn from(value: &GhostCell<'id, T>) -> Self {
// SAFETY: `GhostRoCell` is `#[repr(transparent)]` over `GhostCell`
// TODO: document mutability
unsafe { std::mem::transmute(value) }
}
}
impl<'id, T: ?Sized> From<&T> for &GhostRoCell<'id, T> {
fn from(value: &T) -> Self {
// SAFETY: `GhostRoCell` is `#[repr(transparent)]` over `GhostCell`,
// which is `#[repr(transparent)]` over `T`
// TODO: document mutability
unsafe { std::mem::transmute(value) }
}
}
impl<'id, T: ?Sized> GhostRoCell<'id, T> {
pub fn borrow<'a>(&'a self, token: &'a GhostToken<'id>) -> &'a T {
self.0.borrow(token)
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub enum MaybeThunk {
Int(i64),
Float(f64),
Bool(bool),
Null,
Str(StringId),
Path(StringId),
Thunk(ThunkId),
Arg { layer: u8 },
Builtin(BuiltinId),
BuiltinConst(StringId),
Builtins,
ReplBinding(StringId),
ScopedImportBinding { slot_id: u32, sym: StringId },
}
pub trait Ref<'ir> {
type Ref<T>
where
T: 'ir;
}
pub trait RefExt<'ir>: Ref<'ir> {
type Ir;
type IrRef;
type MaybeThunkRef;
}
impl<'ir, T: Ref<'ir> + 'ir> RefExt<'ir> for T {
type Ir = Ir<'ir, Self>;
type IrRef = Self::Ref<Self::Ir>;
type MaybeThunkRef = Self::Ref<MaybeThunk>;
}
pub struct GhostRef<'id, 'ir>(PhantomData<&'ir GhostCell<'id, ()>>);
pub struct GhostRoRef<'id, 'ir>(PhantomData<&'ir GhostRoCell<'id, ()>>);
pub struct RawRef<'ir>(PhantomData<&'ir ()>);
impl<'id, 'ir> Ref<'ir> for GhostRef<'id, 'ir> {
type Ref<T: 'ir> = &'ir GhostCell<'id, T>;
}
impl<'id, 'ir> Ref<'ir> for GhostRoRef<'id, 'ir> {
type Ref<T: 'ir> = &'ir GhostRoCell<'id, T>;
}
impl<'ir> Ref<'ir> for RawRef<'ir> {
type Ref<T: 'ir> = &'ir T;
}
#[repr(C)]
#[derive(Debug)]
pub enum Ir<'ir, R: RefExt<'ir> + ?Sized + 'ir> {
Int(i64),
Float(f64),
Bool(bool),
Null,
Str(StringId),
Path(R::IrRef),
AttrSet {
stcs: HashMap<'ir, StringId, (R::MaybeThunkRef, TextRange)>,
dyns: Vec<'ir, (R::IrRef, R::MaybeThunkRef, TextRange)>,
},
List {
items: Vec<'ir, R::MaybeThunkRef>,
},
ConcatStrings {
parts: Vec<'ir, R::Ref<Ir<'ir, R>>>,
force_string: bool,
},
// OPs
UnOp {
rhs: R::IrRef,
kind: UnOpKind,
},
BinOp {
lhs: R::IrRef,
rhs: R::IrRef,
kind: BinOpKind,
},
HasAttr {
lhs: R::IrRef,
rhs: Vec<'ir, Attr<R::IrRef>>,
},
Select {
expr: R::IrRef,
attrpath: Vec<'ir, Attr<R::IrRef>>,
default: Option<R::IrRef>,
span: TextRange,
},
// Conditionals
If {
cond: R::IrRef,
consq: R::IrRef,
alter: R::IrRef,
},
Assert {
assertion: R::IrRef,
expr: R::IrRef,
assertion_raw: String,
span: TextRange,
},
WithLookup {
sym: StringId,
namespaces: Vec<'ir, R::MaybeThunkRef>,
},
// Function related
Func {
body: R::IrRef,
param: Option<Param<'ir>>,
thunks: Vec<'ir, (ThunkId, R::IrRef)>,
},
Arg {
layer: u8,
},
Call {
func: R::IrRef,
arg: R::MaybeThunkRef,
span: TextRange,
},
// Builtins
Builtins,
Builtin(BuiltinId),
BuiltinConst(StringId),
// Misc
TopLevel {
body: R::IrRef,
thunks: Vec<'ir, (ThunkId, R::IrRef)>,
},
MaybeThunk(R::MaybeThunkRef),
ReplBinding(StringId),
ScopedImportBinding {
sym: StringId,
slot_id: u32,
},
}
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ThunkId(pub usize);
/// Represents a key in an attribute path.
#[allow(unused)]
#[derive(Debug)]
pub enum Attr<Ref> {
/// A dynamic attribute key, which is an expression that must evaluate to a string.
/// Example: `attrs.${key}`
Dynamic(Ref, TextRange),
/// A static attribute key.
/// Example: `attrs.key`
Str(StringId, TextRange),
}
/// The kinds of binary operations supported in Nix.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum BinOpKind {
// Arithmetic
Add,
Sub,
Div,
Mul,
// Comparison
Eq,
Neq,
Lt,
Gt,
Leq,
Geq,
// Logical
And,
Or,
Impl,
// Set/String/Path operations
Con, // List concatenation (`++`)
Upd, // AttrSet update (`//`)
}
/// The kinds of unary operations.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum UnOpKind {
Neg, // Negation (`-`)
Not, // Logical not (`!`)
}
impl From<ast::UnaryOpKind> for UnOpKind {
fn from(value: ast::UnaryOpKind) -> Self {
match value {
ast::UnaryOpKind::Invert => UnOpKind::Not,
ast::UnaryOpKind::Negate => UnOpKind::Neg,
}
}
}
/// Describes the parameters of a function.
#[derive(Debug)]
pub struct Param<'ir> {
pub required: Vec<'ir, (StringId, TextRange)>,
pub optional: Vec<'ir, (StringId, TextRange)>,
pub ellipsis: bool,
}
pub fn new_global_env(
strings: &mut DefaultStringInterner,
) -> hashbrown::HashMap<StringId, MaybeThunk> {
let mut global_env = hashbrown::HashMap::new();
let builtins_sym = StringId(strings.get_or_intern("builtins"));
global_env.insert(builtins_sym, MaybeThunk::Builtins);
for (idx, &(name, _)) in BUILTINS.iter().enumerate() {
let id = BuiltinId::try_from(idx as u8).expect("infallible");
let name = StringId(strings.get_or_intern(name));
global_env.insert(name, MaybeThunk::Builtin(id));
}
let consts = [
(
"__currentSystem",
MaybeThunk::BuiltinConst(StringId(strings.get_or_intern("currentSystem"))),
),
("__langVersion", MaybeThunk::Int(6)),
(
"__nixVersion",
MaybeThunk::BuiltinConst(StringId(strings.get_or_intern("nixVersion"))),
),
(
"__storeDir",
MaybeThunk::BuiltinConst(StringId(strings.get_or_intern("storeDir"))),
),
(
"__nixPath",
MaybeThunk::BuiltinConst(StringId(strings.get_or_intern("nixPath"))),
),
("null", MaybeThunk::Null),
("true", MaybeThunk::Bool(true)),
("false", MaybeThunk::Bool(false)),
];
for (name, ir) in consts {
let name = StringId(strings.get_or_intern(name));
global_env.insert(name, ir);
}
global_env
}
@@ -1,7 +1,6 @@
use bumpalo::collections::{CollectIn, Vec}; use bumpalo::collections::{CollectIn, Vec};
use fix_builtins::BuiltinId;
use fix_common::Symbol;
use fix_error::{Error, Result, Source}; use fix_error::{Error, Result, Source};
use fix_lang::{BuiltinId, Symbol};
use hashbrown::HashSet; use hashbrown::HashSet;
use hashbrown::hash_map::Entry; use hashbrown::hash_map::Entry;
use rnix::TextRange; use rnix::TextRange;
@@ -39,12 +38,12 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>, T, E: std::fmt::Display>
} }
pub trait DowngradeContext<'id: 'ir, 'ir> { pub trait DowngradeContext<'id: 'ir, 'ir> {
fn new_expr(&self, expr: Ir<'ir, IrRef<'id, 'ir>>) -> IrRef<'id, 'ir>; fn new_expr(&self, expr: Ir<'ir, GhostRoRef<'id, 'ir>>) -> GhostRoIrRef<'id, 'ir>;
fn maybe_thunk(&mut self, ir: IrRef<'id, 'ir>) -> MaybeThunk; fn maybe_thunk(&mut self, ir: GhostRoIrRef<'id, 'ir>) -> GhostRoMaybeThunkRef<'id, 'ir>;
fn intern_string(&mut self, sym: impl AsRef<str>) -> StringId; fn intern_string(&mut self, sym: impl AsRef<str>) -> StringId;
fn resolve_sym(&self, id: StringId) -> Symbol<'_>; fn resolve_sym(&self, id: StringId) -> Symbol<'_>;
fn lookup(&self, sym: StringId, span: TextRange) -> Result<MaybeThunk>; fn lookup(&mut self, sym: StringId, span: TextRange) -> Result<GhostRoMaybeThunkRef<'id, 'ir>>;
fn get_current_source(&self) -> Source; fn get_current_source(&self) -> Source;
@@ -53,11 +52,11 @@ pub trait DowngradeContext<'id: 'ir, 'ir> {
F: FnOnce(&mut Self) -> R; F: FnOnce(&mut Self) -> R;
fn with_let_scope<F, R>(&mut self, bindings: &[StringId], f: F) -> Result<R> fn with_let_scope<F, R>(&mut self, bindings: &[StringId], f: F) -> Result<R>
where where
F: FnOnce(&mut Self) -> Result<(Vec<'ir, IrRef<'id, 'ir>>, R)>; F: FnOnce(&mut Self) -> Result<(Vec<'ir, GhostRoMaybeThunkRef<'id, 'ir>>, R)>;
fn with_with_scope<F, R>(&mut self, f: F) -> R fn with_with_scope<F, R>(&mut self, namespace: GhostRoMaybeThunkRef<'id, 'ir>, f: F) -> R
where where
F: FnOnce(&mut Self) -> R; F: FnOnce(&mut Self) -> R;
fn with_thunk_scope<F, R>(&mut self, f: F) -> (R, Vec<'ir, (ThunkId, IrRef<'id, 'ir>)>) fn with_thunk_scope<F, R>(&mut self, f: F) -> (R, Vec<'ir, (ThunkId, GhostRoIrRef<'id, 'ir>)>)
where where
F: FnOnce(&mut Self) -> R; F: FnOnce(&mut Self) -> R;
@@ -65,11 +64,11 @@ pub trait DowngradeContext<'id: 'ir, 'ir> {
} }
pub trait Downgrade<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> { pub trait Downgrade<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>>; fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>>;
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for Expr { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for Expr {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
use Expr::*; use Expr::*;
match self { match self {
Apply(apply) => apply.downgrade(ctx), Apply(apply) => apply.downgrade(ctx),
@@ -98,7 +97,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
AttrSet(attrs) => attrs.downgrade(ctx), AttrSet(attrs) => attrs.downgrade(ctx),
UnaryOp(op) => op.downgrade(ctx), UnaryOp(op) => op.downgrade(ctx),
Ident(ident) => ident.downgrade(ctx), Ident(ident) => ident.downgrade(ctx),
CurPos(curpos) => Ok(ctx.new_expr(Ir::CurPos(curpos.syntax().text_range()))), CurPos(curpos) => curpos.downgrade(ctx),
With(with) => with.downgrade(ctx), With(with) => with.downgrade(ctx),
HasAttr(has) => has.downgrade(ctx), HasAttr(has) => has.downgrade(ctx),
Paren(paren) => paren Paren(paren) => paren
@@ -114,7 +113,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Assert { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Assert {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let assertion = self.condition().require(ctx, span)?; let assertion = self.condition().require(ctx, span)?;
let assertion_raw = assertion.to_string(); let assertion_raw = assertion.to_string();
@@ -130,7 +129,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::IfElse { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::IfElse {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let cond = self.condition().require(ctx, span)?.downgrade(ctx)?; let cond = self.condition().require(ctx, span)?.downgrade(ctx)?;
let consq = self.body().require(ctx, span)?.downgrade(ctx)?; let consq = self.body().require(ctx, span)?.downgrade(ctx)?;
@@ -142,7 +141,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
macro_rules! path { macro_rules! path {
($ty:ident) => { ($ty:ident) => {
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::$ty { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::$ty {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
downgrade_path(self.parts(), ctx) downgrade_path(self.parts(), ctx)
} }
} }
@@ -152,7 +151,7 @@ path!(PathAbs);
path!(PathRel); path!(PathRel);
path!(PathHome); path!(PathHome);
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::PathSearch { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::PathSearch {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let path = { let path = {
let temp = self.content().require(ctx, span)?; let temp = self.content().require(ctx, span)?;
@@ -180,7 +179,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Str { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Str {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let normalized = self.normalized_parts(); let normalized = self.normalized_parts();
let is_single_literal = normalized.len() == 1 let is_single_literal = normalized.len() == 1
&& matches!(normalized.first(), Some(ast::InterpolPart::Literal(_))); && matches!(normalized.first(), Some(ast::InterpolPart::Literal(_)));
@@ -210,7 +209,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Literal { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Literal {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let expr = match self.kind() { let expr = match self.kind() {
ast::LiteralKind::Integer(int) => Ir::Int(int.value().require(ctx, span)?), ast::LiteralKind::Integer(int) => Ir::Int(int.value().require(ctx, span)?),
@@ -225,16 +224,73 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Ident { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Ident {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let text = self.ident_token().require(ctx, span)?.to_string(); let text = self.ident_token().require(ctx, span)?.to_string();
let sym = ctx.intern_string(text); let sym = ctx.intern_string(text);
ctx.lookup(sym, span).map(|thunk| thunk.to_ir(ctx)) ctx.lookup(sym, span)
.map(|thunk| ctx.new_expr(Ir::MaybeThunk(thunk)))
}
}
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::CurPos {
fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
fn byte_offset_to_line_col(content: &str, offset: usize) -> (u32, u32) {
let mut line = 1u32;
let mut col = 1u32;
for (idx, ch) in content.char_indices() {
if idx >= offset {
break;
}
if ch == '\n' {
line += 1;
col = 1;
} else {
col += 1;
}
}
(line, col)
}
let span = self.syntax().text_range();
let source = ctx.get_current_source();
let (line, column) = byte_offset_to_line_col(&source.src, span.start().into());
let file_sym = ctx.intern_string("file");
let line_sym = ctx.intern_string("line");
let column_sym = ctx.intern_string("column");
let file: GhostRoMaybeThunkRef = ctx
.bump()
.alloc(GhostCell::new(MaybeThunk::Str(ctx.intern_string(source.get_name()))).into());
let line = ctx
.bump()
.alloc(GhostCell::new(MaybeThunk::Int(i64::from(line))).into());
let column = ctx
.bump()
.alloc(GhostCell::new(MaybeThunk::Int(i64::from(column))).into());
let map = {
let mut map = HashMap::new_in(ctx.bump());
map.insert(file_sym, (file, TextRange::default()));
map.insert(line_sym, (line, TextRange::default()));
map.insert(column_sym, (column, TextRange::default()));
map
};
Ok(ctx.new_expr(Ir::AttrSet {
stcs: map,
dyns: Vec::new_in(ctx.bump()),
}))
} }
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::AttrSet { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::AttrSet {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let rec = self.rec_token().is_some(); let rec = self.rec_token().is_some();
if !rec { if !rec {
let attrs = downgrade_attrs(self, ctx)?; let attrs = downgrade_attrs(self, ctx)?;
@@ -250,7 +306,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::List { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::List {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let bump = ctx.bump(); let bump = ctx.bump();
let items = self let items = self
.items() .items()
@@ -264,17 +320,52 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::BinOp { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::BinOp {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
use BinOpKind::*;
use ast::BinOpKind as Kind;
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let lhs = self.lhs().require(ctx, span)?.downgrade(ctx)?; let lhs = self.lhs().require(ctx, span)?.downgrade(ctx)?;
let rhs = self.rhs().require(ctx, span)?.downgrade(ctx)?; let rhs = self.rhs().require(ctx, span)?.downgrade(ctx)?;
let kind = self.operator().require(ctx, span)?.into(); let kind = match self.operator().require(ctx, span)? {
Kind::Concat => Con,
Kind::Update => Upd,
Kind::Add => Add,
Kind::Sub => Sub,
Kind::Mul => Mul,
Kind::Div => Div,
Kind::And => And,
Kind::Equal => Eq,
Kind::Implication => Impl,
Kind::Less => Lt,
Kind::LessOrEq => Leq,
Kind::More => Gt,
Kind::MoreOrEq => Geq,
Kind::NotEqual => Neq,
Kind::Or => Or,
Kind::PipeLeft => {
let arg = ctx.maybe_thunk(rhs);
return Ok(ctx.new_expr(Ir::Call {
func: lhs,
arg,
span,
}));
}
Kind::PipeRight => {
let arg = ctx.maybe_thunk(lhs);
return Ok(ctx.new_expr(Ir::Call {
func: rhs,
arg,
span,
}));
}
};
Ok(ctx.new_expr(Ir::BinOp { lhs, rhs, kind })) Ok(ctx.new_expr(Ir::BinOp { lhs, rhs, kind }))
} }
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::HasAttr { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::HasAttr {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let lhs = self.expr().require(ctx, span)?.downgrade(ctx)?; let lhs = self.expr().require(ctx, span)?.downgrade(ctx)?;
let rhs = downgrade_attrpath(self.attrpath().require(ctx, span)?, ctx)?; let rhs = downgrade_attrpath(self.attrpath().require(ctx, span)?, ctx)?;
@@ -283,7 +374,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::UnaryOp { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::UnaryOp {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let rhs = self.expr().require(ctx, span)?.downgrade(ctx)?; let rhs = self.expr().require(ctx, span)?.downgrade(ctx)?;
let kind = self.operator().require(ctx, span)?.into(); let kind = self.operator().require(ctx, span)?.into();
@@ -292,7 +383,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Select { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Select {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let expr = self.expr().require(ctx, span)?.downgrade(ctx)?; let expr = self.expr().require(ctx, span)?.downgrade(ctx)?;
let attrpath = downgrade_attrpath(self.attrpath().require(ctx, span)?, ctx)?; let attrpath = downgrade_attrpath(self.attrpath().require(ctx, span)?, ctx)?;
@@ -311,7 +402,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::LegacyLet { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::LegacyLet {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let entries: Vec<'ir, _> = self.entries().collect_in(ctx.bump()); let entries: Vec<'ir, _> = self.entries().collect_in(ctx.bump());
let attrset_expr = downgrade_let_bindings(entries, ctx, |ctx, binding_keys| { let attrset_expr = downgrade_let_bindings(entries, ctx, |ctx, binding_keys| {
@@ -340,7 +431,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::LetIn { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::LetIn {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let entries: Vec<'ir, _> = self.entries().collect_in(ctx.bump()); let entries: Vec<'ir, _> = self.entries().collect_in(ctx.bump());
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let body_expr = self.body().require(ctx, span)?; let body_expr = self.body().require(ctx, span)?;
@@ -350,33 +441,25 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::With { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::With {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let namespace = self.namespace().require(ctx, span)?.downgrade(ctx)?; let namespace = self.namespace().require(ctx, span)?.downgrade(ctx)?;
let namespace = ctx.maybe_thunk(namespace); let namespace = ctx.maybe_thunk(namespace);
let body_expr = self.body().require(ctx, span)?; let body_expr = self.body().require(ctx, span)?;
let (body, thunks) = ctx.with_with_scope(namespace, |ctx| body_expr.downgrade(ctx))
ctx.with_thunk_scope(|ctx| ctx.with_with_scope(|ctx| body_expr.downgrade(ctx)));
let body = body?;
Ok(ctx.new_expr(Ir::With {
namespace,
body,
thunks,
}))
} }
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Lambda { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Lambda {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let raw_param = self.param().require(ctx, span)?; let raw_param = self.param().require(ctx, span)?;
let body_ast = self.body().require(ctx, span)?; let body_ast = self.body().require(ctx, span)?;
struct Ret<'id, 'ir> { struct Ret<'id, 'ir> {
param: Option<Param<'ir>>, param: Option<Param<'ir>>,
body: IrRef<'id, 'ir>, body: GhostRoIrRef<'id, 'ir>,
} }
let (ret, thunks) = ctx.with_thunk_scope(|ctx| { let (ret, thunks) = ctx.with_thunk_scope(|ctx| {
@@ -433,7 +516,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Apply { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Apply {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let func = self.lambda().require(ctx, span)?.downgrade(ctx)?; let func = self.lambda().require(ctx, span)?.downgrade(ctx)?;
let arg = self.argument().require(ctx, span)?.downgrade(ctx)?; let arg = self.argument().require(ctx, span)?.downgrade(ctx)?;
@@ -835,8 +918,15 @@ fn make_attrpath_value_entry<'ir>(path: Vec<'ir, ast::Attr>, value: ast::Expr) -
} }
struct FinalizedAttrSet<'id, 'ir> { struct FinalizedAttrSet<'id, 'ir> {
stcs: HashMap<'ir, StringId, (MaybeThunk, TextRange)>, stcs: HashMap<'ir, StringId, (GhostRoMaybeThunkRef<'id, 'ir>, TextRange)>,
dyns: Vec<'ir, (IrRef<'id, 'ir>, MaybeThunk, TextRange)>, dyns: Vec<
'ir,
(
GhostRoIrRef<'id, 'ir>,
GhostRoMaybeThunkRef<'id, 'ir>,
TextRange,
),
>,
} }
fn downgrade_attrs<'id, 'ir>( fn downgrade_attrs<'id, 'ir>(
@@ -851,7 +941,7 @@ fn downgrade_attrs<'id, 'ir>(
fn downgrade_attr<'id, 'ir>( fn downgrade_attr<'id, 'ir>(
attr: ast::Attr, attr: ast::Attr,
ctx: &mut impl DowngradeContext<'id, 'ir>, ctx: &mut impl DowngradeContext<'id, 'ir>,
) -> Result<Attr<IrRef<'id, 'ir>>> { ) -> Result<Attr<GhostRoIrRef<'id, 'ir>>> {
use ast::Attr::*; use ast::Attr::*;
use ast::InterpolPart::*; use ast::InterpolPart::*;
match attr { match attr {
@@ -912,7 +1002,7 @@ fn downgrade_attr<'id, 'ir>(
fn downgrade_attrpath<'id, 'ir>( fn downgrade_attrpath<'id, 'ir>(
attrpath: ast::Attrpath, attrpath: ast::Attrpath,
ctx: &mut impl DowngradeContext<'id, 'ir>, ctx: &mut impl DowngradeContext<'id, 'ir>,
) -> Result<Vec<'ir, Attr<IrRef<'id, 'ir>>>> { ) -> Result<Vec<'ir, Attr<GhostRoIrRef<'id, 'ir>>>> {
let bump = ctx.bump(); let bump = ctx.bump();
attrpath attrpath
.attrs() .attrs()
@@ -921,7 +1011,7 @@ fn downgrade_attrpath<'id, 'ir>(
} }
struct PatternBindings<'id, 'ir> { struct PatternBindings<'id, 'ir> {
body: IrRef<'id, 'ir>, body: GhostRoIrRef<'id, 'ir>,
required: Vec<'ir, (StringId, TextRange)>, required: Vec<'ir, (StringId, TextRange)>,
optional: Vec<'ir, (StringId, TextRange)>, optional: Vec<'ir, (StringId, TextRange)>,
} }
@@ -930,7 +1020,7 @@ fn downgrade_pattern_bindings<'id, 'ir, Ctx>(
pat_entries: impl Iterator<Item = ast::PatEntry>, pat_entries: impl Iterator<Item = ast::PatEntry>,
alias: Option<StringId>, alias: Option<StringId>,
ctx: &mut Ctx, ctx: &mut Ctx,
body_fn: impl FnOnce(&mut Ctx, &[StringId]) -> Result<IrRef<'id, 'ir>>, body_fn: impl FnOnce(&mut Ctx, &[StringId]) -> Result<GhostRoIrRef<'id, 'ir>>,
) -> Result<PatternBindings<'id, 'ir>> ) -> Result<PatternBindings<'id, 'ir>>
where where
Ctx: DowngradeContext<'id, 'ir>, Ctx: DowngradeContext<'id, 'ir>,
@@ -989,6 +1079,7 @@ where
} }
let arg = ctx.new_expr(Ir::Arg { layer: 0 }); let arg = ctx.new_expr(Ir::Arg { layer: 0 });
let arg_thunk = ctx.maybe_thunk(arg);
ctx.with_let_scope(&keys, |ctx| { ctx.with_let_scope(&keys, |ctx| {
let vals = params let vals = params
.into_iter() .into_iter()
@@ -1000,21 +1091,16 @@ where
span, span,
} = param; } = param;
let default = default.map(|default| default.downgrade(ctx)).transpose()?; let default = default.map(|default| default.downgrade(ctx)).transpose()?;
// let default = if let Some(default) = default {
// let default = default.clone().downgrade(ctx)?;
// Some(ctx.maybe_thunk(default))
// } else {
// None
// };
Ok(ctx.new_expr(Ir::Select { let expr = ctx.new_expr(Ir::Select {
expr: arg, expr: arg,
attrpath: Vec::from_iter_in([Attr::Str(sym, sym_span)], bump), attrpath: Vec::from_iter_in([Attr::Str(sym, sym_span)], bump),
default, default,
span, span,
})) });
Ok(ctx.maybe_thunk(expr))
}) })
.chain(alias.into_iter().map(|_| Ok(arg))) .chain(alias.into_iter().map(|_| Ok(arg_thunk)))
.collect_in::<Result<_>>(bump)?; .collect_in::<Result<_>>(bump)?;
let body = body_fn(ctx, &keys)?; let body = body_fn(ctx, &keys)?;
@@ -1034,10 +1120,10 @@ fn downgrade_let_bindings<'id, 'ir, Ctx, F>(
entries: Vec<'ir, ast::Entry>, entries: Vec<'ir, ast::Entry>,
ctx: &mut Ctx, ctx: &mut Ctx,
body_fn: F, body_fn: F,
) -> Result<IrRef<'id, 'ir>> ) -> Result<GhostRoIrRef<'id, 'ir>>
where where
Ctx: DowngradeContext<'id, 'ir>, Ctx: DowngradeContext<'id, 'ir>,
F: FnOnce(&mut Ctx, &[StringId]) -> Result<IrRef<'id, 'ir>>, F: FnOnce(&mut Ctx, &[StringId]) -> Result<GhostRoIrRef<'id, 'ir>>,
{ {
downgrade_rec_attrs_impl::<_, _, false>(entries, ctx, |ctx, binding_keys, _dyns| { downgrade_rec_attrs_impl::<_, _, false>(entries, ctx, |ctx, binding_keys, _dyns| {
body_fn(ctx, binding_keys) body_fn(ctx, binding_keys)
@@ -1047,7 +1133,7 @@ where
fn downgrade_rec_bindings<'id, 'ir, Ctx>( fn downgrade_rec_bindings<'id, 'ir, Ctx>(
entries: Vec<'ir, ast::Entry>, entries: Vec<'ir, ast::Entry>,
ctx: &mut Ctx, ctx: &mut Ctx,
) -> Result<IrRef<'id, 'ir>> ) -> Result<GhostRoIrRef<'id, 'ir>>
where where
Ctx: DowngradeContext<'id, 'ir>, Ctx: DowngradeContext<'id, 'ir>,
{ {
@@ -1068,14 +1154,18 @@ fn downgrade_rec_attrs_impl<'id, 'ir, Ctx, F, const ALLOW_DYN: bool>(
entries: Vec<'ir, ast::Entry>, entries: Vec<'ir, ast::Entry>,
ctx: &mut Ctx, ctx: &mut Ctx,
body_fn: F, body_fn: F,
) -> Result<IrRef<'id, 'ir>> ) -> Result<GhostRoIrRef<'id, 'ir>>
where where
Ctx: DowngradeContext<'id, 'ir>, Ctx: DowngradeContext<'id, 'ir>,
F: FnOnce( F: FnOnce(
&mut Ctx, &mut Ctx,
&[StringId], &[StringId],
&[(IrRef<'id, 'ir>, MaybeThunk, TextRange)], &[(
) -> Result<IrRef<'id, 'ir>>, GhostRoIrRef<'id, 'ir>,
GhostRoMaybeThunkRef<'id, 'ir>,
TextRange,
)],
) -> Result<GhostRoIrRef<'id, 'ir>>,
{ {
let mut pending = PendingAttrSet::new_in(ctx.bump()); let mut pending = PendingAttrSet::new_in(ctx.bump());
pending.collect_entries(entries.iter().cloned(), ctx)?; pending.collect_entries(entries.iter().cloned(), ctx)?;
@@ -1090,7 +1180,7 @@ where
let vals = { let vals = {
let mut temp = Vec::with_capacity_in(keys.len(), ctx.bump()); let mut temp = Vec::with_capacity_in(keys.len(), ctx.bump());
for sym in &keys { for sym in &keys {
temp.push(finalized.stcs.get(sym).expect("WTF").0.to_ir(ctx)); temp.push(finalized.stcs.get(sym).expect("WTF").0);
} }
temp temp
}; };
@@ -1101,7 +1191,7 @@ where
fn collect_inherit_lookups<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>>( fn collect_inherit_lookups<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>>(
entries: &[ast::Entry], entries: &[ast::Entry],
ctx: &mut Ctx, ctx: &mut Ctx,
) -> Result<HashMap<'ir, StringId, (MaybeThunk, TextRange)>> { ) -> Result<HashMap<'ir, StringId, (GhostRoMaybeThunkRef<'id, 'ir>, TextRange)>> {
let mut inherit_lookups = HashMap::new_in(ctx.bump()); let mut inherit_lookups = HashMap::new_in(ctx.bump());
for entry in entries { for entry in entries {
if let ast::Entry::Inherit(inherit) = entry if let ast::Entry::Inherit(inherit) = entry
@@ -1141,7 +1231,7 @@ fn collect_binding_syms<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>, const AL
fn finalize_pending_set<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW_DYN: bool>( fn finalize_pending_set<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW_DYN: bool>(
pending: PendingAttrSet, pending: PendingAttrSet,
inherit_lookups: &HashMap<StringId, (MaybeThunk, TextRange)>, inherit_lookups: &HashMap<StringId, (GhostRoMaybeThunkRef<'id, 'ir>, TextRange)>,
ctx: &mut Ctx, ctx: &mut Ctx,
) -> Result<FinalizedAttrSet<'id, 'ir>> { ) -> Result<FinalizedAttrSet<'id, 'ir>> {
let mut stcs = HashMap::new_in(ctx.bump()); let mut stcs = HashMap::new_in(ctx.bump());
@@ -1169,9 +1259,9 @@ fn finalize_pending_set<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW_D
fn finalize_pending_value<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW_DYN: bool>( fn finalize_pending_value<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW_DYN: bool>(
value: PendingValue, value: PendingValue,
inherit_lookups: &HashMap<StringId, (MaybeThunk, TextRange)>, inherit_lookups: &HashMap<StringId, (GhostRoMaybeThunkRef<'id, 'ir>, TextRange)>,
ctx: &mut Ctx, ctx: &mut Ctx,
) -> Result<IrRef<'id, 'ir>> { ) -> Result<GhostRoIrRef<'id, 'ir>> {
match value { match value {
PendingValue::Expr(expr) => expr.downgrade(ctx), PendingValue::Expr(expr) => expr.downgrade(ctx),
PendingValue::InheritFrom(from_expr, sym, span) => { PendingValue::InheritFrom(from_expr, sym, span) => {
@@ -1185,9 +1275,10 @@ fn finalize_pending_value<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW
} }
PendingValue::InheritScope(sym, span) => { PendingValue::InheritScope(sym, span) => {
if let Some(&(expr, _)) = inherit_lookups.get(&sym) { if let Some(&(expr, _)) = inherit_lookups.get(&sym) {
Ok(expr.to_ir(ctx)) Ok(ctx.new_expr(Ir::MaybeThunk(expr)))
} else { } else {
ctx.lookup(sym, span).map(|val| val.to_ir(ctx)) ctx.lookup(sym, span)
.map(|val| ctx.new_expr(Ir::MaybeThunk(val)))
} }
} }
PendingValue::Set(set) => { PendingValue::Set(set) => {
@@ -1210,7 +1301,7 @@ fn finalize_pending_value<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW
fn downgrade_path<'id, 'ir>( fn downgrade_path<'id, 'ir>(
parts: impl IntoIterator<Item = ast::InterpolPart<ast::PathContent>>, parts: impl IntoIterator<Item = ast::InterpolPart<ast::PathContent>>,
ctx: &mut impl DowngradeContext<'id, 'ir>, ctx: &mut impl DowngradeContext<'id, 'ir>,
) -> Result<IrRef<'id, 'ir>> { ) -> Result<GhostRoIrRef<'id, 'ir>> {
let bump = ctx.bump(); let bump = ctx.bump();
let parts = parts let parts = parts
.into_iter() .into_iter()
+229 -340
View File
@@ -1,16 +1,14 @@
use std::ops::Deref; use fix_bytecode::{Const, InstructionPtr, Op, OperandType, PrimOpPhase};
use fix_lang::{BUILTINS, StringId};
use fix_builtins::BuiltinId;
use fix_common::StringId;
use fix_ir::{Attr, BinOpKind, Ir, MaybeThunk, Param, RawIrRef, ThunkId, UnOpKind};
use hashbrown::HashMap; use hashbrown::HashMap;
use num_enum::TryFromPrimitive;
use rnix::TextRange; use rnix::TextRange;
use string_interner::Symbol as _; use string_interner::Symbol as _;
pub mod disassembler; mod context;
pub mod ir;
pub struct InstructionPtr(pub usize); pub use context::{CodeState, ExtraScope};
pub use fix_bytecode::disassembler;
pub use ir::{Attr, BinOpKind, Ir, MaybeThunk, Param, RawIrRef, ThunkId, UnOpKind};
pub trait BytecodeContext { pub trait BytecodeContext {
fn intern_string(&mut self, s: &str) -> StringId; fn intern_string(&mut self, s: &str) -> StringId;
@@ -18,86 +16,11 @@ pub trait BytecodeContext {
fn get_code(&self) -> &[u8]; fn get_code(&self) -> &[u8];
fn get_code_mut(&mut self) -> &mut Vec<u8>; fn get_code_mut(&mut self) -> &mut Vec<u8>;
fn add_constant(&mut self, val: Const) -> u32; fn add_constant(&mut self, val: Const) -> u32;
} fn current_source_dir(&mut self) -> StringId;
#[repr(u8)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
#[allow(clippy::enum_variant_names)]
pub enum Op {
PushSmi,
PushBigInt,
PushFloat,
PushString,
PushNull,
PushTrue,
PushFalse,
LoadLocal,
LoadOuter,
StoreLocal,
AllocLocals,
MakeThunk,
MakeClosure,
MakePatternClosure,
Call,
MakeAttrs,
MakeEmptyAttrs,
SelectStatic,
SelectDynamic,
JumpIfSelectSucceeded,
HasAttr,
MakeList,
MakeEmptyList,
OpAdd,
OpSub,
OpMul,
OpDiv,
OpEq,
OpNeq,
OpLt,
OpGt,
OpLeq,
OpGeq,
OpConcat,
OpUpdate,
OpNeg,
OpNot,
JumpIfFalse,
JumpIfTrue,
Jump,
ConcatStrings,
ResolvePath,
Assert,
PushWith,
PopWith,
LookupWith,
PrepareWith,
LoadBuiltins,
LoadBuiltin,
MkPos,
LoadReplBinding,
LoadScopedBinding,
Return,
Illegal,
} }
struct ScopeInfo { struct ScopeInfo {
depth: u16, depth: u8,
thunk_map: HashMap<ThunkId, u32>, thunk_map: HashMap<ThunkId, u32>,
} }
@@ -106,36 +29,14 @@ struct BytecodeEmitter<'a, Ctx: BytecodeContext> {
scope_stack: Vec<ScopeInfo>, scope_stack: Vec<ScopeInfo>,
} }
#[repr(u8)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
pub enum OperandType {
Const,
Local,
Builtins,
BigInt,
}
pub enum Const {
Smi(i32),
Float(f64),
Bool(bool),
String(StringId),
PrimOp { id: BuiltinId, arity: u8 },
Null,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
pub enum AttrKeyType {
Static,
Dynamic,
}
pub enum InlineOperand { pub enum InlineOperand {
Const(Const), Const(Const),
Local { layer: u16, local: u32 },
Builtins,
BigInt(i64), BigInt(i64),
Local { layer: u8, local: u32 },
BuiltinConst(StringId),
Builtins,
ReplBinding(StringId),
ScopedImportBinding { id: StringId, slot_id: u32 },
} }
pub fn compile_bytecode(ir: RawIrRef<'_>, ctx: &mut impl BytecodeContext) -> InstructionPtr { pub fn compile_bytecode(ir: RawIrRef<'_>, ctx: &mut impl BytecodeContext) -> InstructionPtr {
@@ -154,12 +55,12 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
} }
#[must_use] #[must_use]
fn inline_maybe_thunk(&mut self, val: MaybeThunk) -> InlineOperand { fn inline_maybe_thunk(&self, val: &MaybeThunk) -> InlineOperand {
use MaybeThunk::*; use MaybeThunk::*;
match val { match *val {
Int(x) => { Int(x) => {
if x <= i32::MAX as i64 { if let Ok(x) = x.try_into() {
InlineOperand::Const(Const::Smi(x as i32)) InlineOperand::Const(Const::Smi(x))
} else { } else {
InlineOperand::BigInt(x) InlineOperand::BigInt(x)
} }
@@ -168,38 +69,63 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
Bool(b) => InlineOperand::Const(Const::Bool(b)), Bool(b) => InlineOperand::Const(Const::Bool(b)),
Null => InlineOperand::Const(Const::Null), Null => InlineOperand::Const(Const::Null),
Str(id) => InlineOperand::Const(Const::String(id)), Str(id) => InlineOperand::Const(Const::String(id)),
Path(id) => InlineOperand::Const(Const::String(id)),
Thunk(id) => { Thunk(id) => {
let (layer, local) = self.resolve_thunk(id); let (layer, local) = self.resolve_thunk(id);
InlineOperand::Local { layer, local } InlineOperand::Local { layer, local }
} }
Arg { layer } => InlineOperand::Local { Arg { layer } => InlineOperand::Local { layer, local: 0 },
layer: layer.try_into().expect("scope too deep!"), Builtin(id) => {
local: 0, let (_, arity) = BUILTINS[id as usize];
}, InlineOperand::Const(Const::PrimOp {
_ => todo!(), id,
arity,
dispatch_ip: PrimOpPhase::entry_for_builtin(id).ip(),
})
}
BuiltinConst(id) => InlineOperand::BuiltinConst(id),
Builtins => InlineOperand::Builtins,
ReplBinding(id) => InlineOperand::ReplBinding(id),
ScopedImportBinding { slot_id, sym: id } => {
InlineOperand::ScopedImportBinding { slot_id, id }
}
} }
} }
fn emit_maybe_thunk(&mut self, val: MaybeThunk) { fn emit_maybe_thunk(&mut self, val: &MaybeThunk) {
use InlineOperand::*;
let operand = self.inline_maybe_thunk(val); let operand = self.inline_maybe_thunk(val);
match operand { match operand {
InlineOperand::Const(val) => { Const(val) => {
let idx = self.ctx.add_constant(val); let idx = self.ctx.add_constant(val);
self.emit_u8(OperandType::Const as u8); self.emit_u8(OperandType::Const as u8);
self.emit_u32(idx); self.emit_u32(idx);
} }
InlineOperand::Local { layer, local } => { BigInt(val) => {
self.emit_u8(OperandType::Local as u8);
self.emit_u8(layer as u8);
self.emit_u32(local);
}
InlineOperand::Builtins => {
self.emit_u8(OperandType::Builtins as u8);
}
InlineOperand::BigInt(val) => {
self.emit_u8(OperandType::BigInt as u8); self.emit_u8(OperandType::BigInt as u8);
self.emit_i64(val); self.emit_i64(val);
} }
Local { layer, local } => {
self.emit_u8(OperandType::Local as u8);
self.emit_u8(layer);
self.emit_u32(local);
}
BuiltinConst(id) => {
self.emit_u8(OperandType::BuiltinConst as u8);
self.emit_str_id(id);
}
Builtins => {
self.emit_u8(OperandType::Builtins as u8);
}
ReplBinding(id) => {
self.emit_u8(OperandType::ReplBinding as u8);
self.emit_str_id(id);
}
ScopedImportBinding { id, slot_id } => {
self.emit_u8(OperandType::ScopedImportBinding as u8);
self.emit_u32(slot_id);
self.emit_str_id(id);
}
} }
} }
@@ -208,6 +134,11 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
self.ctx.get_code_mut().push(op as u8); self.ctx.get_code_mut().push(op as u8);
} }
#[inline]
fn emit_bool(&mut self, val: bool) {
self.emit_u8(u8::from(val));
}
#[inline] #[inline]
fn emit_u8(&mut self, val: u8) { fn emit_u8(&mut self, val: u8) {
self.ctx.get_code_mut().push(val); self.ctx.get_code_mut().push(val);
@@ -279,11 +210,11 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
.extend_from_slice(&(id.0.to_usize() as u32).to_le_bytes()); .extend_from_slice(&(id.0.to_usize() as u32).to_le_bytes());
} }
fn current_depth(&self) -> u16 { fn current_depth(&self) -> u8 {
self.scope_stack.last().map_or(0, |s| s.depth) self.scope_stack.last().map_or(0, |s| s.depth)
} }
fn resolve_thunk(&self, id: ThunkId) -> (u16, u32) { fn resolve_thunk(&self, id: ThunkId) -> (u8, u32) {
for scope in self.scope_stack.iter().rev() { for scope in self.scope_stack.iter().rev() {
if let Some(&local_idx) = scope.thunk_map.get(&id) { if let Some(&local_idx) = scope.thunk_map.get(&id) {
let layer = self.current_depth() - scope.depth; let layer = self.current_depth() - scope.depth;
@@ -293,111 +224,19 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
panic!("ThunkId {:?} not found in any scope", id); panic!("ThunkId {:?} not found in any scope", id);
} }
fn emit_load(&mut self, layer: u16, local: u32) { fn emit_load(&mut self, layer: u8, local: u32) {
if layer == 0 { if layer == 0 {
self.emit_op(Op::LoadLocal); self.emit_op(Op::LoadLocal);
self.emit_u32(local); self.emit_u32(local);
} else { } else {
self.emit_op(Op::LoadOuter); self.emit_op(Op::LoadOuter);
self.emit_u8(layer as u8); self.emit_u8(layer);
self.emit_u32(local); self.emit_u32(local);
} }
} }
fn count_with_thunks(&self, ir: RawIrRef<'_>) -> usize {
match ir.deref() {
Ir::With { thunks, body, .. } => thunks.len() + self.count_with_thunks(*body),
Ir::TopLevel { thunks, body } => thunks.len() + self.count_with_thunks(*body),
Ir::If { cond, consq, alter } => {
self.count_with_thunks(*cond)
+ self.count_with_thunks(*consq)
+ self.count_with_thunks(*alter)
}
Ir::BinOp { lhs, rhs, .. } => {
self.count_with_thunks(*lhs) + self.count_with_thunks(*rhs)
}
Ir::UnOp { rhs, .. } => self.count_with_thunks(*rhs),
Ir::Call { func, .. } => self.count_with_thunks(*func),
Ir::Assert {
assertion, expr, ..
} => self.count_with_thunks(*assertion) + self.count_with_thunks(*expr),
Ir::Select { expr, .. } => self.count_with_thunks(*expr),
Ir::HasAttr { lhs, .. } => self.count_with_thunks(*lhs),
Ir::ConcatStrings { parts, .. } => {
parts.iter().map(|p| self.count_with_thunks(*p)).sum()
}
_ => 0,
}
}
fn collect_all_thunks<'ir>(
&self,
own_thunks: &[(ThunkId, RawIrRef<'ir>)],
body: RawIrRef<'ir>,
) -> Vec<(ThunkId, RawIrRef<'ir>)> {
let mut all = Vec::from(own_thunks);
self.collect_with_thunks_recursive(body, &mut all);
let mut i = 0;
while i < all.len() {
let thunk_body = all[i].1;
self.collect_with_thunks_recursive(thunk_body, &mut all);
i += 1;
}
all
}
fn collect_with_thunks_recursive<'ir>(
&self,
ir: RawIrRef<'ir>,
out: &mut Vec<(ThunkId, RawIrRef<'ir>)>,
) {
match ir.deref() {
Ir::With { thunks, body, .. } => {
for &(id, inner) in thunks.iter() {
out.push((id, inner));
}
self.collect_with_thunks_recursive(*body, out);
}
Ir::TopLevel { thunks, body } => {
for &(id, inner) in thunks.iter() {
out.push((id, inner));
}
self.collect_with_thunks_recursive(*body, out);
}
Ir::If { cond, consq, alter } => {
self.collect_with_thunks_recursive(*cond, out);
self.collect_with_thunks_recursive(*consq, out);
self.collect_with_thunks_recursive(*alter, out);
}
Ir::BinOp { lhs, rhs, .. } => {
self.collect_with_thunks_recursive(*lhs, out);
self.collect_with_thunks_recursive(*rhs, out);
}
Ir::UnOp { rhs, .. } => self.collect_with_thunks_recursive(*rhs, out),
Ir::Call { func, .. } => {
self.collect_with_thunks_recursive(*func, out);
}
Ir::Assert {
assertion, expr, ..
} => {
self.collect_with_thunks_recursive(*assertion, out);
self.collect_with_thunks_recursive(*expr, out);
}
Ir::Select { expr, .. } => {
self.collect_with_thunks_recursive(*expr, out);
}
Ir::HasAttr { lhs, .. } => self.collect_with_thunks_recursive(*lhs, out),
Ir::ConcatStrings { parts, .. } => {
for p in parts.iter() {
self.collect_with_thunks_recursive(*p, out);
}
}
_ => (),
}
}
fn push_scope(&mut self, has_arg: bool, thunk_ids: &[ThunkId]) { fn push_scope(&mut self, has_arg: bool, thunk_ids: &[ThunkId]) {
let depth = self.scope_stack.len() as u16; let depth = self.scope_stack.len().try_into().expect("scope too deep!");
let thunk_base = if has_arg { 1u32 } else { 0u32 }; let thunk_base = if has_arg { 1u32 } else { 0u32 };
let thunk_map = thunk_ids let thunk_map = thunk_ids
.iter() .iter()
@@ -412,19 +251,13 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
} }
fn emit_toplevel(&mut self, ir: RawIrRef<'_>) { fn emit_toplevel(&mut self, ir: RawIrRef<'_>) {
match ir.deref() { match ir {
&Ir::TopLevel { body, ref thunks } => { &Ir::TopLevel { body, ref thunks } => {
let with_thunk_count = self.count_with_thunks(body); let thunk_ids: Vec<ThunkId> = thunks.iter().map(|&(id, _)| id).collect();
let total_slots = thunks.len() + with_thunk_count;
let all_thunks = self.collect_all_thunks(thunks, body);
let thunk_ids: Vec<ThunkId> = all_thunks.iter().map(|&(id, _)| id).collect();
self.push_scope(false, &thunk_ids); self.push_scope(false, &thunk_ids);
if !thunks.is_empty() {
if total_slots > 0 {
self.emit_op(Op::AllocLocals); self.emit_op(Op::AllocLocals);
self.emit_u32(total_slots as u32); self.emit_u32(thunks.len().try_into().expect("too many thunks"));
} }
self.emit_scope_thunks(thunks); self.emit_scope_thunks(thunks);
@@ -458,11 +291,11 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
} }
fn emit_expr(&mut self, ir: RawIrRef<'_>) { fn emit_expr(&mut self, ir: RawIrRef<'_>) {
match ir.deref() { match ir {
&Ir::Int(x) => { &Ir::Int(x) => {
if x <= i32::MAX as i64 { if let Ok(x) = x.try_into() {
self.emit_op(Op::PushSmi); self.emit_op(Op::PushSmi);
self.emit_i32(x as i32); self.emit_i32(x);
} else { } else {
self.emit_op(Op::PushBigInt); self.emit_op(Op::PushBigInt);
self.emit_i64(x); self.emit_i64(x);
@@ -482,6 +315,8 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
&Ir::Path(p) => { &Ir::Path(p) => {
self.emit_expr(p); self.emit_expr(p);
self.emit_op(Op::ResolvePath); self.emit_op(Op::ResolvePath);
let dir_id = self.ctx.current_source_dir();
self.emit_str_id(dir_id);
} }
&Ir::If { cond, consq, alter } => { &Ir::If { cond, consq, alter } => {
self.emit_expr(cond); self.emit_expr(cond);
@@ -544,7 +379,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
self.emit_maybe_thunk(arg); self.emit_maybe_thunk(arg);
} }
&Ir::Arg { layer } => { &Ir::Arg { layer } => {
self.emit_load(layer.try_into().expect("scope too deep!"), 0); self.emit_load(layer, 0);
} }
&Ir::TopLevel { body, ref thunks } => { &Ir::TopLevel { body, ref thunks } => {
self.emit_toplevel_inner(body, thunks); self.emit_toplevel_inner(body, thunks);
@@ -557,10 +392,6 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
} => { } => {
self.emit_select(expr, attrpath, default, span); self.emit_select(expr, attrpath, default, span);
} }
&Ir::Thunk(id) => {
let (layer, local) = self.resolve_thunk(id);
self.emit_load(layer, local);
}
Ir::Builtins => { Ir::Builtins => {
self.emit_op(Op::LoadBuiltins); self.emit_op(Op::LoadBuiltins);
} }
@@ -570,24 +401,23 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
} }
&Ir::BuiltinConst(id) => { &Ir::BuiltinConst(id) => {
self.emit_select( self.emit_select(
RawIrRef(&Ir::Builtins), &Ir::Builtins,
&[Attr::Str(id, TextRange::default())], &[Attr::Str(id, TextRange::default())],
None, None,
TextRange::default(), TextRange::default(),
); );
} }
&Ir::ConcatStrings { &Ir::ConcatStrings {
parts: _, ref parts,
force_string: _, force_string,
} => { } => {
todo!("redesign ConcatStrings"); for &part in parts.iter() {
// self.emit_op(Op::ConcatStrings); self.emit_expr(part);
// self.emit_u16(parts.len() as u16); self.emit_op(Op::CoerceToString);
// self.emit_u8(if force_string { 1 } else { 0 }); }
// for &part in parts.iter() { self.emit_op(Op::ConcatStrings);
// let operand = self.inline_maybe_thunk(part); self.emit_u16(parts.len() as u16);
// self.emit_inline_operand(operand); self.emit_bool(force_string);
// }
} }
&Ir::HasAttr { lhs, ref rhs } => { &Ir::HasAttr { lhs, ref rhs } => {
self.emit_has_attr(lhs, rhs); self.emit_has_attr(lhs, rhs);
@@ -601,37 +431,92 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
let raw_idx = self.ctx.intern_string(assertion_raw); let raw_idx = self.ctx.intern_string(assertion_raw);
let span_id = self.ctx.register_span(*span); let span_id = self.ctx.register_span(*span);
self.emit_expr(*assertion); self.emit_expr(*assertion);
self.emit_expr(*expr);
self.emit_op(Op::Assert); self.emit_op(Op::Assert);
self.emit_str_id(raw_idx); self.emit_str_id(raw_idx);
self.emit_u32(span_id); self.emit_u32(span_id);
} self.emit_expr(*expr);
&Ir::CurPos(span) => {
let span_id = self.ctx.register_span(span);
self.emit_op(Op::MkPos);
self.emit_u32(span_id);
} }
&Ir::ReplBinding(name) => { &Ir::ReplBinding(name) => {
self.emit_op(Op::LoadReplBinding); self.emit_op(Op::LoadReplBinding);
self.emit_str_id(name); self.emit_str_id(name);
} }
&Ir::ScopedImportBinding(name) => { &Ir::ScopedImportBinding { sym, slot_id } => {
self.emit_op(Op::LoadScopedBinding); self.emit_op(Op::LoadScopedBinding);
self.emit_str_id(name); self.emit_u32(slot_id);
self.emit_str_id(sym);
} }
&Ir::With { Ir::WithLookup { sym, namespaces } => {
namespace, // counter
body, self.emit_expr(&Ir::Int(0));
ref thunks,
} => {
self.emit_with(namespace, body, thunks);
}
&Ir::WithLookup(name) => {
// TODO: specialize shallow with lookups
self.emit_op(Op::PrepareWith);
self.emit_op(Op::LookupWith); self.emit_op(Op::LookupWith);
self.emit_str_id(*sym);
self.emit_u8(
namespaces
.len()
.try_into()
.expect("too many `with` namespaces"),
);
for namespace in namespaces {
self.emit_maybe_thunk(namespace);
}
}
&Ir::MaybeThunk(thunk) => {
use MaybeThunk::*;
match *thunk {
Int(x) => {
if let Ok(x) = x.try_into() {
self.emit_op(Op::PushSmi);
self.emit_i32(x);
} else {
self.emit_op(Op::PushBigInt);
self.emit_i64(x);
}
}
Float(x) => {
self.emit_op(Op::PushFloat);
self.emit_f64(x);
}
Bool(true) => self.emit_op(Op::PushTrue),
Bool(false) => self.emit_op(Op::PushFalse),
Null => self.emit_op(Op::PushNull),
Str(id) => {
self.emit_op(Op::PushString);
self.emit_str_id(id);
}
Path(id) => {
self.emit_op(Op::PushString);
self.emit_str_id(id);
self.emit_op(Op::ResolvePath);
let dir_id = self.ctx.current_source_dir();
self.emit_str_id(dir_id);
}
Thunk(id) => {
let (layer, local) = self.resolve_thunk(id);
self.emit_load(layer, local);
}
Arg { layer } => self.emit_load(layer, 0),
Builtin(id) => {
self.emit_op(Op::LoadBuiltin);
self.emit_u8(id as u8);
}
BuiltinConst(id) => self.emit_select(
&Ir::Builtins,
&[Attr::Str(id, TextRange::default())],
None,
TextRange::default(),
),
Builtins => self.emit_op(Op::LoadBuiltins),
ReplBinding(name) => {
self.emit_op(Op::LoadReplBinding);
self.emit_str_id(name); self.emit_str_id(name);
} }
ScopedImportBinding { slot_id, sym } => {
self.emit_op(Op::LoadScopedBinding);
self.emit_u32(slot_id);
self.emit_str_id(sym);
}
}
}
} }
} }
@@ -695,18 +580,6 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
let end_offset = (self.ctx.get_code_mut().len() as i32) - (after_jump as i32); let end_offset = (self.ctx.get_code_mut().len() as i32) - (after_jump as i32);
self.patch_i32(end_placeholder, end_offset); self.patch_i32(end_placeholder, end_offset);
} }
PipeL => {
todo!("new call");
// self.emit_expr(rhs);
// self.emit_expr(lhs);
// self.emit_op(Op::Call);
}
PipeR => {
todo!("new call");
// self.emit_expr(lhs);
// self.emit_expr(rhs);
// self.emit_op(Op::Call);
}
_ => { _ => {
self.emit_expr(lhs); self.emit_expr(lhs);
self.emit_expr(rhs); self.emit_expr(rhs);
@@ -729,17 +602,13 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
} }
} }
fn emit_func( fn emit_func<'ir>(
&mut self, &mut self,
thunks: &[(ThunkId, RawIrRef<'_>)], thunks: &[(ThunkId, RawIrRef<'ir>)],
param: &Option<Param<'_>>, param: &Option<Param<'ir>>,
body: RawIrRef<'_>, body: RawIrRef<'ir>,
) { ) {
let with_thunk_count = self.count_with_thunks(body); let thunk_ids: Vec<ThunkId> = thunks.iter().map(|&(id, _)| id).collect();
let total_slots = thunks.len() + with_thunk_count;
let all_thunks = self.collect_all_thunks(thunks, body);
let thunk_ids: Vec<ThunkId> = all_thunks.iter().map(|&(id, _)| id).collect();
let skip_patch = self.emit_jump_placeholder(); let skip_patch = self.emit_jump_placeholder();
let entry_point = self.ctx.get_code().len() as u32; let entry_point = self.ctx.get_code().len() as u32;
@@ -758,10 +627,10 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
{ {
self.emit_op(Op::MakePatternClosure); self.emit_op(Op::MakePatternClosure);
self.emit_u32(entry_point); self.emit_u32(entry_point);
self.emit_u32(total_slots as u32); self.emit_u32(thunks.len().try_into().expect("too many thunks"));
self.emit_u16(required.len() as u16); self.emit_u16(required.len() as u16);
self.emit_u16(optional.len() as u16); self.emit_u16(optional.len() as u16);
self.emit_u8(if *ellipsis { 1 } else { 0 }); self.emit_bool(*ellipsis);
for &(sym, _) in required.iter() { for &(sym, _) in required.iter() {
self.emit_str_id(sym); self.emit_str_id(sym);
@@ -777,39 +646,39 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
} else { } else {
self.emit_op(Op::MakeClosure); self.emit_op(Op::MakeClosure);
self.emit_u32(entry_point); self.emit_u32(entry_point);
self.emit_u32(total_slots as u32); self.emit_u32(thunks.len().try_into().expect("too many thunks"));
} }
} }
fn emit_attrset( fn emit_attrset(
&mut self, &mut self,
stcs: &fix_ir::HashMap<'_, StringId, (MaybeThunk, TextRange)>, stcs: &ir::HashMap<'_, StringId, (&MaybeThunk, TextRange)>,
dyns: &[(RawIrRef<'_>, MaybeThunk, TextRange)], dyns: &[(RawIrRef<'_>, &MaybeThunk, TextRange)],
) { ) {
if stcs.is_empty() && dyns.is_empty() { if stcs.is_empty() && dyns.is_empty() {
self.emit_op(Op::MakeEmptyAttrs); self.emit_op(Op::MakeEmptyAttrs);
return; return;
} }
let total = stcs.len() + dyns.len(); for &(key_expr, _val, _span) in dyns.iter() {
self.emit_expr(key_expr);
}
self.emit_op(Op::MakeAttrs); self.emit_op(Op::MakeAttrs);
self.emit_u32(total as u32); self.emit_u32(stcs.len() as u32);
self.emit_u32(dyns.len() as u32);
for (&sym, &(val, span)) in stcs.iter() { for (&sym, &(val, span)) in stcs.iter() {
self.emit_u8(AttrKeyType::Static as u8);
self.emit_str_id(sym); self.emit_str_id(sym);
self.emit_maybe_thunk(val); self.emit_maybe_thunk(val);
let span_id = self.ctx.register_span(span); let span_id = self.ctx.register_span(span);
self.emit_u32(span_id); self.emit_u32(span_id);
} }
for &(_key, _val, _span) in dyns.iter() {
todo!("redesign dynamic attr key"); for &(_key, val, span) in dyns.iter() {
// self.emit_u8(AttrKeyType::Dynamic as u8); self.emit_maybe_thunk(val);
// self.emit_maybe_thunk(key); let span_id = self.ctx.register_span(span);
// let val_operand = self.inline_maybe_thunk(val); self.emit_u32(span_id);
// self.emit_maybe_thunk(val);
// let span_id = self.ctx.register_span(span);
// self.emit_u32(span_id);
} }
} }
@@ -822,6 +691,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
) { ) {
self.emit_expr(expr); self.emit_expr(expr);
let mut dynamic_patches = Vec::new();
for attr in attrpath.iter() { for attr in attrpath.iter() {
match *attr { match *attr {
Attr::Str(sym, _) => { Attr::Str(sym, _) => {
@@ -831,6 +701,8 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
self.emit_str_id(sym); self.emit_str_id(sym);
} }
Attr::Dynamic(key_expr, _) => { Attr::Dynamic(key_expr, _) => {
self.emit_op(Op::JumpIfSelectFailed);
dynamic_patches.push(self.emit_i32_placeholder());
self.emit_expr(key_expr); self.emit_expr(key_expr);
let span_id = self.ctx.register_span(span); let span_id = self.ctx.register_span(span);
self.emit_op(Op::SelectDynamic); self.emit_op(Op::SelectDynamic);
@@ -840,48 +712,65 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
} }
if let Some(default) = default { if let Some(default) = default {
let before: i32 = self.ctx.get_code().len().try_into().unwrap();
for patch in dynamic_patches {
self.patch_jump_target(patch);
}
self.emit_op(Op::JumpIfSelectSucceeded); self.emit_op(Op::JumpIfSelectSucceeded);
let placeholder = self.emit_i32_placeholder(); let placeholder = self.emit_i32_placeholder();
let before: i32 = self.ctx.get_code().len().try_into().unwrap();
self.emit_expr(default); self.emit_expr(default);
let after: i32 = self.ctx.get_code().len().try_into().unwrap(); let after: i32 = self.ctx.get_code().len().try_into().unwrap();
self.patch_i32(placeholder, after - before); // Offset is relative to after the placeholder, so subtract the
// size of JumpIfSelectSucceeded (1) + placeholder (4).
self.patch_i32(placeholder, after - before - 5);
} else {
for patch in dynamic_patches {
self.patch_jump_target(patch);
}
} }
} }
fn emit_has_attr(&mut self, lhs: RawIrRef<'_>, rhs: &[Attr<RawIrRef<'_>>]) { fn emit_has_attr(&mut self, lhs: RawIrRef<'_>, rhs: &[Attr<RawIrRef<'_>>]) {
self.emit_expr(lhs); self.emit_expr(lhs);
for attr in rhs.iter() {
if let Attr::Dynamic(expr, _) = *attr { let mut dynamic_patches = Vec::new();
self.emit_expr(expr); let [attrs @ .., last] = rhs else {
} panic!("attrpath is empty");
} };
self.emit_op(Op::HasAttr); for attr in attrs {
self.emit_u16(rhs.len() as u16);
for attr in rhs.iter() {
match *attr { match *attr {
Attr::Str(sym, _) => { Attr::Str(sym, span) => {
self.emit_u8(AttrKeyType::Static as u8); let span_id = self.ctx.register_span(span);
self.emit_op(Op::HasAttrPathStatic);
self.emit_u32(span_id);
self.emit_str_id(sym); self.emit_str_id(sym);
} }
Attr::Dynamic(_, _) => { Attr::Dynamic(key_expr, span) => {
self.emit_u8(AttrKeyType::Dynamic as u8); self.emit_op(Op::JumpIfSelectFailed);
dynamic_patches.push(self.emit_i32_placeholder());
self.emit_expr(key_expr);
let span_id = self.ctx.register_span(span);
self.emit_op(Op::HasAttrPathDynamic);
self.emit_u32(span_id);
} }
} }
} }
match *last {
Attr::Str(sym, _) => {
self.emit_op(Op::HasAttrStatic);
self.emit_str_id(sym);
} }
Attr::Dynamic(key_expr, _) => {
fn emit_with( self.emit_op(Op::JumpIfSelectFailed);
&mut self, dynamic_patches.push(self.emit_i32_placeholder());
namespace: MaybeThunk, self.emit_expr(key_expr);
body: RawIrRef<'_>, self.emit_op(Op::HasAttrDynamic);
thunks: &[(ThunkId, RawIrRef<'_>)], }
) { }
self.emit_op(Op::PushWith); for patch in dynamic_patches {
self.emit_maybe_thunk(namespace); self.patch_jump_target(patch);
self.emit_scope_thunks(thunks); }
self.emit_expr(body); self.emit_op(Op::HasAttrResolve);
self.emit_op(Op::PopWith);
} }
fn emit_toplevel_inner(&mut self, body: RawIrRef<'_>, thunks: &[(ThunkId, RawIrRef<'_>)]) { fn emit_toplevel_inner(&mut self, body: RawIrRef<'_>, thunks: &[(ThunkId, RawIrRef<'_>)]) {
+1 -1
View File
@@ -5,5 +5,5 @@ edition = "2024"
[dependencies] [dependencies]
miette = { version = "7.6", features = ["fancy"] } miette = { version = "7.6", features = ["fancy"] }
thiserror = "2.0"
rnix = { workspace = true } rnix = { workspace = true }
thiserror = "2.0"
-342
View File
@@ -1,342 +0,0 @@
use std::hash::Hash;
use std::ops::Deref;
use bumpalo::Bump;
use bumpalo::collections::Vec;
use fix_builtins::{BUILTINS, BuiltinId};
use fix_common::StringId;
use ghost_cell::{GhostCell, GhostToken};
use num_enum::TryFromPrimitive as _;
use rnix::{TextRange, ast};
use string_interner::DefaultStringInterner;
use crate::downgrade::DowngradeContext;
pub mod downgrade;
pub type HashMap<'ir, K, V> = hashbrown::HashMap<K, V, hashbrown::DefaultHashBuilder, &'ir Bump>;
#[repr(transparent)]
#[derive(Clone, Copy)]
pub struct IrRef<'id, 'ir>(&'ir GhostCell<'id, Ir<'ir, Self>>);
impl<'id, 'ir> IrRef<'id, 'ir> {
pub fn new(ir: &'ir GhostCell<'id, Ir<'ir, Self>>) -> Self {
Self(ir)
}
pub fn alloc(bump: &'ir Bump, ir: Ir<'ir, Self>) -> Self {
Self(bump.alloc(GhostCell::new(ir)))
}
pub fn borrow<'a>(&'a self, token: &'a GhostToken<'id>) -> &'a Ir<'ir, Self> {
self.0.borrow(token)
}
/// Freeze a mutable IR reference into a read-only one, consuming the
/// `GhostToken` to prevent any further mutation.
///
/// # Safety
/// The transmute is sound because:
/// - `GhostCell<'id, T>` is `#[repr(transparent)]` over `T`
/// - `IrRef<'id, 'ir>` is `#[repr(transparent)]` over
/// `&'ir GhostCell<'id, Ir<'ir, Self>>`
/// - `RawIrRef<'ir>` is `#[repr(transparent)]` over `&'ir Ir<'ir, Self>`
/// - `Ir<'ir, Ref>` is `#[repr(C)]` and both ref types are pointer-sized
///
/// Consuming the `GhostToken` guarantees no `borrow_mut` calls can occur
/// afterwards, so the shared `&Ir` references from `RawIrRef::Deref` can
/// never alias with mutable references.
pub fn freeze(self, _token: GhostToken<'id>) -> RawIrRef<'ir> {
unsafe { std::mem::transmute(self) }
}
}
#[repr(transparent)]
#[derive(Clone, Copy, Debug)]
pub struct RawIrRef<'ir>(pub &'ir Ir<'ir, Self>);
impl<'ir> Deref for RawIrRef<'ir> {
type Target = Ir<'ir, RawIrRef<'ir>>;
fn deref(&self) -> &Self::Target {
self.0
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub enum MaybeThunk {
Int(i64),
Float(f64),
Bool(bool),
Null,
Str(StringId),
Path(StringId),
Thunk(ThunkId),
Arg { layer: usize },
Builtin(BuiltinId),
Builtins,
ReplBinding(StringId),
ScopedImportBinding(StringId),
WithLookup(StringId),
}
impl MaybeThunk {
fn to_ir<'id, 'ir>(self, ctx: &mut impl DowngradeContext<'id, 'ir>) -> IrRef<'id, 'ir> {
use MaybeThunk::*;
let ir = match self {
Int(x) => Ir::Int(x),
Float(x) => Ir::Float(x),
Bool(x) => Ir::Bool(x),
Null => Ir::Null,
Str(x) => Ir::Str(x),
Path(x) => Ir::Path(ctx.new_expr(Ir::Str(x))),
Thunk(x) => Ir::Thunk(x),
Arg { layer } => Ir::Arg { layer },
Builtin(x) => Ir::Builtin(x),
Builtins => Ir::Builtins,
ReplBinding(x) => Ir::ReplBinding(x),
ScopedImportBinding(x) => Ir::ScopedImportBinding(x),
WithLookup(x) => Ir::WithLookup(x),
};
ctx.new_expr(ir)
}
}
#[repr(C)]
#[derive(Debug)]
pub enum Ir<'ir, Ref> {
Int(i64),
Float(f64),
Bool(bool),
Null,
Str(StringId),
Path(Ref),
AttrSet {
stcs: HashMap<'ir, StringId, (MaybeThunk, TextRange)>,
dyns: Vec<'ir, (Ref, MaybeThunk, TextRange)>,
},
List {
items: Vec<'ir, MaybeThunk>,
},
ConcatStrings {
parts: Vec<'ir, Ref>,
force_string: bool,
},
// OPs
UnOp {
rhs: Ref,
kind: UnOpKind,
},
BinOp {
lhs: Ref,
rhs: Ref,
kind: BinOpKind,
},
HasAttr {
lhs: Ref,
rhs: Vec<'ir, Attr<Ref>>,
},
Select {
expr: Ref,
attrpath: Vec<'ir, Attr<Ref>>,
default: Option<Ref>,
span: TextRange,
},
// Conditionals
If {
cond: Ref,
consq: Ref,
alter: Ref,
},
Assert {
assertion: Ref,
expr: Ref,
assertion_raw: String,
span: TextRange,
},
With {
namespace: MaybeThunk,
body: Ref,
thunks: Vec<'ir, (ThunkId, Ref)>,
},
WithLookup(StringId),
// Function related
Func {
body: Ref,
param: Option<Param<'ir>>,
thunks: Vec<'ir, (ThunkId, Ref)>,
},
Arg {
layer: usize,
},
Call {
func: Ref,
arg: MaybeThunk,
span: TextRange,
},
// Builtins
Builtins,
Builtin(BuiltinId),
BuiltinConst(StringId),
// Misc
TopLevel {
body: Ref,
thunks: Vec<'ir, (ThunkId, Ref)>,
},
Thunk(ThunkId),
CurPos(TextRange),
ReplBinding(StringId),
ScopedImportBinding(StringId),
}
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ThunkId(pub usize);
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct SpanId(pub u32);
/// Represents a key in an attribute path.
#[allow(unused)]
#[derive(Debug)]
pub enum Attr<Ref> {
/// A dynamic attribute key, which is an expression that must evaluate to a string.
/// Example: `attrs.${key}`
Dynamic(Ref, TextRange),
/// A static attribute key.
/// Example: `attrs.key`
Str(StringId, TextRange),
}
/// The kinds of binary operations supported in Nix.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum BinOpKind {
// Arithmetic
Add,
Sub,
Div,
Mul,
// Comparison
Eq,
Neq,
Lt,
Gt,
Leq,
Geq,
// Logical
And,
Or,
Impl,
// Set/String/Path operations
Con, // List concatenation (`++`)
Upd, // AttrSet update (`//`)
// Not standard, but part of rnix AST
PipeL,
PipeR,
}
impl From<ast::BinOpKind> for BinOpKind {
fn from(op: ast::BinOpKind) -> Self {
use BinOpKind::*;
use ast::BinOpKind as kind;
match op {
kind::Concat => Con,
kind::Update => Upd,
kind::Add => Add,
kind::Sub => Sub,
kind::Mul => Mul,
kind::Div => Div,
kind::And => And,
kind::Equal => Eq,
kind::Implication => Impl,
kind::Less => Lt,
kind::LessOrEq => Leq,
kind::More => Gt,
kind::MoreOrEq => Geq,
kind::NotEqual => Neq,
kind::Or => Or,
kind::PipeLeft => PipeL,
kind::PipeRight => PipeR,
}
}
}
/// The kinds of unary operations.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum UnOpKind {
Neg, // Negation (`-`)
Not, // Logical not (`!`)
}
impl From<ast::UnaryOpKind> for UnOpKind {
fn from(value: ast::UnaryOpKind) -> Self {
match value {
ast::UnaryOpKind::Invert => UnOpKind::Not,
ast::UnaryOpKind::Negate => UnOpKind::Neg,
}
}
}
/// Describes the parameters of a function.
#[derive(Debug)]
pub struct Param<'ir> {
pub required: Vec<'ir, (StringId, TextRange)>,
pub optional: Vec<'ir, (StringId, TextRange)>,
pub ellipsis: bool,
}
pub fn new_global_env(
strings: &mut DefaultStringInterner,
) -> hashbrown::HashMap<StringId, Ir<'static, RawIrRef<'static>>> {
let mut global_env = hashbrown::HashMap::new();
let builtins_sym = StringId(strings.get_or_intern("builtins"));
global_env.insert(builtins_sym, Ir::Builtins);
for (idx, &(name, _)) in BUILTINS.iter().enumerate() {
let id = BuiltinId::try_from_primitive(idx as u8).expect("infallible");
let name = StringId(strings.get_or_intern(name));
global_env.insert(name, Ir::Builtin(id));
}
let consts = [
(
"__currentSystem",
Ir::BuiltinConst(StringId(strings.get_or_intern("currentSystem"))),
),
("__langVersion", Ir::Int(6)),
(
"__nixVersion",
Ir::BuiltinConst(StringId(strings.get_or_intern("nixVersion"))),
),
(
"__storeDir",
Ir::BuiltinConst(StringId(strings.get_or_intern("storeDir"))),
),
(
"__nixPath",
Ir::BuiltinConst(StringId(strings.get_or_intern("nixPath"))),
),
("null", Ir::Null),
("true", Ir::Bool(true)),
("false", Ir::Bool(false)),
];
for (name, ir) in consts {
let name = StringId(strings.get_or_intern(name));
global_env.insert(name, ir);
}
global_env
}
@@ -1,9 +1,10 @@
[package] [package]
name = "fix-common" name = "fix-lang"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
gc-arena = { workspace = true }
string-interner = { workspace = true }
ere = { workspace = true } ere = { workspace = true }
gc-arena = { workspace = true }
num_enum = { workspace = true }
string-interner = { workspace = true }
+124 -2
View File
@@ -4,6 +4,128 @@ use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use gc_arena::Collect; use gc_arena::Collect;
use num_enum::TryFromPrimitive;
macro_rules! define_builtins {
($(($name:literal, $variant:ident, $arity:expr)),* $(,)?) => {
pub const BUILTINS: &[(&str, u8)] = &[
$(($name, $arity),)*
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive, Collect)]
#[repr(u8)]
#[collect(require_static)]
pub enum BuiltinId {
$($variant,)*
}
};
}
define_builtins! {
("abort", Abort, 1),
("__add", Add, 2),
("__addErrorContext", AddErrorContext, 2),
("__all", All, 2),
("__any", Any, 2),
("__appendContext", AppendContext, 2),
("__attrNames", AttrNames, 1),
("__attrValues", AttrValues, 1),
("baseNameOf", BaseNameOf, 1),
("__bitAnd", BitAnd, 2),
("__bitOr", BitOr, 2),
("__bitXor", BitXor, 2),
("break", Break, 1),
("__catAttrs", CatAttrs, 2),
("__ceil", Ceil, 1),
("__compareVersions", CompareVersions, 2),
("__concatLists", ConcatLists, 1),
("__concatMap", ConcatMap, 2),
("__concatStringsSep", ConcatStringsSep, 2),
("__convertHash", ConvertHash, 1),
("__deepSeq", DeepSeq, 2),
("derivation", Derivation, 1),
("derivationStrict", DerivationStrict, 1),
("dirOf", DirOf, 1),
("__div", Div, 2),
("__elem", Elem, 2),
("__elemAt", ElemAt, 2),
("fetchGit", FetchGit, 1),
("fetchMercurial", FetchMercurial, 1),
("fetchTarball", FetchTarball, 1),
("fetchTree", FetchTree, 1),
("__fetchurl", FetchUrl, 1),
("__filter", Filter, 2),
("__filterSource", FilterSource, 2),
("__findFile", FindFile, 2),
("__floor", Floor, 1),
("__foldl'", FoldlStrict, 3),
("__fromJSON", FromJSON, 1),
("fromTOML", FromTOML, 1),
("__functionArgs", FunctionArgs, 1),
("__genList", GenList, 2),
("__genericClosure", GenericClosure, 1),
("__getAttr", GetAttr, 2),
("__getContext", GetContext, 1),
("__getEnv", GetEnv, 1),
("__groupBy", GroupBy, 2),
("__hasAttr", HasAttr, 2),
("__hasContext", HasContext, 1),
("__hashFile", HashFile, 2),
("__hashString", HashString, 2),
("__head", Head, 1),
("import", Import, 1),
("__intersectAttrs", IntersectAttrs, 2),
("__isAttrs", IsAttrs, 1),
("__isBool", IsBool, 1),
("__isFloat", IsFloat, 1),
("__isFunction", IsFunction, 1),
("__isInt", IsInt, 1),
("__isList", IsList, 1),
("isNull", IsNull, 1),
("__isPath", IsPath, 1),
("__isString", IsString, 1),
("__length", Length, 1),
("__lessThan", LessThan, 2),
("__listToAttrs", ListToAttrs, 1),
("map", Map, 2),
("__mapAttrs", MapAttrs, 2),
("__match", Match, 2),
("__mul", Mul, 2),
("__parseDrvName", ParseDrvName, 1),
("__partition", Partition, 2),
("__path", Path, 1),
("__pathExists", PathExists, 1),
("placeholder", Placeholder, 1),
("__readDir", ReadDir, 1),
("__readFile", ReadFile, 1),
("__readFileType", ReadFileType, 1),
("removeAttrs", RemoveAttrs, 2),
("__replaceStrings", ReplaceStrings, 3),
("scopedImport", ScopedImport, 2),
("__seq", Seq, 2),
("__sort", Sort, 2),
("__split", Split, 2),
("__splitVersion", SplitVersion, 1),
("__storePath", StorePath, 1),
("__stringLength", StringLength, 1),
("__sub", Sub, 2),
("__substring", Substring, 3),
("__tail", Tail, 1),
("throw", Throw, 1),
("__toFile", ToFile, 2),
("__toJSON", ToJSON, 1),
("__toPath", ToPath, 1),
("toString", ToString, 1),
("__toXML", ToXML, 1),
("__trace", Trace, 2),
("__tryEval", TryEval, 1),
("__typeOf", TypeOf, 1),
("__unsafeDiscardStringContext", UnsafeDiscardStringContext, 1),
("__unsafeDiscardOutputDependency", UnsafeDiscardOutputDependency, 1),
("__unsafeGetAttrPos", UnsafeGetAttrPos, 2),
("__warn", Warn, 2),
("__zipAttrsWith", ZipAttrsWith, 2),
}
#[repr(transparent)] #[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Collect)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Collect)]
@@ -272,9 +394,9 @@ pub enum Value {
/// A function (lambda). /// A function (lambda).
Func, Func,
/// A primitive (built-in) operation. /// A primitive (built-in) operation.
PrimOp(String), PrimOp(&'static str),
/// A partially applied primitive operation. /// A partially applied primitive operation.
PrimOpApp(String), PrimOpApp(&'static str),
/// A marker for a value that has been seen before during serialization, to break cycles. /// A marker for a value that has been seen before during serialization, to break cycles.
/// This is used to prevent infinite recursion when printing or serializing cyclic data structures. /// This is used to prevent infinite recursion when printing or serializing cyclic data structures.
Repeated, Repeated,
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "fix-runtime"
version = "0.1.0"
edition = "2024"
[dependencies]
gc-arena = { workspace = true }
hashbrown = { workspace = true }
smallvec = { workspace = true }
sptr = "0.3"
string-interner = { workspace = true }
fix-bytecode = { path = "../fix-bytecode" }
fix-error = { path = "../fix-error" }
fix-lang = { path = "../fix-lang" }
@@ -24,7 +24,7 @@ impl<T: Default + Copy, const N: usize> ArrayExt<N> for [T; N] {
} }
} }
pub(crate) trait RawStore: Sized { pub trait RawStore: Sized {
fn to_val(self, value: &mut Value); fn to_val(self, value: &mut Value);
fn from_val(value: &Value) -> Self; fn from_val(value: &Value) -> Self;
} }
@@ -157,24 +157,24 @@ enum TagVal {
} }
#[derive(Copy, Clone, PartialEq, Eq)] #[derive(Copy, Clone, PartialEq, Eq)]
pub(crate) struct RawTag(TagVal); pub struct RawTag(TagVal);
impl RawTag { impl RawTag {
pub(crate) const P1: RawTag = RawTag(TagVal::_P1); pub const P1: RawTag = RawTag(TagVal::_P1);
pub(crate) const P2: RawTag = RawTag(TagVal::_P2); pub const P2: RawTag = RawTag(TagVal::_P2);
pub(crate) const P3: RawTag = RawTag(TagVal::_P3); pub const P3: RawTag = RawTag(TagVal::_P3);
pub(crate) const P4: RawTag = RawTag(TagVal::_P4); pub const P4: RawTag = RawTag(TagVal::_P4);
pub(crate) const P5: RawTag = RawTag(TagVal::_P5); pub const P5: RawTag = RawTag(TagVal::_P5);
pub(crate) const P6: RawTag = RawTag(TagVal::_P6); pub const P6: RawTag = RawTag(TagVal::_P6);
pub(crate) const P7: RawTag = RawTag(TagVal::_P7); pub const P7: RawTag = RawTag(TagVal::_P7);
pub(crate) const N1: RawTag = RawTag(TagVal::_N1); pub const N1: RawTag = RawTag(TagVal::_N1);
pub(crate) const N2: RawTag = RawTag(TagVal::_N2); pub const N2: RawTag = RawTag(TagVal::_N2);
pub(crate) const N3: RawTag = RawTag(TagVal::_N3); pub const N3: RawTag = RawTag(TagVal::_N3);
pub(crate) const N4: RawTag = RawTag(TagVal::_N4); pub const N4: RawTag = RawTag(TagVal::_N4);
pub(crate) const N5: RawTag = RawTag(TagVal::_N5); pub const N5: RawTag = RawTag(TagVal::_N5);
pub(crate) const N6: RawTag = RawTag(TagVal::_N6); pub const N6: RawTag = RawTag(TagVal::_N6);
pub(crate) const N7: RawTag = RawTag(TagVal::_N7); pub const N7: RawTag = RawTag(TagVal::_N7);
#[inline] #[inline]
#[must_use] #[must_use]
@@ -260,7 +260,7 @@ impl RawTag {
#[inline] #[inline]
#[must_use] #[must_use]
pub(crate) const fn neg_val(self) -> (bool, u8) { pub const fn neg_val(self) -> (bool, u8) {
match self.0 { match self.0 {
TagVal::_P1 => (false, 1), TagVal::_P1 => (false, 1),
TagVal::_P2 => (false, 2), TagVal::_P2 => (false, 2),
@@ -323,7 +323,7 @@ impl Header {
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Copy, Clone, Debug, PartialEq)]
#[repr(C, align(8))] #[repr(C, align(8))]
pub(crate) struct Value { pub struct Value {
#[cfg(target_endian = "big")] #[cfg(target_endian = "big")]
header: Header, header: Header,
data: [u8; 6], data: [u8; 6],
@@ -373,7 +373,7 @@ impl Value {
#[inline] #[inline]
#[must_use] #[must_use]
pub(crate) fn data(&self) -> &[u8; 6] { pub fn data(&self) -> &[u8; 6] {
&self.data &self.data
} }
@@ -1,45 +1,49 @@
use fix_common::StringId; use fix_lang::StringId;
use gc_arena::{Gc, Mutation}; use gc_arena::{Gc, Mutation};
use crate::value::*; use crate::{
use crate::{Break, BytecodeReader, NixNum, Step, Vm}; AttrSet, Break, BytecodeReader, Closure, List, Machine, NixNum, NixString, NixType, Null,
PrimOp, PrimOpApp, Step, StrictValue,
};
pub(crate) trait Forced<'gc>: Sized { pub trait Forced<'gc>: Sized {
const WIDTH: usize; const WIDTH: usize;
/// Force and type-check the `WIDTH` slots starting at `base_depth` from /// Force and type-check the `WIDTH` slots starting at `base_depth` from
/// TOS, deepest-first. If a slot holds a thunk, enter it and return /// TOS, deepest-first. If a slot holds a thunk, enter it and return
/// `Break::Force`. If a slot holds a value of the wrong type, call /// `Break::Force`. If a slot holds a value of the wrong type, call
/// `finish_type_err` and return `Break::Done`. /// `finish_type_err` and return `Break::Done`.
fn force_and_check( fn force_and_check<M: Machine<'gc>>(
vm: &mut Vm<'gc>, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
base_depth: usize, base_depth: usize,
resume_pc: usize,
) -> Step; ) -> Step;
/// After `force_and_check` returned `Continue`, pop `WIDTH` slots /// After `force_and_check` returned `Continue`, pop `WIDTH` slots
/// (TOS first) and convert. Type assertions are infallible because /// (TOS first) and convert. Type assertions are infallible because
/// `force_and_check` already validated every slot. /// `force_and_check` already validated every slot.
fn pop_converted(vm: &mut Vm<'gc>) -> Self; fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self;
} }
impl<'gc> Forced<'gc> for StrictValue<'gc> { impl<'gc> Forced<'gc> for StrictValue<'gc> {
const WIDTH: usize = 1; const WIDTH: usize = 1;
#[inline(always)] #[inline(always)]
fn force_and_check( fn force_and_check<M: Machine<'gc>>(
vm: &mut Vm<'gc>, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
base_depth: usize, base_depth: usize,
resume_pc: usize,
) -> Step { ) -> Step {
vm.force_slot(base_depth, reader, mc) m.force_slot_to_pc(base_depth, reader, mc, resume_pc)
} }
#[inline(always)] #[inline(always)]
fn pop_converted(vm: &mut Vm<'gc>) -> Self { fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
vm.pop_forced() m.pop_forced()
} }
} }
@@ -50,24 +54,25 @@ macro_rules! impl_forced_inline {
const WIDTH: usize = 1; const WIDTH: usize = 1;
#[inline(always)] #[inline(always)]
fn force_and_check( fn force_and_check<M: Machine<'gc>>(
vm: &mut Vm<'gc>, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
base_depth: usize, base_depth: usize,
resume_pc: usize,
) -> Step { ) -> Step {
vm.force_slot(base_depth, reader, mc)?; m.force_slot_to_pc(base_depth, reader, mc, resume_pc)?;
let v = vm.peek_forced(base_depth); let v = m.peek_forced(base_depth);
if v.as_inline::<$ty>().is_none() { if v.as_inline::<$ty>().is_none() {
let _: Step = vm.finish_type_err($nix_ty, v.ty()); let _: Step = m.finish_type_err($nix_ty, v.ty());
return Step::Break(Break::Done); return Step::Break(Break::Done);
} }
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
fn pop_converted(vm: &mut Vm<'gc>) -> Self { fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
vm.pop_forced() m.pop_forced()
.as_inline::<$ty>() .as_inline::<$ty>()
.expect("type checked in force_and_check") .expect("type checked in force_and_check")
} }
@@ -83,24 +88,25 @@ macro_rules! impl_forced_gc {
const WIDTH: usize = 1; const WIDTH: usize = 1;
#[inline(always)] #[inline(always)]
fn force_and_check( fn force_and_check<M: Machine<'gc>>(
vm: &mut Vm<'gc>, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
base_depth: usize, base_depth: usize,
resume_pc: usize,
) -> Step { ) -> Step {
vm.force_slot(base_depth, reader, mc)?; m.force_slot_to_pc(base_depth, reader, mc, resume_pc)?;
let v = vm.peek_forced(base_depth); let v = m.peek_forced(base_depth);
if v.as_gc::<$ty>().is_none() { if v.as_gc::<$ty>().is_none() {
let _: Step = vm.finish_type_err($nix_ty, v.ty()); let _: Step = m.finish_type_err($nix_ty, v.ty());
return Step::Break(Break::Done); return Step::Break(Break::Done);
} }
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
fn pop_converted(vm: &mut Vm<'gc>) -> Self { fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
vm.pop_forced() m.pop_forced()
.as_gc::<$ty>() .as_gc::<$ty>()
.expect("type checked in force_and_check") .expect("type checked in force_and_check")
} }
@@ -130,24 +136,25 @@ impl<'gc> Forced<'gc> for NixNum {
const WIDTH: usize = 1; const WIDTH: usize = 1;
#[inline(always)] #[inline(always)]
fn force_and_check( fn force_and_check<M: Machine<'gc>>(
vm: &mut Vm<'gc>, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
base_depth: usize, base_depth: usize,
resume_pc: usize,
) -> Step { ) -> Step {
vm.force_slot(base_depth, reader, mc)?; m.force_slot_to_pc(base_depth, reader, mc, resume_pc)?;
let v = vm.peek_forced(base_depth); let v = m.peek_forced(base_depth);
if v.as_num().is_none() { if v.as_num().is_none() {
let _: Step = vm.finish_type_err(NixType::Int, v.ty()); let _: Step = m.finish_type_err(NixType::Int, v.ty());
return Step::Break(Break::Done); return Step::Break(Break::Done);
} }
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
fn pop_converted(vm: &mut Vm<'gc>) -> Self { fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
vm.pop_forced() m.pop_forced()
.as_num() .as_num()
.expect("type checked in force_and_check") .expect("type checked in force_and_check")
} }
@@ -157,24 +164,25 @@ impl<'gc> Forced<'gc> for f64 {
const WIDTH: usize = 1; const WIDTH: usize = 1;
#[inline(always)] #[inline(always)]
fn force_and_check( fn force_and_check<M: Machine<'gc>>(
vm: &mut Vm<'gc>, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
base_depth: usize, base_depth: usize,
resume_pc: usize,
) -> Step { ) -> Step {
vm.force_slot(base_depth, reader, mc)?; m.force_slot_to_pc(base_depth, reader, mc, resume_pc)?;
let v = vm.peek_forced(base_depth); let v = m.peek_forced(base_depth);
if v.as_float().is_none() { if v.as_float().is_none() {
let _: Step = vm.finish_type_err(NixType::Float, v.ty()); let _: Step = m.finish_type_err(NixType::Float, v.ty());
return Step::Break(Break::Done); return Step::Break(Break::Done);
} }
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
fn pop_converted(vm: &mut Vm<'gc>) -> Self { fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
vm.pop_forced() m.pop_forced()
.as_float() .as_float()
.expect("type checked in force_and_check") .expect("type checked in force_and_check")
} }
@@ -184,20 +192,21 @@ impl<'gc, A: Forced<'gc>, B: Forced<'gc>> Forced<'gc> for (A, B) {
const WIDTH: usize = A::WIDTH + B::WIDTH; const WIDTH: usize = A::WIDTH + B::WIDTH;
#[inline(always)] #[inline(always)]
fn force_and_check( fn force_and_check<M: Machine<'gc>>(
vm: &mut Vm<'gc>, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
base: usize, base: usize,
resume_pc: usize,
) -> Step { ) -> Step {
A::force_and_check(vm, reader, mc, base + B::WIDTH)?; A::force_and_check(m, reader, mc, base + B::WIDTH, resume_pc)?;
B::force_and_check(vm, reader, mc, base) B::force_and_check(m, reader, mc, base, resume_pc)
} }
#[inline(always)] #[inline(always)]
fn pop_converted(vm: &mut Vm<'gc>) -> Self { fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
let b = B::pop_converted(vm); let b = B::pop_converted(m);
let a = A::pop_converted(vm); let a = A::pop_converted(m);
(a, b) (a, b)
} }
} }
@@ -206,22 +215,23 @@ impl<'gc, A: Forced<'gc>, B: Forced<'gc>, C: Forced<'gc>> Forced<'gc> for (A, B,
const WIDTH: usize = A::WIDTH + B::WIDTH + C::WIDTH; const WIDTH: usize = A::WIDTH + B::WIDTH + C::WIDTH;
#[inline(always)] #[inline(always)]
fn force_and_check( fn force_and_check<M: Machine<'gc>>(
vm: &mut Vm<'gc>, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
base: usize, base: usize,
resume_pc: usize,
) -> Step { ) -> Step {
A::force_and_check(vm, reader, mc, base + B::WIDTH + C::WIDTH)?; A::force_and_check(m, reader, mc, base + B::WIDTH + C::WIDTH, resume_pc)?;
B::force_and_check(vm, reader, mc, base + C::WIDTH)?; B::force_and_check(m, reader, mc, base + C::WIDTH, resume_pc)?;
C::force_and_check(vm, reader, mc, base) C::force_and_check(m, reader, mc, base, resume_pc)
} }
#[inline(always)] #[inline(always)]
fn pop_converted(vm: &mut Vm<'gc>) -> Self { fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
let c = C::pop_converted(vm); let c = C::pop_converted(m);
let b = B::pop_converted(vm); let b = B::pop_converted(m);
let a = A::pop_converted(vm); let a = A::pop_converted(m);
(a, b, c) (a, b, c)
} }
} }
@@ -232,24 +242,31 @@ impl<'gc, A: Forced<'gc>, B: Forced<'gc>, C: Forced<'gc>, D: Forced<'gc>> Forced
const WIDTH: usize = A::WIDTH + B::WIDTH + C::WIDTH + D::WIDTH; const WIDTH: usize = A::WIDTH + B::WIDTH + C::WIDTH + D::WIDTH;
#[inline(always)] #[inline(always)]
fn force_and_check( fn force_and_check<M: Machine<'gc>>(
vm: &mut Vm<'gc>, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
base: usize, base: usize,
resume_pc: usize,
) -> Step { ) -> Step {
A::force_and_check(vm, reader, mc, base + B::WIDTH + C::WIDTH + D::WIDTH)?; A::force_and_check(
B::force_and_check(vm, reader, mc, base + C::WIDTH + D::WIDTH)?; m,
C::force_and_check(vm, reader, mc, base + D::WIDTH)?; reader,
D::force_and_check(vm, reader, mc, base) mc,
base + B::WIDTH + C::WIDTH + D::WIDTH,
resume_pc,
)?;
B::force_and_check(m, reader, mc, base + C::WIDTH + D::WIDTH, resume_pc)?;
C::force_and_check(m, reader, mc, base + D::WIDTH, resume_pc)?;
D::force_and_check(m, reader, mc, base, resume_pc)
} }
#[inline(always)] #[inline(always)]
fn pop_converted(vm: &mut Vm<'gc>) -> Self { fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
let d = D::pop_converted(vm); let d = D::pop_converted(m);
let c = C::pop_converted(vm); let c = C::pop_converted(m);
let b = B::pop_converted(vm); let b = B::pop_converted(m);
let a = A::pop_converted(vm); let a = A::pop_converted(m);
(a, b, c, d) (a, b, c, d)
} }
} }
+165
View File
@@ -0,0 +1,165 @@
use fix_bytecode::InstructionPtr;
use fix_error::Source;
use fix_lang::{self, BUILTINS, StringId};
use hashbrown::HashSet;
use crate::{
AttrSet, Closure, ExtraScope, List, NixString, NixType, Null, Path, PrimOp, PrimOpApp,
StaticValue, StrictValue, StringContext, Thunk, ThunkState, Value,
};
pub trait VmContext {
fn split(&mut self) -> (&mut impl VmCode, &mut impl VmRuntimeCtx);
}
pub trait VmRuntimeCtx {
fn intern_string(&mut self, s: impl AsRef<str>) -> StringId;
fn resolve_string(&self, id: StringId) -> &str;
fn get_const(&self, id: u32) -> StaticValue;
fn add_const(&mut self, val: StaticValue) -> u32;
}
pub trait VmCode {
fn bytecode(&self) -> &[u8];
fn compile_with_scope(
&mut self,
source: Source,
extra_scope: Option<ExtraScope>,
ctx: &mut impl VmRuntimeCtx,
) -> fix_error::Result<InstructionPtr>;
}
pub trait VmRuntimeCtxExt: VmRuntimeCtx {
fn get_string<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str>;
fn get_string_or_path<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str>;
fn get_string_id<'a, 'gc: 'a>(
&'a mut self,
val: StrictValue<'gc>,
) -> std::result::Result<StringId, NixType>;
/// Returns the string context attached to `val`, or `&[]` if `val` is
/// either a non-string or a string without context.
fn get_string_context<'gc>(&self, val: StrictValue<'gc>) -> &'gc StringContext;
fn convert_value(&self, val: Value) -> fix_lang::Value;
}
impl<T: VmRuntimeCtx> VmRuntimeCtxExt for T {
fn get_string<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str> {
if let Some(sid) = val.as_inline::<StringId>() {
Some(self.resolve_string(sid))
} else {
val.as_gc::<NixString>().map(|ns| ns.as_ref().as_str())
}
}
/// Like `get_string`, but also accepts `Path` values (returning their
/// underlying canonical-path string). Use this in places where Nix
/// would coerce a path to a string (string interpolation, file IO
/// builtins, etc.).
fn get_string_or_path<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str> {
if let Some(p) = val.as_inline::<Path>() {
Some(self.resolve_string(p.0))
} else {
self.get_string(val)
}
}
fn get_string_id<'a, 'gc: 'a>(
&'a mut self,
val: StrictValue<'gc>,
) -> std::result::Result<StringId, NixType> {
if let Some(sid) = val.as_inline::<StringId>() {
Ok(sid)
} else if let Some(s) = val.as_gc::<NixString>().map(|ns| ns.as_ref().as_str()) {
Ok(self.intern_string(s))
} else {
Err(val.ty())
}
}
fn get_string_context<'gc>(&self, val: StrictValue<'gc>) -> &'gc StringContext {
if let Some(ns) = val.as_gc::<NixString>() {
ns.as_ref().context()
} else {
StringContext::empty()
}
}
fn convert_value(&self, val: Value) -> fix_lang::Value {
self.convert_value_with_seen(val, &mut HashSet::new())
}
}
pub(crate) trait ConvertValueWithSeen: VmRuntimeCtx {
fn convert_value_with_seen(&self, val: Value, seen: &mut HashSet<u64>) -> fix_lang::Value;
}
impl<T: VmRuntimeCtx> ConvertValueWithSeen for T {
fn convert_value_with_seen(&self, val: Value, seen: &mut HashSet<u64>) -> fix_lang::Value {
use fix_lang::Value;
if let Some(i) = val.as_inline::<i32>() {
Value::Int(i as i64)
} else if let Some(gc_i) = val.as_gc::<i64>() {
Value::Int(*gc_i)
} else if let Some(f) = val.as_float() {
Value::Float(f)
} else if let Some(b) = val.as_inline::<bool>() {
Value::Bool(b)
} else if val.is::<Null>() {
Value::Null
} else if let Some(sid) = val.as_inline::<StringId>() {
let s = self.resolve_string(sid).to_owned();
Value::String(s)
} else if let Some(ns) = val.as_gc::<NixString>() {
Value::String(ns.as_str().to_owned())
} else if let Some(p) = val.as_inline::<Path>() {
Value::Path(self.resolve_string(p.0).to_owned())
} else if let Some(attrs) = val.as_gc::<AttrSet>() {
let bits = val.to_bits();
if attrs.entries.is_empty() {
return Value::AttrSet(Default::default());
}
if !seen.insert(bits) {
return Value::Repeated;
}
let mut map = std::collections::BTreeMap::new();
for &(key, val) in attrs.entries.iter() {
let key = self.resolve_string(key).to_owned();
let converted = self.convert_value_with_seen(val, seen);
map.insert(fix_lang::Symbol::from(key), converted);
}
Value::AttrSet(fix_lang::AttrSet::new(map))
} else if let Some(list) = val.as_gc::<List>() {
let bits = val.to_bits();
if list.inner.borrow().is_empty() {
return Value::List(Default::default());
}
if !seen.insert(bits) {
return Value::Repeated;
}
let items: Vec<_> = list
.inner
.borrow()
.iter()
.copied()
.map(|v| self.convert_value_with_seen(v, seen))
.collect();
Value::List(fix_lang::List::new(items))
} else if val.is::<Closure>() {
Value::Func
} else if let Some(thunk) = val.as_gc::<Thunk>() {
if let ThunkState::Evaluated(v) = *thunk.borrow() {
self.convert_value_with_seen(v.relax(), seen)
} else {
Value::Thunk
}
} else if let Some(primop) = val.as_inline::<PrimOp>() {
let name = BUILTINS[primop.id as usize].0;
Value::PrimOp(name.strip_prefix("__").unwrap_or(name))
} else if let Some(app) = val.as_gc::<PrimOpApp>() {
let name = BUILTINS[app.primop.id as usize].0;
Value::PrimOpApp(name.strip_prefix("__").unwrap_or(name))
} else {
Value::Null
}
}
}
+19
View File
@@ -0,0 +1,19 @@
mod boxing;
mod forced;
mod host;
mod machine;
mod path_util;
mod resolve;
mod state;
mod string_context;
mod value;
pub use fix_bytecode::{BytecodeReader, OperandData};
pub use forced::*;
pub use host::*;
pub use machine::*;
pub use path_util::*;
pub use resolve::*;
pub use state::*;
pub use string_context::*;
pub use value::*;
+178
View File
@@ -0,0 +1,178 @@
use std::ops::ControlFlow;
use std::path::{Path, PathBuf};
use fix_error::Error;
use fix_lang::{self, StringId};
use gc_arena::Mutation;
use crate::{
Break, BytecodeReader, CallFrame, ForceMode, Forced, GcEnv, NixType, PendingLoad, Step,
StrictValue, Value, VmError,
};
/// Abstract VM-side operations consumed by instruction handlers and primops.
///
/// Implementors maintain a value stack, a call stack, an environment chain,
/// pending result/error state, and a set of GC-allocated globals. Methods
/// fall into a few groups:
///
/// - Stack ops (`push` / `pop` / `peek` / `replace` / `pop_forced` / ...)
/// - Forcing primitives (`force_slot` / `force_slot_to_pc`)
/// - Calling (`call` / `return_from_primop`)
/// - Call-frame management (`push_call_frame` / `pop_call_frame` / call-depth)
/// - Environment access (`env` / `set_env` / `local`)
/// - Result finalization (`finish_ok` / `finish_err` / ...)
/// - Global lookup (`builtins` / `empty_list` / `empty_attrs` / ...)
/// - Imports and scope slots (`import_cache_*` / `scope_slot*` / `set_pending_load`)
pub trait Machine<'gc> {
fn push(&mut self, val: Value<'gc>);
#[must_use]
fn pop(&mut self) -> Value<'gc>;
#[must_use]
fn peek(&self, depth: usize) -> Value<'gc>;
#[must_use]
fn peek_forced(&self, depth: usize) -> StrictValue<'gc>;
fn pop_forced(&mut self) -> StrictValue<'gc>;
fn replace(&mut self, depth: usize, val: Value<'gc>);
fn drop_n(&mut self, depth: usize);
fn stack_len(&self) -> usize;
fn force_slot_to_pc(
&mut self,
depth: usize,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
resume_pc: usize,
) -> Step;
#[inline(always)]
fn force_slot(
&mut self,
depth: usize,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let pc = reader.inst_start_pc();
self.force_slot_to_pc(depth, reader, mc, pc)
}
fn call(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
arg: Value<'gc>,
resume_pc: usize,
) -> Step;
#[inline(always)]
fn return_from_primop(&mut self, val: Value<'gc>, reader: &mut BytecodeReader<'_>) -> Step {
self.push(val);
let Some(CallFrame {
pc: ret_pc,
thunk: _,
env,
}) = self.pop_call_frame()
else {
unreachable!()
};
reader.set_pc(ret_pc);
self.dec_call_depth();
self.set_env(env);
Step::Continue(())
}
fn push_call_frame(&mut self, frame: CallFrame<'gc>);
fn pop_call_frame(&mut self) -> Option<CallFrame<'gc>>;
fn call_depth(&self) -> usize;
fn inc_call_depth(&mut self);
fn dec_call_depth(&mut self);
fn env(&self) -> GcEnv<'gc>;
fn set_env(&mut self, env: GcEnv<'gc>);
#[inline(always)]
fn local(&self, layer: u8, idx: u32) -> Value<'gc> {
let mut cur = self.env();
for _ in 0..layer {
let prev = cur.borrow().prev.expect("env chain too short");
cur = prev;
}
cur.borrow().locals[idx as usize]
}
fn finish_ok(&mut self, val: fix_lang::Value) -> Step;
fn finish_err(&mut self, err: Box<Error>) -> Step;
fn finish_type_err(&mut self, expected: NixType, got: NixType) -> Step;
#[inline(always)]
fn finish_vm_err(&mut self, err: VmError) -> Step {
self.finish_err(err.into_error())
}
fn builtins(&self) -> Value<'gc>;
fn functor_sym(&self) -> StringId;
fn empty_list(&self) -> Value<'gc>;
fn empty_attrs(&self) -> Value<'gc>;
fn force_mode(&self) -> ForceMode;
fn import_cache_get(&self, path: &Path) -> Option<Value<'gc>>;
fn import_cache_insert(&mut self, path: PathBuf, val: Value<'gc>);
fn scope_slot(&self, idx: u32) -> Value<'gc>;
fn scope_slots_push(&mut self, val: Value<'gc>) -> u32;
fn set_pending_load(&mut self, load: PendingLoad);
}
/// Extension trait with convenience helpers built on top of [`Machine`].
///
/// Auto-implemented for every `Machine<'gc>` so callers just need to bring
/// `MachineExt` (or `Machine`) into scope.
pub trait MachineExt<'gc>: Machine<'gc> {
/// Force the top `T::WIDTH` stack slots and return them as `T`.
///
/// If any slot holds a pending thunk, this method pushes a call frame
/// whose resume PC is the **start of the current instruction**
/// (`reader.inst_start_pc()`), enters the thunk, and returns
/// `Break::Force`. When the thunk eventually returns, the VM will
/// **re-execute the entire opcode handler from the beginning**.
///
/// # Invariants
///
/// * **Do not call this method more than once in a single handler.**
/// If you need to force multiple values, use a tuple type such as
/// `(StrictValue, StrictValue)` so they are forced and popped in one
/// atomic operation.
/// * The stack layout at the call site must be **identical** every time
/// the handler is re-entered.
/// * Propagate the return value with `?` so `Break::Force` correctly
/// unwinds to the dispatch loop.
#[inline(always)]
fn force_and_retry<T: Forced<'gc>>(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> ControlFlow<Break, T>
where
Self: Sized,
{
let pc = reader.inst_start_pc();
self.force_and_retry_pc(reader, mc, pc)
}
/// Same as [`force_and_retry`](Self::force_and_retry) but allows
/// specifying a custom resume PC.
#[inline(always)]
fn force_and_retry_pc<T: Forced<'gc>>(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
resume_pc: usize,
) -> ControlFlow<Break, T>
where
Self: Sized,
{
T::force_and_check(self, reader, mc, 0, resume_pc)?;
ControlFlow::Continue(T::pop_converted(self))
}
}
impl<'gc, M: Machine<'gc>> MachineExt<'gc> for M {}
+18
View File
@@ -0,0 +1,18 @@
use std::path::{Component, PathBuf};
pub fn canon_path_str(path: impl AsRef<std::path::Path>) -> String {
let p = path.as_ref();
let mut normalized = PathBuf::new();
for component in p.components() {
match component {
Component::Prefix(p) => normalized.push(p.as_os_str()),
Component::RootDir => normalized.push("/"),
Component::CurDir => {}
Component::ParentDir => {
normalized.pop();
}
Component::Normal(c) => normalized.push(c),
}
}
normalized.to_string_lossy().into_owned()
}
+35
View File
@@ -0,0 +1,35 @@
use fix_bytecode::OperandData;
use gc_arena::{Gc, Mutation};
use crate::{AttrSet, Machine, Value, VmRuntimeCtx};
/// Resolve a decoded operand into a runtime [`Value`].
///
/// The operand decoder ([`crate::BytecodeReader::read_operand_data`])
/// produces a static enum; this function materializes it against the
/// running [`Machine`] (env chain, builtins, scope slots, ...).
#[inline]
pub fn resolve_operand<'gc, M: Machine<'gc>>(
op: &OperandData,
mc: &Mutation<'gc>,
ctx: &impl VmRuntimeCtx,
m: &M,
) -> Value<'gc> {
use OperandData::*;
match *op {
Const(id) => ctx.get_const(id).into(),
BigInt(val) => Value::new_gc(Gc::new(mc, val)),
Local { layer, idx } => m.local(layer, idx),
#[allow(clippy::unwrap_used)]
BuiltinConst(id) => m.builtins().as_gc::<AttrSet>().unwrap().lookup(id).unwrap(),
Builtins => m.builtins(),
ReplBinding(_id) => todo!(),
ScopedImportBinding { slot_id, name } => {
let scope = m.scope_slot(slot_id);
#[allow(clippy::unwrap_used)]
let attrs = scope.as_gc::<AttrSet>().expect("scope must be attrset");
#[allow(clippy::unwrap_used)]
attrs.lookup(name).expect("scoped binding not found")
}
}
}
+89
View File
@@ -0,0 +1,89 @@
use std::ops::ControlFlow;
use std::path::PathBuf;
use fix_error::Error;
use fix_lang::StringId;
use gc_arena::{Collect, Gc};
use hashbrown::HashSet;
use crate::{GcEnv, Thunk};
#[allow(dead_code)]
pub enum VmError {
Catchable(String),
Uncatchable(Box<Error>),
}
impl From<Box<Error>> for VmError {
fn from(e: Box<Error>) -> Self {
VmError::Uncatchable(e)
}
}
impl VmError {
pub fn into_error(self) -> Box<Error> {
match self {
VmError::Catchable(_) => todo!("Check for tryEval catch frames"),
VmError::Uncatchable(e) => e,
}
}
}
pub fn vm_err(msg: impl Into<String>) -> VmError {
VmError::Uncatchable(Error::eval_error(msg.into()))
}
#[derive(Collect, Clone, Copy, Debug, PartialEq, Eq, Default)]
#[collect(require_static)]
pub enum ForceMode {
#[default]
AsIs,
Shallow,
Deep,
}
#[repr(u8)]
pub enum Break {
Force,
Done,
LoadFile,
}
pub type Step = ControlFlow<Break>;
#[allow(dead_code)]
pub struct ErrorFrame {
pub span_id: u32,
pub message: Option<String>,
}
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub struct CallFrame<'gc> {
pub pc: usize,
pub thunk: Option<Gc<'gc, Thunk<'gc>>>,
pub env: GcEnv<'gc>,
}
#[derive(Debug)]
pub struct PendingLoad {
pub path: PathBuf,
pub scope: Option<PendingScope>,
}
#[derive(Debug)]
pub struct PendingScope {
pub keys: HashSet<StringId>,
pub slot_id: u32,
}
/// Extra scope passed to a re-entrant compile from inside a running VM.
///
/// Currently only `ScopedImport` is produced (by the `scopedImport` builtin),
/// but the variant is kept open so REPL bindings could later land here too.
pub enum ExtraScope {
ScopedImport {
keys: HashSet<StringId>,
slot_id: u32,
},
}
+161
View File
@@ -0,0 +1,161 @@
use std::cmp::Ordering;
use smallvec::SmallVec;
/// A string context element
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum StringContextElem {
// Plain store path reference
Opaque {
path: Box<str>,
},
// All outputs of a derivation
// encoded `=<drvPath>`
DrvDeep {
drv_path: Box<str>,
},
// A specific output of a derivation
// encoded `!<output>!<drvPath>`
Built {
drv_path: Box<str>,
output: Box<str>,
},
}
impl StringContextElem {
/// Decode the CppNix wire form (`!out!/p`, `=/p`, `/p`). Falls back to
/// `Opaque` for malformed `!`-prefixed inputs (matching nix-js).
pub fn decode(encoded: &str) -> Self {
if let Some(drv_path) = encoded.strip_prefix('=') {
Self::DrvDeep {
drv_path: drv_path.into(),
}
} else if let Some(rest) = encoded.strip_prefix('!') {
if let Some(second_bang) = rest.find('!') {
Self::Built {
output: rest[..second_bang].into(),
drv_path: rest[second_bang + 1..].into(),
}
} else {
Self::Opaque {
path: encoded.into(),
}
}
} else {
Self::Opaque {
path: encoded.into(),
}
}
}
pub fn encode(&self) -> String {
match self {
Self::Opaque { path } => path.to_string(),
Self::DrvDeep { drv_path } => format!("={drv_path}"),
Self::Built { drv_path, output } => format!("!{output}!{drv_path}"),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct StringContext {
data: SmallVec<[StringContextElem; 1]>,
}
impl IntoIterator for StringContext {
type Item = StringContextElem;
type IntoIter = <SmallVec<[StringContextElem; 1]> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.data.into_iter()
}
}
impl<'a> IntoIterator for &'a StringContext {
type Item = &'a StringContextElem;
type IntoIter = <&'a SmallVec<[StringContextElem; 1]> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.data.iter()
}
}
impl<'a> IntoIterator for &'a mut StringContext {
type Item = &'a mut StringContextElem;
type IntoIter = <&'a mut SmallVec<[StringContextElem; 1]> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.data.iter_mut()
}
}
impl FromIterator<StringContextElem> for StringContext {
fn from_iter<T: IntoIterator<Item = StringContextElem>>(iter: T) -> Self {
Self {
data: iter.into_iter().collect(),
}
}
}
impl StringContext {
pub fn empty() -> &'static Self {
static EMPTY: StringContext = StringContext {
data: SmallVec::new_const(),
};
&EMPTY
}
pub fn new() -> Self {
Self::default()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
pub fn insert(&mut self, elem: StringContextElem) {
match self.data.binary_search(&elem) {
Ok(_) => {}
Err(pos) => self.data.insert(pos, elem),
}
}
pub fn merge(&self, other: &Self) -> Self {
if self.data.is_empty() {
return other.clone();
}
if other.data.is_empty() {
return self.clone();
}
let a = &self.data;
let b = &other.data;
let mut out = SmallVec::with_capacity(a.len() + b.len());
let (mut i, mut j) = (0, 0);
while i < a.len() && j < b.len() {
match a[i].cmp(&b[j]) {
Ordering::Less => {
out.push(a[i].clone());
i += 1;
}
Ordering::Greater => {
out.push(b[j].clone());
j += 1;
}
Ordering::Equal => {
out.push(a[i].clone());
i += 1;
j += 1;
}
}
}
out.extend(a[i..].iter().cloned());
out.extend(b[j..].iter().cloned());
Self { data: out }
}
pub fn iter(&self) -> <&Self as IntoIterator>::IntoIter {
self.into_iter()
}
pub fn iter_mut(&mut self) -> <&mut Self as IntoIterator>::IntoIter {
self.into_iter()
}
}
+170 -113
View File
@@ -1,21 +1,19 @@
#![allow(dead_code)] use std::cell::RefCell;
use std::fmt; use std::fmt;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::mem::size_of; use std::mem::size_of;
use std::ops::Deref; use std::ops::Deref;
use fix_builtins::BuiltinId; use fix_lang::*;
use fix_common::*; use gc_arena::barrier::Unlock;
use gc_arena::collect::Trace; use gc_arena::collect::Trace;
use gc_arena::{Collect, Gc, GcRefLock, Mutation, RefLock}; use gc_arena::{Collect, Gc, GcRefLock, Mutation, RefLock};
use num_enum::TryFromPrimitive;
use smallvec::SmallVec; use smallvec::SmallVec;
use string_interner::Symbol; use string_interner::Symbol;
use string_interner::symbol::SymbolU32; use string_interner::symbol::SymbolU32;
use crate::NixNum;
use crate::boxing::{RawBox, RawStore, RawTag, Value as RawValue}; use crate::boxing::{RawBox, RawStore, RawTag, Value as RawValue};
use crate::string_context::StringContext;
mod private { mod private {
pub trait Cealed {} pub trait Cealed {}
@@ -23,14 +21,12 @@ mod private {
/// # Safety /// # Safety
/// ///
/// TAG must be unique among all implementors. /// [`Self::TAG`] must be unique among all implementors.
#[allow(private_interfaces)] unsafe trait Storable: private::Cealed {
pub unsafe trait Storable: private::Cealed {
const TAG: RawTag; const TAG: RawTag;
} }
#[allow(private_bounds)] trait InlineStorable: Storable + RawStore {}
pub trait InlineStorable: Storable + RawStore {} trait GcStorable: Storable {}
pub trait GcStorable: Storable {}
macro_rules! define_value_types { macro_rules! define_value_types {
( (
@@ -38,7 +34,6 @@ macro_rules! define_value_types {
gc { $($gtype:ty => $gtag:expr, $gname:literal;)* } gc { $($gtype:ty => $gtag:expr, $gname:literal;)* }
) => { ) => {
$( $(
#[allow(private_interfaces)]
unsafe impl Storable for $itype { unsafe impl Storable for $itype {
const TAG: RawTag = $itag; const TAG: RawTag = $itag;
} }
@@ -46,7 +41,6 @@ macro_rules! define_value_types {
impl private::Cealed for $itype {} impl private::Cealed for $itype {}
)* )*
$( $(
#[allow(private_interfaces)]
unsafe impl Storable for $gtype { unsafe impl Storable for $gtype {
const TAG: RawTag = $gtag; const TAG: RawTag = $gtag;
} }
@@ -115,15 +109,16 @@ define_value_types! {
Null => RawTag::P3, "Null"; Null => RawTag::P3, "Null";
StringId => RawTag::P4, "SmallString"; StringId => RawTag::P4, "SmallString";
PrimOp => RawTag::P5, "PrimOp"; PrimOp => RawTag::P5, "PrimOp";
Path => RawTag::P6, "Path";
} }
gc { gc {
i64 => RawTag::P6, "BigInt"; i64 => RawTag::P7, "BigInt";
NixString => RawTag::P7, "String"; NixString => RawTag::N1, "String";
AttrSet<'_> => RawTag::N1, "AttrSet"; AttrSet<'_> => RawTag::N2, "AttrSet";
List<'_> => RawTag::N2, "List"; List<'_> => RawTag::N3, "List";
Thunk<'_> => RawTag::N3, "Thunk"; Thunk<'_> => RawTag::N4, "Thunk";
Closure<'_> => RawTag::N4, "Closure"; Closure<'_> => RawTag::N5, "Closure";
PrimOpApp<'_> => RawTag::N5, "PrimOpApp"; PrimOpApp<'_> => RawTag::N6, "PrimOpApp";
} }
} }
@@ -183,20 +178,16 @@ impl<'gc> Value<'gc> {
} }
#[inline] #[inline]
#[allow(private_bounds)]
pub fn new_inline<T: InlineStorable>(val: T) -> Self { pub fn new_inline<T: InlineStorable>(val: T) -> Self {
Self::from_raw_value(RawValue::store( Self::from_raw_value(RawValue::store(T::TAG, val))
T::TAG,
val,
))
} }
#[inline] #[inline]
#[allow(private_bounds)]
pub fn new_gc<T: GcStorable>(gc: Gc<'gc, T>) -> Self { pub fn new_gc<T: GcStorable>(gc: Gc<'gc, T>) -> Self {
let ptr = Gc::as_ptr(gc); let ptr = Gc::as_ptr(gc);
Self::from_raw_value(RawValue::store( Self::from_raw_value(RawValue::store(T::TAG, ptr))
T::TAG,
ptr,
))
} }
#[inline] #[inline]
@@ -216,6 +207,7 @@ impl<'gc> Value<'gc> {
} }
#[inline] #[inline]
#[allow(private_bounds)]
pub fn is<T: Storable>(self) -> bool { pub fn is<T: Storable>(self) -> bool {
self.tag() == Some(T::TAG) self.tag() == Some(T::TAG)
} }
@@ -228,6 +220,7 @@ impl<'gc> Value<'gc> {
} }
#[inline] #[inline]
#[allow(private_bounds)]
pub fn as_inline<T: InlineStorable>(self) -> Option<T> { pub fn as_inline<T: InlineStorable>(self) -> Option<T> {
if self.is::<T>() { if self.is::<T>() {
Some(unsafe { Some(unsafe {
@@ -240,6 +233,7 @@ impl<'gc> Value<'gc> {
} }
#[inline] #[inline]
#[allow(private_bounds)]
pub fn as_gc<T: GcStorable>(self) -> Option<Gc<'gc, T>> { pub fn as_gc<T: GcStorable>(self) -> Option<Gc<'gc, T>> {
if self.is::<T>() { if self.is::<T>() {
Some(unsafe { Some(unsafe {
@@ -252,6 +246,11 @@ impl<'gc> Value<'gc> {
} }
} }
#[inline]
pub fn to_bits(self) -> u64 {
self.raw.to_bits()
}
#[inline] #[inline]
pub fn as_num(self) -> Option<NixNum> { pub fn as_num(self) -> Option<NixNum> {
if let Some(i) = self.as_inline::<i32>() { if let Some(i) = self.as_inline::<i32>() {
@@ -288,6 +287,8 @@ impl<'gc> Value<'gc> {
NixType::PrimOp NixType::PrimOp
} else if self.is::<NixString>() { } else if self.is::<NixString>() {
NixType::String NixType::String
} else if self.is::<Path>() {
NixType::Path
} else if self.is::<AttrSet>() { } else if self.is::<AttrSet>() {
NixType::AttrSet NixType::AttrSet
} else if self.is::<List>() { } else if self.is::<List>() {
@@ -304,27 +305,29 @@ impl<'gc> Value<'gc> {
} }
#[inline] #[inline]
pub(crate) fn expect_inline<T: InlineStorable>(self) -> Result<T, NixType> { #[allow(private_bounds)]
pub fn expect_inline<T: InlineStorable>(self) -> Result<T, NixType> {
self.as_inline::<T>().ok_or_else(|| self.ty()) self.as_inline::<T>().ok_or_else(|| self.ty())
} }
#[inline] #[inline]
pub(crate) fn expect_gc<T: GcStorable>(self) -> Result<Gc<'gc, T>, NixType> { #[allow(private_bounds)]
pub fn expect_gc<T: GcStorable>(self) -> Result<Gc<'gc, T>, NixType> {
self.as_gc::<T>().ok_or_else(|| self.ty()) self.as_gc::<T>().ok_or_else(|| self.ty())
} }
#[inline] #[inline]
pub(crate) fn expect_num(self) -> Result<NixNum, NixType> { pub fn expect_num(self) -> Result<NixNum, NixType> {
self.as_num().ok_or_else(|| self.ty()) self.as_num().ok_or_else(|| self.ty())
} }
#[inline] #[inline]
pub(crate) fn expect_bool(self) -> Result<bool, NixType> { pub fn expect_bool(self) -> Result<bool, NixType> {
self.as_inline::<bool>().ok_or_else(|| self.ty()) self.as_inline::<bool>().ok_or_else(|| self.ty())
} }
#[inline] #[inline]
pub(crate) fn expect_float(self) -> Result<f64, NixType> { pub fn expect_float(self) -> Result<f64, NixType> {
self.as_float().ok_or_else(|| self.ty()) self.as_float().ok_or_else(|| self.ty())
} }
} }
@@ -347,18 +350,24 @@ impl StaticValue {
Self(Value::new_float(val)) Self(Value::new_float(val))
} }
#[inline] #[inline]
#[allow(private_bounds)]
pub fn new_inline<T: InlineStorable>(val: T) -> Self { pub fn new_inline<T: InlineStorable>(val: T) -> Self {
Self(Value::new_inline(val)) Self(Value::new_inline(val))
} }
#[inline] #[inline]
pub fn new_primop(id: BuiltinId, arity: u8) -> Self { pub fn new_primop(id: BuiltinId, arity: u8, dispatch_ip: u32) -> Self {
Self(Value::new_inline(PrimOp { id, arity })) Self(Value::new_inline(PrimOp {
id,
arity,
dispatch_ip,
}))
} }
#[inline] #[inline]
pub fn is_float(self) -> bool { pub fn is_float(self) -> bool {
self.0.is_float() self.0.is_float()
} }
#[inline] #[inline]
#[allow(private_bounds)]
pub fn is<T: InlineStorable>(self) -> bool { pub fn is<T: InlineStorable>(self) -> bool {
self.0.is::<T>() self.0.is::<T>()
} }
@@ -367,6 +376,7 @@ impl StaticValue {
self.0.as_float() self.0.as_float()
} }
#[inline] #[inline]
#[allow(private_bounds)]
pub fn as_inline<T: InlineStorable>(self) -> Option<T> { pub fn as_inline<T: InlineStorable>(self) -> Option<T> {
self.0.as_inline::<T>() self.0.as_inline::<T>()
} }
@@ -377,7 +387,7 @@ impl StaticValue {
} }
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub(crate) struct Null; pub struct Null;
impl RawStore for Null { impl RawStore for Null {
fn to_val(self, value: &mut RawValue) { fn to_val(self, value: &mut RawValue) {
value.set_data([0; 6]); value.set_data([0; 6]);
@@ -399,26 +409,55 @@ impl RawStore for StringId {
} }
} }
/// Heap-allocated Nix string. /// A canonicalized absolute path. Inline value carrying an interned
/// /// `StringId` whose contents are the path's absolute, dot-resolved form.
/// Stored on the GC heap via `Gc<'gc, NixString>`. The string data itself #[derive(Clone, Copy, Debug, PartialEq, Eq)]
/// lives in a standard `Box<str>` owned by this struct; the GC only manages pub struct Path(pub StringId);
/// the outer allocation.
impl RawStore for Path {
fn to_val(self, value: &mut RawValue) {
self.0.to_val(value);
}
fn from_val(value: &RawValue) -> Self {
Self(StringId::from_val(value))
}
}
#[derive(Collect)] #[derive(Collect)]
#[collect(require_static)] #[collect(require_static)]
pub(crate) struct NixString { pub struct NixString {
data: Box<str>, data: Box<str>,
// TODO: string context for derivation dependency tracking context: StringContext,
} }
impl NixString { impl NixString {
pub(crate) fn new(s: impl Into<Box<str>>) -> Self { pub fn new(s: impl Into<Box<str>>) -> Self {
Self { data: s.into() } Self {
data: s.into(),
context: StringContext::new(),
}
} }
pub(crate) fn as_str(&self) -> &str { /// Construct a `NixString` whose `context` is already sorted+deduped.
/// The caller is responsible for invariant maintenance.
pub fn with_context(s: impl Into<Box<str>>, context: StringContext) -> Self {
Self {
data: s.into(),
context,
}
}
pub fn as_str(&self) -> &str {
&self.data &self.data
} }
pub fn context(&self) -> &StringContext {
&self.context
}
pub fn has_context(&self) -> bool {
!self.context.is_empty()
}
} }
impl fmt::Debug for NixString { impl fmt::Debug for NixString {
@@ -429,35 +468,28 @@ impl fmt::Debug for NixString {
#[derive(Collect, Debug, Default)] #[derive(Collect, Debug, Default)]
#[collect(no_drop)] #[collect(no_drop)]
pub(crate) struct AttrSet<'gc> { pub struct AttrSet<'gc> {
entries: SmallVec<[(StringId, Value<'gc>); 4]>, pub entries: SmallVec<[(StringId, Value<'gc>); 4]>,
}
impl<'gc> Deref for AttrSet<'gc> {
type Target = [(StringId, Value<'gc>)];
fn deref(&self) -> &Self::Target {
&self.entries
}
} }
impl<'gc> AttrSet<'gc> { impl<'gc> AttrSet<'gc> {
pub(crate) fn from_sorted_unchecked(entries: SmallVec<[(StringId, Value<'gc>); 4]>) -> Self { pub fn from_sorted_unchecked(entries: SmallVec<[(StringId, Value<'gc>); 4]>) -> Self {
debug_assert!(entries.is_sorted_by_key(|(key, _)| *key)); debug_assert!(entries.is_sorted_by_key(|(key, _)| *key));
Self { entries } Self { entries }
} }
pub(crate) fn lookup(&self, key: StringId) -> Option<Value<'gc>> { pub fn lookup(&self, key: StringId) -> Option<Value<'gc>> {
self.entries self.entries
.binary_search_by_key(&key, |(k, _)| *k) .binary_search_by_key(&key, |(k, _)| *k)
.ok() .ok()
.map(|i| self.entries[i].1) .map(|i| self.entries[i].1)
} }
pub(crate) fn has(&self, key: StringId) -> bool { pub fn has(&self, key: StringId) -> bool {
self.entries.binary_search_by_key(&key, |(k, _)| *k).is_ok() self.entries.binary_search_by_key(&key, |(k, _)| *k).is_ok()
} }
pub(crate) fn merge(&self, other: &Self, mc: &Mutation<'gc>) -> Gc<'gc, Self> { pub fn merge(&self, other: &Self, mc: &Mutation<'gc>) -> Gc<'gc, Self> {
use std::cmp::Ordering::*; use std::cmp::Ordering::*;
debug_assert!(self.entries.is_sorted_by_key(|(key, _)| *key)); debug_assert!(self.entries.is_sorted_by_key(|(key, _)| *key));
@@ -493,64 +525,70 @@ impl<'gc> AttrSet<'gc> {
} }
#[derive(Collect, Debug, Default)] #[derive(Collect, Debug, Default)]
#[repr(transparent)]
#[collect(no_drop)] #[collect(no_drop)]
pub(crate) struct List<'gc> { pub struct List<'gc> {
pub(crate) inner: SmallVec<[Value<'gc>; 4]>, pub inner: RefLock<SmallVec<[Value<'gc>; 4]>>,
} }
impl<'gc> Deref for List<'gc> {
type Target = SmallVec<[Value<'gc>; 4]>; impl<'gc> List<'gc> {
fn deref(&self) -> &Self::Target { pub fn new(mc: &Mutation<'gc>, data: SmallVec<[Value<'gc>; 4]>) -> Gc<'gc, Self> {
&self.inner Gc::new(
mc,
Self {
inner: RefLock::new(data),
},
)
}
pub fn new_gc(mc: &Mutation<'gc>) -> Gc<'gc, Self> {
Gc::new(mc, Self::default())
} }
} }
pub(crate) type Thunk<'gc> = RefLock<ThunkState<'gc>>; impl<'gc> Unlock for List<'gc> {
type Unlocked = RefCell<SmallVec<[Value<'gc>; 4]>>;
unsafe fn unlock_unchecked(&self) -> &Self::Unlocked {
unsafe { self.inner.unlock_unchecked() }
}
}
pub type Thunk<'gc> = RefLock<ThunkState<'gc>>;
#[derive(Collect, Debug)] #[derive(Collect, Debug)]
#[collect(no_drop)] #[collect(no_drop)]
pub(crate) enum ThunkState<'gc> { pub enum ThunkState<'gc> {
Pending { Pending { ip: usize, env: GcEnv<'gc> },
ip: usize, Apply { func: Value<'gc>, arg: Value<'gc> },
env: GcEnv<'gc>,
with_env: Option<GcWithEnv<'gc>>,
},
Apply {
func: Value<'gc>,
arg: Value<'gc>,
},
Blackhole, Blackhole,
Evaluated(StrictValue<'gc>), Evaluated(StrictValue<'gc>),
} }
#[derive(Collect, Debug)] #[derive(Collect, Debug)]
#[collect(no_drop)] #[collect(no_drop)]
pub(crate) struct Env<'gc> { pub struct Env<'gc> {
pub(crate) locals: SmallVec<[Value<'gc>; 4]>, pub locals: SmallVec<[Value<'gc>; 4]>,
pub(crate) prev: Option<GcEnv<'gc>>, pub prev: Option<GcEnv<'gc>>,
} }
pub(crate) type GcEnv<'gc> = GcRefLock<'gc, Env<'gc>>; pub type GcEnv<'gc> = GcRefLock<'gc, Env<'gc>>;
#[derive(Collect, Debug)] #[derive(Collect, Debug)]
#[collect(no_drop)] #[collect(no_drop)]
pub(crate) struct WithEnv<'gc> { pub struct WithEnv<'gc> {
pub(crate) env: Value<'gc>, pub env: Value<'gc>,
pub(crate) prev: Option<GcWithEnv<'gc>>, pub prev: Option<GcWithEnv<'gc>>,
} }
pub(crate) type GcWithEnv<'gc> = Gc<'gc, WithEnv<'gc>>; pub type GcWithEnv<'gc> = Gc<'gc, WithEnv<'gc>>;
impl<'gc> Env<'gc> { impl<'gc> Env<'gc> {
pub(crate) fn empty() -> Self { pub fn empty() -> Self {
Env { Env {
locals: SmallVec::new(), locals: SmallVec::new(),
prev: None, prev: None,
} }
} }
pub(crate) fn with_arg( pub fn with_arg(arg: Value<'gc>, n_locals: u32, prev: Gc<'gc, RefLock<Env<'gc>>>) -> Self {
arg: Value<'gc>,
n_locals: u32,
prev: Gc<'gc, RefLock<Env<'gc>>>,
) -> Self {
let mut locals = smallvec::smallvec![Value::default(); 1 + n_locals as usize]; let mut locals = smallvec::smallvec![Value::default(); 1 + n_locals as usize];
locals[0] = arg; locals[0] = arg;
Env { Env {
@@ -562,57 +600,69 @@ impl<'gc> Env<'gc> {
#[derive(Collect, Debug)] #[derive(Collect, Debug)]
#[collect(no_drop)] #[collect(no_drop)]
pub(crate) struct Closure<'gc> { pub struct Closure<'gc> {
pub(crate) ip: u32, pub ip: u32,
pub(crate) n_locals: u32, pub n_locals: u32,
pub(crate) env: Gc<'gc, RefLock<Env<'gc>>>, pub env: Gc<'gc, RefLock<Env<'gc>>>,
pub(crate) pattern: Option<Gc<'gc, PatternInfo>>, pub pattern: Option<Gc<'gc, PatternInfo>>,
} }
#[derive(Collect, Debug)] #[derive(Collect, Debug)]
#[collect(require_static)] #[collect(require_static)]
pub(crate) struct PatternInfo { pub struct PatternInfo {
pub(crate) required: SmallVec<[StringId; 4]>, pub required: SmallVec<[StringId; 4]>,
pub(crate) optional: SmallVec<[StringId; 4]>, pub optional: SmallVec<[StringId; 4]>,
pub(crate) ellipsis: bool, pub ellipsis: bool,
pub(crate) param_spans: Box<[(StringId, u32)]>, pub param_spans: Box<[(StringId, u32)]>,
} }
#[repr(packed, Rust)]
#[derive(Clone, Copy, Debug, Collect)] #[derive(Clone, Copy, Debug, Collect)]
#[collect(require_static)] #[collect(require_static)]
pub(crate) struct PrimOp { pub struct PrimOp {
pub(crate) id: BuiltinId, pub id: BuiltinId,
pub(crate) arity: u8, pub arity: u8,
pub dispatch_ip: u32,
} }
impl RawStore for PrimOp { impl RawStore for PrimOp {
fn to_val(self, value: &mut RawValue) { fn to_val(self, value: &mut RawValue) {
value.set_data([0, 0, 0, 0, self.id as u8, self.arity]); let bytes = self.dispatch_ip.to_le_bytes();
value.set_data([
self.id as u8,
self.arity,
bytes[0],
bytes[1],
bytes[2],
bytes[3],
]);
} }
fn from_val(value: &RawValue) -> Self { fn from_val(value: &RawValue) -> Self {
let [.., id, arity] = *value.data(); let [id, arity, bytes @ ..] = *value.data();
Self { Self {
id: BuiltinId::try_from_primitive(id).expect("invalid BuiltinId"), id: BuiltinId::try_from(id).expect("invalid BuiltinId"),
arity, arity,
dispatch_ip: u32::from_le_bytes(bytes),
} }
} }
} }
#[derive(Collect, Debug)] #[derive(Collect, Debug)]
#[collect(no_drop)] #[collect(no_drop)]
pub(crate) struct PrimOpApp<'gc> { pub struct PrimOpApp<'gc> {
pub(crate) primop: PrimOp, pub primop: PrimOp,
pub(crate) args: SmallVec<[Value<'gc>; 2]>, pub arity: u8,
pub args: [Value<'gc>; 3],
} }
#[derive(Copy, Clone, Default, Collect)] #[derive(Copy, Clone, Default, Collect)]
#[repr(transparent)] #[repr(transparent)]
#[collect(no_drop)] #[collect(no_drop)]
pub(crate) struct StrictValue<'gc>(Value<'gc>); pub struct StrictValue<'gc>(Value<'gc>);
impl<'gc> StrictValue<'gc> { impl<'gc> StrictValue<'gc> {
#[inline] #[inline]
pub(crate) fn relax(self) -> Value<'gc> { pub fn relax(self) -> Value<'gc> {
self.0 self.0
} }
} }
@@ -633,12 +683,13 @@ impl fmt::Debug for StrictValue<'_> {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Collect)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Collect)]
#[collect(require_static)] #[collect(require_static)]
pub(crate) enum NixType { pub enum NixType {
Int, Int,
Float, Float,
Bool, Bool,
Null, Null,
String, String,
Path,
AttrSet, AttrSet,
List, List,
Thunk, Thunk,
@@ -656,6 +707,7 @@ impl NixType {
Bool => "a boolean", Bool => "a boolean",
Null => "null", Null => "null",
String => "a string", String => "a string",
Path => "a path",
AttrSet => "a set", AttrSet => "a set",
List => "a list", List => "a list",
Thunk => "a thunk", Thunk => "a thunk",
@@ -671,3 +723,8 @@ impl std::fmt::Display for NixType {
write!(f, "{}", self.display()) write!(f, "{}", self.display())
} }
} }
pub enum NixNum {
Int(i64),
Float(f64),
}
+7 -10
View File
@@ -3,19 +3,16 @@ name = "fix-vm"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[features]
tailcall = []
[dependencies] [dependencies]
gc-arena = { workspace = true } gc-arena = { workspace = true }
hashbrown = { workspace = true } hashbrown = { workspace = true }
num_enum = { workspace = true }
smallvec = { workspace = true } smallvec = { workspace = true }
string-interner = { workspace = true } sysinfo = { version = "0.38", default-features = false, features = ["system"] }
likely_stable = { workspace = true }
sptr = "0.3"
fix-builtins = { path = "../fix-builtins" } fix-bytecode = { path = "../fix-bytecode" }
fix-codegen = { path = "../fix-codegen" }
fix-common = { path = "../fix-common" }
fix-error = { path = "../fix-error" } fix-error = { path = "../fix-error" }
fix-lang = { path = "../fix-lang" }
fix-runtime = { path = "../fix-runtime" }
[features]
tailcall = []
-149
View File
@@ -1,149 +0,0 @@
use fix_codegen::OperandType;
use fix_common::StringId;
use num_enum::TryFromPrimitive;
use string_interner::Symbol as _;
use crate::OperandData;
pub(crate) struct BytecodeReader<'a> {
bytecode: &'a [u8],
pc: usize,
inst_start_pc: usize,
}
impl<'a> BytecodeReader<'a> {
#[cfg_attr(feature = "tailcall", allow(dead_code))]
pub(crate) fn new(bytecode: &'a [u8], pc: usize) -> Self {
Self {
bytecode,
pc,
inst_start_pc: pc,
}
}
#[inline(always)]
#[cfg_attr(not(feature = "tailcall"), allow(dead_code))]
pub(crate) fn from_after_op(bytecode: &'a [u8], inst_start_pc: usize) -> Self {
Self {
bytecode,
pc: inst_start_pc + 1,
inst_start_pc,
}
}
#[inline(always)]
#[cfg_attr(debug_assertions, track_caller)]
fn read_array<const N: usize>(&mut self) -> [u8; N] {
let ret = self.bytecode[self.pc..self.pc + N]
.try_into()
.expect("read_array failed");
self.pc += N;
ret
}
#[inline(always)]
#[cfg_attr(feature = "tailcall", allow(dead_code))]
pub(crate) fn read_op(&mut self) -> fix_codegen::Op {
use fix_codegen::Op;
self.inst_start_pc = self.pc;
let byte = self.bytecode[self.pc];
if !likely_stable::likely((0..Op::Illegal as u8).contains(&byte)) {
panic!("unknown opcode: {byte:#04x}")
}
self.pc += 1;
unsafe { std::mem::transmute::<u8, Op>(byte) }
}
#[inline(always)]
pub(crate) fn read_u8(&mut self) -> u8 {
let val = self.bytecode[self.pc];
self.pc += 1;
val
}
#[inline(always)]
pub(crate) fn read_u16(&mut self) -> u16 {
u16::from_le_bytes(self.read_array())
}
#[inline(always)]
pub(crate) fn read_u32(&mut self) -> u32 {
u32::from_le_bytes(self.read_array())
}
#[inline(always)]
pub(crate) fn read_i32(&mut self) -> i32 {
i32::from_le_bytes(self.read_array())
}
#[inline(always)]
pub(crate) fn read_i64(&mut self) -> i64 {
i64::from_le_bytes(self.read_array())
}
#[inline(always)]
pub(crate) fn read_f64(&mut self) -> f64 {
f64::from_le_bytes(self.read_array())
}
#[inline(always)]
pub(crate) fn read_string_id(&mut self) -> StringId {
let raw = self.read_u32();
#[allow(clippy::unwrap_used)]
StringId(string_interner::symbol::SymbolU32::try_from_usize(raw as usize).unwrap())
}
#[inline(always)]
pub(crate) fn read_operand_data<C: crate::VmContext>(&mut self, ctx: &C) -> OperandData {
let tag = self.read_u8();
let Ok(ty) = OperandType::try_from_primitive(tag)
.map_err(|err| panic!("unknown operand tag: {:#04x}", err.number));
match ty {
OperandType::Const => {
let id = self.read_u32();
OperandData::Const(ctx.get_const(id))
}
OperandType::Local => {
let layer = self.read_u8();
let idx = self.read_u32();
OperandData::Local { layer, idx }
}
OperandType::Builtins => OperandData::Builtins,
OperandType::BigInt => {
let val = self.read_i64();
OperandData::BigInt(val)
}
}
}
#[inline(always)]
pub(crate) fn read_attr_key_data<C: crate::VmContext>(
&mut self,
ctx: &C,
) -> crate::AttrKeyData {
use fix_codegen::AttrKeyType;
let tag = self.read_u8();
let ty = AttrKeyType::try_from_primitive(tag)
.unwrap_or_else(|err| panic!("unknown key tag: {:#04x}", err.number));
match ty {
AttrKeyType::Static => crate::AttrKeyData::Static(self.read_string_id()),
AttrKeyType::Dynamic => crate::AttrKeyData::Dynamic(self.read_operand_data(ctx)),
}
}
pub(crate) fn pc(&self) -> usize {
self.pc
}
pub(crate) fn set_pc(&mut self, pc: usize) {
self.pc = pc;
}
pub(crate) fn inst_start_pc(&self) -> usize {
self.inst_start_pc
}
pub(crate) fn bytecode(&self) -> &'a [u8] {
self.bytecode
}
}
+44 -34
View File
@@ -2,11 +2,12 @@
use gc_arena::Mutation; use gc_arena::Mutation;
use crate::{Break, BytecodeReader, Step, Vm, VmContext}; use crate::{Break, BytecodeReader, Step, Vm, VmRuntimeCtx};
pub(crate) enum TailResult { pub(crate) enum TailResult {
YieldFuel(u32), YieldFuel(u32),
Done, Done,
LoadFile,
} }
pub(crate) type OpFn<'gc, C> = extern "rust-preserve-none" fn( pub(crate) type OpFn<'gc, C> = extern "rust-preserve-none" fn(
@@ -19,9 +20,9 @@ pub(crate) type OpFn<'gc, C> = extern "rust-preserve-none" fn(
u32, u32,
) -> TailResult; ) -> TailResult;
pub(crate) struct DispatchTable<'gc, C: VmContext>(pub(crate) [OpFn<'gc, C>; 256]); pub(crate) struct DispatchTable<'gc, C: VmRuntimeCtx>(pub(crate) [OpFn<'gc, C>; 256]);
extern "rust-preserve-none" fn op_illegal<'gc, C: VmContext>( extern "rust-preserve-none" fn op_illegal<'gc, C: VmRuntimeCtx>(
_vm: &mut Vm<'gc>, _vm: &mut Vm<'gc>,
_mc: &Mutation<'gc>, _mc: &Mutation<'gc>,
_ctx: &mut C, _ctx: &mut C,
@@ -37,6 +38,7 @@ macro_rules! tail_dispatch_after {
($result:expr, $new_pc:expr, $vm:ident, $mc:ident, $ctx:ident, $bc:ident, $table:ident, $fuel:ident) => {{ ($result:expr, $new_pc:expr, $vm:ident, $mc:ident, $ctx:ident, $bc:ident, $table:ident, $fuel:ident) => {{
match $result { match $result {
Step::Continue(()) | Step::Break(Break::Force) => {} Step::Continue(()) | Step::Break(Break::Force) => {}
Step::Break(Break::LoadFile) => return TailResult::LoadFile,
Step::Break(Break::Done) => return TailResult::Done, Step::Break(Break::Done) => return TailResult::Done,
} }
let new_pc: u32 = $new_pc; let new_pc: u32 = $new_pc;
@@ -50,7 +52,7 @@ macro_rules! tail_dispatch_after {
macro_rules! tail_fn { macro_rules! tail_fn {
($name:ident, ()) => { ($name:ident, ()) => {
extern "rust-preserve-none" fn $name<'gc, C: VmContext>( extern "rust-preserve-none" fn $name<'gc, C: VmRuntimeCtx>(
vm: &mut Vm<'gc>, vm: &mut Vm<'gc>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
ctx: &mut C, ctx: &mut C,
@@ -59,12 +61,12 @@ macro_rules! tail_fn {
pc: u32, pc: u32,
fuel: u32, fuel: u32,
) -> TailResult { ) -> TailResult {
let result = vm.$name(); let result = crate::instructions::$name(vm);
tail_dispatch_after!(result, pc + 1, vm, mc, ctx, bc, table, fuel) tail_dispatch_after!(result, pc + 1, vm, mc, ctx, bc, table, fuel)
} }
}; };
($name:ident, (reader)) => { ($name:ident, (reader)) => {
extern "rust-preserve-none" fn $name<'gc, C: VmContext>( extern "rust-preserve-none" fn $name<'gc, C: VmRuntimeCtx>(
vm: &mut Vm<'gc>, vm: &mut Vm<'gc>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
ctx: &mut C, ctx: &mut C,
@@ -74,12 +76,12 @@ macro_rules! tail_fn {
fuel: u32, fuel: u32,
) -> TailResult { ) -> TailResult {
let mut reader = BytecodeReader::from_after_op(bc, pc as usize); let mut reader = BytecodeReader::from_after_op(bc, pc as usize);
let result = vm.$name(&mut reader); let result = crate::instructions::$name(vm, &mut reader);
tail_dispatch_after!(result, reader.pc() as u32, vm, mc, ctx, bc, table, fuel) tail_dispatch_after!(result, reader.pc() as u32, vm, mc, ctx, bc, table, fuel)
} }
}; };
($name:ident, (reader, mc)) => { ($name:ident, (reader, mc)) => {
extern "rust-preserve-none" fn $name<'gc, C: VmContext>( extern "rust-preserve-none" fn $name<'gc, C: VmRuntimeCtx>(
vm: &mut Vm<'gc>, vm: &mut Vm<'gc>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
ctx: &mut C, ctx: &mut C,
@@ -89,12 +91,12 @@ macro_rules! tail_fn {
fuel: u32, fuel: u32,
) -> TailResult { ) -> TailResult {
let mut reader = BytecodeReader::from_after_op(bc, pc as usize); let mut reader = BytecodeReader::from_after_op(bc, pc as usize);
let result = vm.$name(&mut reader, mc); let result = crate::instructions::$name(vm, &mut reader, mc);
tail_dispatch_after!(result, reader.pc() as u32, vm, mc, ctx, bc, table, fuel) tail_dispatch_after!(result, reader.pc() as u32, vm, mc, ctx, bc, table, fuel)
} }
}; };
($name:ident, (ctx, reader, mc)) => { ($name:ident, (ctx, reader, mc)) => {
extern "rust-preserve-none" fn $name<'gc, C: VmContext>( extern "rust-preserve-none" fn $name<'gc, C: VmRuntimeCtx>(
vm: &mut Vm<'gc>, vm: &mut Vm<'gc>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
ctx: &mut C, ctx: &mut C,
@@ -104,12 +106,12 @@ macro_rules! tail_fn {
fuel: u32, fuel: u32,
) -> TailResult { ) -> TailResult {
let mut reader = BytecodeReader::from_after_op(bc, pc as usize); let mut reader = BytecodeReader::from_after_op(bc, pc as usize);
let result = vm.$name(ctx, &mut reader, mc); let result = crate::instructions::$name(vm, ctx, &mut reader, mc);
tail_dispatch_after!(result, reader.pc() as u32, vm, mc, ctx, bc, table, fuel) tail_dispatch_after!(result, reader.pc() as u32, vm, mc, ctx, bc, table, fuel)
} }
}; };
($name:ident, (ctx)) => { ($name:ident, (ctx)) => {
extern "rust-preserve-none" fn $name<'gc, C: VmContext>( extern "rust-preserve-none" fn $name<'gc, C: VmRuntimeCtx>(
vm: &mut Vm<'gc>, vm: &mut Vm<'gc>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
ctx: &mut C, ctx: &mut C,
@@ -118,7 +120,7 @@ macro_rules! tail_fn {
pc: u32, pc: u32,
fuel: u32, fuel: u32,
) -> TailResult { ) -> TailResult {
let result = vm.$name(ctx); let result = crate::instructions::$name(vm, ctx);
tail_dispatch_after!(result, pc + 1, vm, mc, ctx, bc, table, fuel) tail_dispatch_after!(result, pc + 1, vm, mc, ctx, bc, table, fuel)
} }
}; };
@@ -142,14 +144,20 @@ tail_fn!(op_make_closure, (reader, mc));
tail_fn!(op_make_pattern_closure, (reader, mc)); tail_fn!(op_make_pattern_closure, (reader, mc));
tail_fn!(op_call, (ctx, reader, mc)); tail_fn!(op_call, (ctx, reader, mc));
tail_fn!(op_dispatch_primop, (ctx, reader, mc));
tail_fn!(op_return, (ctx, reader, mc)); tail_fn!(op_return, (ctx, reader, mc));
tail_fn!(op_make_attrs, (ctx, reader, mc)); tail_fn!(op_make_attrs, (ctx, reader, mc));
tail_fn!(op_make_empty_attrs, ()); tail_fn!(op_make_empty_attrs, ());
tail_fn!(op_select_static, (ctx, reader, mc)); tail_fn!(op_select_static, (ctx, reader, mc));
tail_fn!(op_select_dynamic, (ctx, reader, mc)); tail_fn!(op_select_dynamic, (ctx, reader, mc));
tail_fn!(op_has_attr_path_static, (ctx, reader, mc));
tail_fn!(op_has_attr_path_dynamic, (ctx, reader, mc));
tail_fn!(op_jump_if_select_failed, (reader));
tail_fn!(op_jump_if_select_succeeded, (reader)); tail_fn!(op_jump_if_select_succeeded, (reader));
tail_fn!(op_has_attr, (reader)); tail_fn!(op_has_attr_static, (reader, mc));
tail_fn!(op_has_attr_dynamic, (ctx, reader, mc));
tail_fn!(op_has_attr_resolve, ());
tail_fn!(op_make_list, (ctx, reader, mc)); tail_fn!(op_make_list, (ctx, reader, mc));
tail_fn!(op_make_empty_list, ()); tail_fn!(op_make_empty_list, ());
@@ -167,45 +175,43 @@ tail_fn!(op_geq, (ctx, reader, mc));
tail_fn!(op_concat, (reader, mc)); tail_fn!(op_concat, (reader, mc));
tail_fn!(op_update, (reader, mc)); tail_fn!(op_update, (reader, mc));
tail_fn!(op_neg, ()); tail_fn!(op_neg, (reader, mc));
tail_fn!(op_not, ()); tail_fn!(op_not, (reader, mc));
tail_fn!(op_jump_if_false, (reader, mc)); tail_fn!(op_jump_if_false, (reader, mc));
tail_fn!(op_jump_if_true, (reader, mc)); tail_fn!(op_jump_if_true, (reader, mc));
tail_fn!(op_jump, (reader)); tail_fn!(op_jump, (reader));
tail_fn!(op_coerce_to_string, (reader, mc));
tail_fn!(op_concat_strings, (ctx, reader, mc)); tail_fn!(op_concat_strings, (ctx, reader, mc));
tail_fn!(op_resolve_path, (ctx)); tail_fn!(op_resolve_path, (ctx, reader, mc));
tail_fn!(op_assert, (reader)); tail_fn!(op_assert, (ctx, reader, mc));
tail_fn!(op_push_with, (ctx, reader, mc));
tail_fn!(op_pop_with, ());
tail_fn!(op_lookup_with, (ctx, reader, mc)); tail_fn!(op_lookup_with, (ctx, reader, mc));
tail_fn!(op_prepare_with, ());
tail_fn!(op_load_builtins, ()); tail_fn!(op_load_builtins, ());
tail_fn!(op_load_builtin, (reader)); tail_fn!(op_load_builtin, (reader));
tail_fn!(op_mk_pos, (reader));
tail_fn!(op_load_repl_binding, (reader)); tail_fn!(op_load_repl_binding, (reader));
tail_fn!(op_load_scoped_binding, (reader)); tail_fn!(op_load_scoped_binding, (ctx, reader, mc));
macro_rules! table { macro_rules! table {
($($variant:ident => $fn:ident),* $(,)?) => { ($($variant:ident => $fn:ident),* $(,)?) => {
impl<'gc, C: VmContext> DispatchTable<'gc, C> { impl<'gc, C: VmRuntimeCtx> DispatchTable<'gc, C> {
pub(crate) const NEW: Self = { pub(crate) const NEW: Self = {
let mut arr: [OpFn<'gc, C>; 256] = [op_illegal; 256]; let mut arr: [OpFn<'gc, C>; 256] = [op_illegal; 256];
$( arr[fix_codegen::Op::$variant as usize] = $fn; )* $( arr[fix_bytecode::Op::$variant as usize] = $fn; )*
DispatchTable(arr) DispatchTable(arr)
}; };
} }
// Exhaustiveness check: fails to compile if `fix_codegen::Op` gains, // Exhaustiveness check: fails to compile if `fix_bytecode::Op` gains,
// loses, or renames a variant that isn't wired up above. // loses, or renames a variant that isn't wired up above.
#[allow(dead_code)] #[allow(dead_code)]
const _: fn(fix_codegen::Op) = |op| match op { const _: fn(fix_bytecode::Op) = |op| match op {
$( fix_codegen::Op::$variant => (), )* $( fix_bytecode::Op::$variant => (), )*
}; };
}; };
} }
@@ -229,14 +235,20 @@ table! {
MakePatternClosure => op_make_pattern_closure, MakePatternClosure => op_make_pattern_closure,
Call => op_call, Call => op_call,
DispatchPrimOp => op_dispatch_primop,
Return => op_return, Return => op_return,
MakeAttrs => op_make_attrs, MakeAttrs => op_make_attrs,
MakeEmptyAttrs => op_make_empty_attrs, MakeEmptyAttrs => op_make_empty_attrs,
SelectStatic => op_select_static, SelectStatic => op_select_static,
SelectDynamic => op_select_dynamic, SelectDynamic => op_select_dynamic,
HasAttrPathStatic => op_has_attr_path_static,
HasAttrPathDynamic => op_has_attr_path_dynamic,
HasAttrStatic => op_has_attr_static,
HasAttrDynamic => op_has_attr_dynamic,
HasAttrResolve => op_has_attr_resolve,
JumpIfSelectSucceeded => op_jump_if_select_succeeded, JumpIfSelectSucceeded => op_jump_if_select_succeeded,
HasAttr => op_has_attr, JumpIfSelectFailed => op_jump_if_select_failed,
MakeList => op_make_list, MakeList => op_make_list,
MakeEmptyList => op_make_empty_list, MakeEmptyList => op_make_empty_list,
@@ -261,27 +273,25 @@ table! {
JumpIfTrue => op_jump_if_true, JumpIfTrue => op_jump_if_true,
Jump => op_jump, Jump => op_jump,
CoerceToString => op_coerce_to_string,
ConcatStrings => op_concat_strings, ConcatStrings => op_concat_strings,
ResolvePath => op_resolve_path, ResolvePath => op_resolve_path,
Assert => op_assert, Assert => op_assert,
PushWith => op_push_with,
PopWith => op_pop_with,
LookupWith => op_lookup_with, LookupWith => op_lookup_with,
PrepareWith => op_prepare_with,
LoadBuiltins => op_load_builtins, LoadBuiltins => op_load_builtins,
LoadBuiltin => op_load_builtin, LoadBuiltin => op_load_builtin,
MkPos => op_mk_pos,
LoadReplBinding => op_load_repl_binding, LoadReplBinding => op_load_repl_binding,
LoadScopedBinding => op_load_scoped_binding, LoadScopedBinding => op_load_scoped_binding,
Illegal => op_illegal, Illegal => op_illegal,
} }
pub(crate) fn run_tailcall<'gc, C: VmContext>( pub(crate) fn run_tailcall<'gc, C: VmRuntimeCtx>(
vm: &mut Vm<'gc>, vm: &mut Vm<'gc>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
ctx: &mut C, ctx: &mut C,
-7
View File
@@ -1,7 +0,0 @@
use fix_error::Error;
use crate::VmError;
pub(crate) fn vm_err(msg: impl Into<String>) -> VmError {
VmError::Uncatchable(Error::eval_error(msg.into()))
}
+187 -206
View File
@@ -1,84 +1,104 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use gc_arena::{Gc, Mutation}; use fix_runtime::*;
use gc_arena::{Gc, Mutation, RefLock};
use crate::value::*; use crate::{BytecodeReader, NixNum, Step, VmError, VmRuntimeCtx};
use crate::{BytecodeReader, NixNum, Step, VmContextExt, VmError};
impl<'gc> crate::Vm<'gc> { #[inline(always)]
#[inline(always)] pub(crate) fn op_add<'gc, M: Machine<'gc>>(
pub(crate) fn op_add( m: &mut M,
&mut self, ctx: &mut impl VmRuntimeCtx,
ctx: &mut impl crate::VmContext,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let (lhs, rhs) = self.try_force::<(StrictValue, StrictValue)>(reader, mc)?; let (lhs, rhs) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
if let (Some(ls), Some(rs)) = ( // if the LHS is a path, the result is a path obtained by
VmContextExt::get_string(ctx, lhs), // canonicalizing the concatenated string. RHS may be a path or a
VmContextExt::get_string(ctx, rhs), // string. (A `string + path` keeps the string-typed result, handled
) { // by the next branch.)
let ns = Gc::new(mc, crate::NixString::new(format!("{ls}{rs}"))); if lhs.is::<Path>() {
self.push(Value::new_gc(ns)); let (Some(ls), Some(rs)) = (ctx.get_string_or_path(lhs), ctx.get_string_or_path(rhs))
else {
return m.finish_err(fix_error::Error::eval_error(format!(
"cannot append {} to a path",
rhs.ty()
)));
};
let combined = format!("{ls}{rs}");
let canon = canon_path_str(&combined);
let sid = ctx.intern_string(canon);
m.push(Value::new_inline(fix_runtime::Path(sid)));
return Step::Continue(());
}
if let (Some(ls), Some(rs)) = (ctx.get_string(lhs), ctx.get_string_or_path(rhs)) {
let merged = ctx
.get_string_context(lhs)
.merge(ctx.get_string_context(rhs));
let ns = Gc::new(
mc,
crate::NixString::with_context(format!("{ls}{rs}"), merged),
);
m.push(Value::new_gc(ns));
return Step::Continue(()); return Step::Continue(());
} }
let res = numeric_binop(lhs, rhs, mc, i64::wrapping_add, |a, b| a + b); let res = numeric_binop(lhs, rhs, mc, i64::wrapping_add, |a, b| a + b);
match res { match res {
Ok(val) => { Ok(val) => {
self.push(val); m.push(val);
Step::Continue(()) Step::Continue(())
} }
Err(e) => self.finish_vm_err(e), Err(e) => m.finish_vm_err(e),
}
} }
}
#[inline(always)] #[inline(always)]
pub(crate) fn op_sub( pub(crate) fn op_sub<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
self.op_arith(reader, mc, i64::wrapping_sub, |a, b| a - b) op_arith(m, reader, mc, i64::wrapping_sub, |a, b| a - b)
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_mul( pub(crate) fn op_mul<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
self.op_arith(reader, mc, i64::wrapping_mul, |a, b| a * b) op_arith(m, reader, mc, i64::wrapping_mul, |a, b| a * b)
} }
#[inline(always)] #[inline(always)]
fn op_arith( fn op_arith<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
int_op: fn(i64, i64) -> i64, int_op: fn(i64, i64) -> i64,
float_op: fn(f64, f64) -> f64, float_op: fn(f64, f64) -> f64,
) -> Step { ) -> Step {
let (lhs, rhs) = self.try_force::<(StrictValue, StrictValue)>(reader, mc)?; let (lhs, rhs) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
let res = numeric_binop(lhs, rhs, mc, int_op, float_op); let res = numeric_binop(lhs, rhs, mc, int_op, float_op);
match res { match res {
Ok(val) => { Ok(val) => {
self.push(val); m.push(val);
Step::Continue(()) Step::Continue(())
} }
Err(e) => self.finish_vm_err(e), Err(e) => m.finish_vm_err(e),
}
} }
}
#[inline(always)] #[inline(always)]
pub(crate) fn op_div( pub(crate) fn op_div<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let (lhs, rhs) = self.try_force::<(StrictValue, StrictValue)>(reader, mc)?; let (lhs, rhs) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
match (get_num(rhs), get_num(lhs)) { match (get_num(lhs), get_num(rhs)) {
(_, Some(NixNum::Int(0))) | (_, Some(NixNum::Float(0.))) => { (_, Some(NixNum::Int(0))) | (_, Some(NixNum::Float(0.))) => {
return self.finish_vm_err(VmError::Uncatchable(fix_error::Error::eval_error( return m.finish_vm_err(VmError::Uncatchable(fix_error::Error::eval_error(
"division by zero", "division by zero",
))); )));
} }
@@ -87,199 +107,151 @@ impl<'gc> crate::Vm<'gc> {
let res = numeric_binop(lhs, rhs, mc, |a, b| a / b, |a, b| a / b); let res = numeric_binop(lhs, rhs, mc, |a, b| a / b, |a, b| a / b);
match res { match res {
Ok(val) => { Ok(val) => {
self.push(val); m.push(val);
Step::Continue(()) Step::Continue(())
} }
Err(e) => self.finish_vm_err(e), Err(e) => m.finish_vm_err(e),
}
} }
}
#[inline(always)] #[inline(always)]
pub(crate) fn op_eq( pub(crate) fn op_eq<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
ctx: &mut impl crate::VmContext, ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let (lhs, rhs) = self.try_force::<(StrictValue, StrictValue)>(reader, mc)?; let (lhs, rhs) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
let eq = match self.values_equal(ctx, lhs, rhs) { crate::primops::start_eq(m, ctx, reader, mc, lhs, rhs, false)
Ok(eq) => eq, }
Err(e) => return self.finish_vm_err(e),
};
self.push(Value::new_inline(eq));
Step::Continue(())
}
#[inline(always)] #[inline(always)]
pub(crate) fn op_neq( pub(crate) fn op_neq<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
ctx: &mut impl crate::VmContext, ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let (lhs, rhs) = self.try_force::<(StrictValue, StrictValue)>(reader, mc)?; let (lhs, rhs) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
let eq = match self.values_equal(ctx, lhs, rhs) { crate::primops::start_eq(m, ctx, reader, mc, lhs, rhs, true)
Ok(eq) => eq, }
Err(e) => return self.finish_vm_err(e),
};
self.push(Value::new_inline(!eq));
Step::Continue(())
}
#[inline(always)] #[inline(always)]
pub(crate) fn op_lt( pub(crate) fn op_lt<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
ctx: &mut impl crate::VmContext, ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
self.compare_values(ctx, reader, mc, Ordering::is_lt) compare_values(m, ctx, reader, mc, Ordering::is_lt)
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_gt( pub(crate) fn op_gt<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
ctx: &mut impl crate::VmContext, ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
self.compare_values(ctx, reader, mc, Ordering::is_gt) compare_values(m, ctx, reader, mc, Ordering::is_gt)
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_leq( pub(crate) fn op_leq<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
ctx: &mut impl crate::VmContext, ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
self.compare_values(ctx, reader, mc, Ordering::is_le) compare_values(m, ctx, reader, mc, Ordering::is_le)
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_geq( pub(crate) fn op_geq<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
ctx: &mut impl crate::VmContext, ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
self.compare_values(ctx, reader, mc, Ordering::is_ge) compare_values(m, ctx, reader, mc, Ordering::is_ge)
} }
fn compare_values( fn compare_values<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
ctx: &impl crate::VmContext, ctx: &impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
pred: fn(Ordering) -> bool, pred: fn(Ordering) -> bool,
) -> Step { ) -> Step {
let (lhs, rhs) = self.try_force::<(StrictValue, StrictValue)>(reader, mc)?; let (lhs, rhs) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
match self.compare_values_inner(ctx, pred, lhs, rhs) { match compare_values_inner(m, ctx, pred, lhs, rhs) {
Ok(()) => Step::Continue(()), Ok(()) => Step::Continue(()),
Err(e) => self.finish_vm_err(e), Err(e) => m.finish_vm_err(e),
}
} }
}
#[inline(always)] #[inline(always)]
pub(crate) fn op_concat( pub(crate) fn op_concat<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let (l, r) = self.try_force::<(Gc<List>, Gc<List>)>(reader, mc)?; let (l, r) = m.force_and_retry::<(Gc<List>, Gc<List>)>(reader, mc)?;
let mut items = smallvec::SmallVec::new(); let mut items = smallvec::SmallVec::new();
items.extend_from_slice(&l); items.extend_from_slice(&l.inner.borrow());
items.extend_from_slice(&r); items.extend_from_slice(&r.inner.borrow());
self.push(Value::new_gc(Gc::new(mc, crate::List { inner: items }))); m.push(Value::new_gc(Gc::new(
mc,
crate::List {
inner: RefLock::new(items),
},
)));
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_update( pub(crate) fn op_update<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let (l, r) = self.try_force::<(Gc<AttrSet>, Gc<AttrSet>)>(reader, mc)?; let (l, r) = m.force_and_retry::<(Gc<AttrSet>, Gc<AttrSet>)>(reader, mc)?;
self.push(Value::new_gc(l.merge(&r, mc))); m.push(Value::new_gc(l.merge(&r, mc)));
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_neg(&mut self) -> Step { pub(crate) fn op_neg<'gc, M: Machine<'gc>>(
todo!("implement unary operation"); m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let rhs = m.force_and_retry::<NixNum>(reader, mc)?;
match rhs {
NixNum::Int(int) => m.push(Value::make_int(-int, mc)),
NixNum::Float(float) => m.push(Value::new_float(-float)),
} }
Step::Continue(())
}
#[inline(always)] #[inline(always)]
pub(crate) fn op_not(&mut self) -> Step { pub(crate) fn op_not<'gc, M: Machine<'gc>>(
todo!("implement unary operation"); m: &mut M,
} reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let rhs = m.force_and_retry::<bool>(reader, mc)?;
m.push(Value::new_inline(!rhs));
Step::Continue(())
}
pub(crate) fn values_equal( fn compare_values_inner<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
ctx: &impl crate::VmContext, ctx: &impl VmRuntimeCtx,
lhs: StrictValue<'gc>,
rhs: StrictValue<'gc>,
) -> crate::VmResult<bool> {
if let (Some(a), Some(b)) = (get_num(lhs), get_num(rhs)) {
return Ok(match (a, b) {
(NixNum::Int(a), NixNum::Int(b)) => a == b,
(NixNum::Float(a), NixNum::Float(b)) => a == b,
(NixNum::Int(a), NixNum::Float(b)) => a as f64 == b,
(NixNum::Float(a), NixNum::Int(b)) => a == b as f64,
});
}
if let (Some(a), Some(b)) = (lhs.as_inline::<bool>(), rhs.as_inline::<bool>()) {
return Ok(a == b);
}
if lhs.is::<crate::Null>() && rhs.is::<crate::Null>() {
return Ok(true);
}
if let (Some(a), Some(b)) = (
VmContextExt::get_string(ctx, lhs),
VmContextExt::get_string(ctx, rhs),
) {
return Ok(a == b);
}
if let (Some(a), Some(b)) = (lhs.as_gc::<crate::List>(), rhs.as_gc::<crate::List>()) {
if a.inner.len() != b.inner.len() {
return Ok(false);
}
for (x, y) in a.inner.iter().zip(b.inner.iter()) {
let lx = x.restrict().expect("forced");
let ly = y.restrict().expect("forced");
if !self.values_equal(ctx, lx, ly)? {
return Ok(false);
}
}
return Ok(true);
}
if let (Some(a), Some(b)) = (lhs.as_gc::<crate::AttrSet>(), rhs.as_gc::<crate::AttrSet>()) {
if a.len() != b.len() {
return Ok(false);
}
for ((k1, v1), (k2, v2)) in a.iter().zip(b.iter()) {
if k1 != k2 {
return Ok(false);
}
let lv1 = v1.restrict().expect("forced");
let lv2 = v2.restrict().expect("forced");
if !self.values_equal(ctx, lv1, lv2)? {
return Ok(false);
}
}
return Ok(true);
}
Ok(false)
}
fn compare_values_inner(
&mut self,
ctx: &impl crate::VmContext,
pred: fn(Ordering) -> bool, pred: fn(Ordering) -> bool,
lhs: StrictValue<'gc>, lhs: StrictValue<'gc>,
rhs: StrictValue<'gc>, rhs: StrictValue<'gc>,
) -> crate::VmResult<()> { ) -> crate::VmResult<()> {
if let (Some(a), Some(b)) = (get_num(lhs), get_num(rhs)) { if let (Some(a), Some(b)) = (get_num(lhs), get_num(rhs)) {
let ord = match (a, b) { let ord = match (a, b) {
(NixNum::Int(a), NixNum::Int(b)) => a.cmp(&b), (NixNum::Int(a), NixNum::Int(b)) => a.cmp(&b),
@@ -291,24 +263,30 @@ impl<'gc> crate::Vm<'gc> {
a.partial_cmp(&(b as f64)).unwrap_or(Ordering::Less) a.partial_cmp(&(b as f64)).unwrap_or(Ordering::Less)
} }
}; };
self.push(Value::new_inline(pred(ord))); m.push(Value::new_inline(pred(ord)));
return Ok(()); return Ok(());
} }
if let (Some(a), Some(b)) = ( if let (Some(a), Some(b)) = (ctx.get_string(lhs), ctx.get_string(rhs)) {
VmContextExt::get_string(ctx, lhs), m.push(Value::new_inline(pred(a.cmp(b))));
VmContextExt::get_string(ctx, rhs), return Ok(());
) { }
self.push(Value::new_inline(pred(a.cmp(b)))); if let (Some(a), Some(b)) = (lhs.as_inline::<Path>(), rhs.as_inline::<Path>()) {
let a = ctx.resolve_string(a.0);
let b = ctx.resolve_string(b.0);
m.push(Value::new_inline(pred(a.cmp(b))));
return Ok(()); return Ok(());
} }
// TODO: compare other types // TODO: compare other types
Err(crate::vm_err("cannot compare these types")) Err(crate::vm_err(format!(
} "cannot compare {} with {}",
lhs.ty(),
rhs.ty()
)))
} }
pub(crate) fn get_num(val: StrictValue<'_>) -> Option<NixNum> { pub(crate) fn get_num(val: StrictValue<'_>) -> Option<NixNum> {
if let Some(i) = val.as_inline::<i32>() { if let Some(i) = val.as_inline::<i32>() {
Some(NixNum::Int(i as i64)) Some(NixNum::Int(i64::from(i)))
} else if let Some(gc_i) = val.as_gc::<i64>() { } else if let Some(gc_i) = val.as_gc::<i64>() {
Some(NixNum::Int(*gc_i)) Some(NixNum::Int(*gc_i))
} else { } else {
@@ -333,6 +311,9 @@ fn numeric_binop<'gc>(
(Some(NixNum::Float(a)), Some(NixNum::Int(b))) => { (Some(NixNum::Float(a)), Some(NixNum::Int(b))) => {
Ok(Value::new_float(float_op(a, b as f64))) Ok(Value::new_float(float_op(a, b as f64)))
} }
_ => Err(crate::vm_err("cannot perform arithmetic on non-numbers")), _ => Err(crate::vm_err(format!(
"cannot perform arithmetic on non-numbers: {:?}",
(lhs.ty(), rhs.ty())
))),
} }
} }
-69
View File
@@ -1,69 +0,0 @@
use fix_builtins::BuiltinId;
use num_enum::TryFromPrimitive;
use crate::{BytecodeReader, PrimOp, Step, Value};
impl<'gc> crate::Vm<'gc> {
#[inline(always)]
pub(crate) fn op_load_builtins(&mut self) -> Step {
self.push(self.builtins);
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_load_builtin(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
let Ok(id) = BuiltinId::try_from_primitive(reader.read_u8())
.map_err(|err| panic!("unknown builtin id: {}", err.number));
self.push(Value::new_inline(PrimOp {
id,
arity: fix_builtins::BUILTINS[id as usize].1,
}));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_mk_pos(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
let _span_id = reader.read_u32();
todo!("MkPos");
}
#[inline(always)]
pub(crate) fn op_load_repl_binding(
&mut self,
reader: &mut BytecodeReader<'_>,
) -> Step {
let _name = reader.read_string_id();
todo!("LoadReplBinding");
}
#[inline(always)]
pub(crate) fn op_load_scoped_binding(
&mut self,
reader: &mut BytecodeReader<'_>,
) -> Step {
let _name = reader.read_string_id();
todo!("LoadScopedBinding");
}
#[inline(always)]
pub(crate) fn op_concat_strings(
&mut self,
ctx: &mut impl crate::VmContext,
reader: &mut BytecodeReader<'_>,
_mc: &gc_arena::Mutation<'gc>,
) -> Step {
let _parts_count = reader.read_u16() as usize;
let _force_string = reader.read_u8() != 0;
let mut _operands: smallvec::SmallVec<[crate::OperandData; 4]> =
smallvec::SmallVec::with_capacity(_parts_count);
for _ in 0.._parts_count {
_operands.push(reader.read_operand_data(ctx));
}
todo!("implement ConcatStrings (force parts, coerce to string, concatenate)");
}
#[inline(always)]
pub(crate) fn op_resolve_path(&mut self, _ctx: &mut impl crate::VmContext) -> Step {
todo!("implement ResolvePath");
}
}
+144 -98
View File
@@ -1,135 +1,181 @@
use fix_bytecode::PrimOpPhase;
use fix_error::Error; use fix_error::Error;
use fix_runtime::{resolve_operand, *};
use gc_arena::{Gc, Mutation, RefLock}; use gc_arena::{Gc, Mutation, RefLock};
use crate::value::*; use crate::{
use crate::{BytecodeReader, CallFrame, Closure, Env, Step, ThunkState, VmContextExt}; BytecodeReader, CallFrame, Closure, Env, ForceMode, Step, ThunkState, VmRuntimeCtx,
VmRuntimeCtxExt,
};
impl<'gc> crate::Vm<'gc> { #[inline(always)]
#[inline(always)] pub(crate) fn call<'gc, M: Machine<'gc>>(
pub(crate) fn op_call( m: &mut M,
&mut self,
ctx: &mut impl crate::VmContext,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { arg: Value<'gc>,
let func = self.try_force::<StrictValue>(reader, mc)?; resume_pc: usize,
if self.call_depth > 10000 { ) -> Step {
return self.finish_err(Error::eval_error("stack overflow; max-call-depth exceeded")); let func = m.force_and_retry::<StrictValue>(reader, mc)?;
if m.call_depth() > 10000 {
return m.finish_err(Error::eval_error("stack overflow; max-call-depth exceeded"));
} }
self.call_depth += 1; m.inc_call_depth();
let arg = reader.read_operand_data(ctx).resolve(mc, self);
if let Some(closure) = func.as_gc::<Closure>() { if let Some(closure) = func.as_gc::<Closure>() {
if closure.pattern.is_some() {
// FIXME: better DX...
m.push(func.relax());
m.push(arg);
m.push_call_frame(CallFrame {
pc: resume_pc,
thunk: None,
env: m.env(),
});
reader.set_pc(PrimOpPhase::CallPattern.ip() as usize);
return Step::Continue(());
}
let ip = closure.ip; let ip = closure.ip;
let n_locals = closure.n_locals; let n_locals = closure.n_locals;
let env = closure.env; let env = closure.env;
if let Some(ref _pattern) = closure.pattern {
todo!("pattern call")
} else {
let new_env = Gc::new(mc, RefLock::new(Env::with_arg(arg, n_locals, env))); let new_env = Gc::new(mc, RefLock::new(Env::with_arg(arg, n_locals, env)));
self.call_stack.push(CallFrame { m.push_call_frame(CallFrame {
pc: reader.pc(), pc: resume_pc,
stack_depth: 0,
thunk: None, thunk: None,
env: self.env, env: m.env(),
with_env: self.with_env,
}); });
reader.set_pc(ip as usize); reader.set_pc(ip as usize);
self.env = new_env; m.set_env(new_env);
} } else if let Some(primop) = func.as_inline::<PrimOp>() {
if primop.arity == 1 {
m.push(arg);
m.push_call_frame(CallFrame {
pc: resume_pc,
thunk: None,
env: m.env(),
});
reader.set_pc(primop.dispatch_ip as usize)
} else { } else {
todo!("call other types: {func:?}") let app = PrimOpApp {
primop,
arity: primop.arity - 1,
args: [arg, Value::default(), Value::default()],
};
m.push(Value::new_gc(Gc::new(mc, app)));
}
} else if let Some(app) = func.as_gc::<PrimOpApp>() {
if app.arity == 1 {
for i in 0..app.primop.arity - 1 {
m.push(app.args[i as usize]);
}
m.push(arg);
m.push_call_frame(CallFrame {
pc: resume_pc,
thunk: None,
env: m.env(),
});
reader.set_pc(app.primop.dispatch_ip as usize)
} else {
let position = (app.primop.arity - app.arity) as usize;
let mut new_app = PrimOpApp {
arity: app.arity - 1,
..*app
};
new_app.args[position] = arg;
m.push(Value::new_gc(Gc::new(mc, new_app)))
}
} else if let Some(attrs) = func.as_gc::<AttrSet>()
&& let Some(functor) = attrs.lookup(m.functor_sym())
{
// f arg => (f.__functor f) arg
//
// Stage the work for `CallFunctor1` so retries during force are
// safe: the stack invariant `[..., orig_arg, self, functor]`
// holds every time control re-enters phase 1.
m.dec_call_depth();
m.push_call_frame(CallFrame {
pc: resume_pc,
thunk: None,
env: m.env(),
});
m.push(arg);
m.push(func.relax());
m.push(functor);
reader.set_pc(PrimOpPhase::CallFunctor1.ip() as usize);
return Step::Continue(());
} else {
return m.finish_err(Error::eval_error(format!(
"attempt to call something which is not a function but {}",
func.ty()
)));
} }
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_return( pub(crate) fn op_call<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
ctx: &mut impl crate::VmContext, ctx: &impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
self.handle_return(reader, ctx, mc) let arg = resolve_operand(&reader.read_operand_data(), mc, ctx, m);
} let pc = reader.pc();
m.call(reader, mc, arg, pc)
}
pub(crate) fn handle_return<C: crate::VmContext>( #[inline(always)]
&mut self, pub(crate) fn op_return<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
ctx: &C,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let ret_inst_pc = reader.pc() - 1; let val = m.force_and_retry::<StrictValue>(reader, mc)?;
let Some(CallFrame { let Some(CallFrame {
pc: ret_pc, pc: ret_pc,
stack_depth,
thunk, thunk,
env, env,
with_env, }) = m.pop_call_frame()
}) = self.call_stack.pop()
else { else {
let val = self.pop(); match m.force_mode() {
return self.finish_ok(ctx.convert_value(val)); ForceMode::AsIs => return m.finish_ok(ctx.convert_value(val.relax())),
ForceMode::Shallow => {
m.push(val.relax());
reader.set_pc(PrimOpPhase::ForceResultShallow.ip() as usize);
return Step::Continue(());
}
ForceMode::Deep => {
m.push(val.relax());
m.push(val.relax());
m.push_call_frame(CallFrame {
pc: PrimOpPhase::ForceResultDeepFinish.ip() as usize,
thunk: None,
env: m.env(),
});
m.inc_call_depth();
reader.set_pc(PrimOpPhase::DeepSeq.ip() as usize);
return Step::Continue(());
}
}
}; };
reader.set_pc(ret_pc); reader.set_pc(ret_pc);
if let Some(outer_thunk) = thunk { if let Some(outer_thunk) = thunk {
let val = self.pop();
match val.restrict() {
Ok(val) => {
*outer_thunk.borrow_mut(mc) = ThunkState::Evaluated(val); *outer_thunk.borrow_mut(mc) = ThunkState::Evaluated(val);
if reader.bytecode().get(ret_pc).copied() == Some(fix_codegen::Op::Return as u8)
{
self.push(val.relax());
}
}
Err(inner_thunk) => {
let mut state = inner_thunk.borrow_mut(mc);
match *state {
ThunkState::Pending {
ip: inner_ip,
env: inner_env,
with_env: inner_with_env,
} => {
self.call_stack.push(CallFrame {
pc: ret_pc,
stack_depth,
thunk: Some(outer_thunk),
env,
with_env,
});
self.call_stack.push(CallFrame {
pc: ret_inst_pc,
stack_depth: 0,
thunk: Some(inner_thunk),
env: inner_env,
with_env: inner_with_env,
});
*state = ThunkState::Blackhole;
reader.set_pc(inner_ip);
self.env = inner_env;
self.with_env = inner_with_env;
return Step::Continue(());
}
ThunkState::Evaluated(val) => {
*outer_thunk.borrow_mut(mc) = ThunkState::Evaluated(val);
if reader.bytecode().get(ret_pc).copied()
== Some(fix_codegen::Op::Return as u8)
{
self.push(val.relax());
}
}
ThunkState::Apply { func: _, arg: _ } => todo!("force Apply thunk"),
ThunkState::Blackhole => {
return self
.finish_err(Error::eval_error("infinite recursion encountered"));
}
}
}
}
} else { } else {
self.call_depth -= 1; m.dec_call_depth();
m.push(val.relax())
} }
self.env = env; m.set_env(env);
self.with_env = with_env;
Step::Continue(()) Step::Continue(())
} }
#[inline(always)]
pub(crate) fn op_dispatch_primop<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
crate::primops::dispatch_primop(m, ctx, reader, mc)
} }
+21 -23
View File
@@ -1,33 +1,32 @@
use fix_runtime::Machine;
use gc_arena::{Gc, Mutation, RefLock}; use gc_arena::{Gc, Mutation, RefLock};
use crate::{BytecodeReader, Step, ThunkState, Value}; use crate::{BytecodeReader, Step, ThunkState, Value};
impl<'gc> crate::Vm<'gc> { #[inline(always)]
#[inline(always)] pub(crate) fn op_make_thunk<'gc, M: Machine<'gc>>(
pub(crate) fn op_make_thunk( m: &mut M,
&mut self,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let entry_point = reader.read_u32(); let entry_point = reader.read_u32();
let thunk = Gc::new( let thunk = Gc::new(
mc, mc,
RefLock::new(ThunkState::Pending { RefLock::new(ThunkState::Pending {
ip: entry_point as usize, ip: entry_point as usize,
env: self.env, env: m.env(),
with_env: self.with_env,
}), }),
); );
self.push(Value::new_gc(thunk)); m.push(Value::new_gc(thunk));
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_make_closure( pub(crate) fn op_make_closure<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let entry_point = reader.read_u32(); let entry_point = reader.read_u32();
let n_locals = reader.read_u32(); let n_locals = reader.read_u32();
let closure = Gc::new( let closure = Gc::new(
@@ -35,20 +34,20 @@ impl<'gc> crate::Vm<'gc> {
crate::Closure { crate::Closure {
ip: entry_point, ip: entry_point,
n_locals, n_locals,
env: self.env, env: m.env(),
pattern: None, pattern: None,
}, },
); );
self.push(Value::new_gc(closure)); m.push(Value::new_gc(closure));
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_make_pattern_closure( pub(crate) fn op_make_pattern_closure<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let entry_point = reader.read_u32(); let entry_point = reader.read_u32();
let n_locals = reader.read_u32(); let n_locals = reader.read_u32();
let req_count = reader.read_u16() as usize; let req_count = reader.read_u16() as usize;
@@ -85,11 +84,10 @@ impl<'gc> crate::Vm<'gc> {
crate::Closure { crate::Closure {
ip: entry_point, ip: entry_point,
n_locals, n_locals,
env: self.env, env: m.env(),
pattern: Some(pattern), pattern: Some(pattern),
}, },
); );
self.push(Value::new_gc(closure)); m.push(Value::new_gc(closure));
Step::Continue(()) Step::Continue(())
}
} }
+280 -102
View File
@@ -1,159 +1,337 @@
use fix_error::Error; use fix_error::Error;
use gc_arena::Gc; use fix_lang::StringId;
use fix_runtime::{Machine, MachineExt, NixType, resolve_operand};
use gc_arena::{Gc, RefLock};
use smallvec::SmallVec; use smallvec::SmallVec;
use crate::{ use crate::{
AttrKeyData, AttrSet, BytecodeReader, List, NixString, OperandData, Step, StrictValue, Value, AttrSet, BytecodeReader, List, Step, StrictValue, Value, VmRuntimeCtx, VmRuntimeCtxExt,
}; };
impl<'gc> crate::Vm<'gc> { #[inline(always)]
#[inline(always)] pub(crate) fn op_make_attrs<'gc, M: Machine<'gc>>(
pub(crate) fn op_make_attrs( m: &mut M,
&mut self, ctx: &mut impl VmRuntimeCtx,
ctx: &mut impl crate::VmContext,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>, mc: &gc_arena::Mutation<'gc>,
) -> Step { ) -> Step {
let count = reader.read_u32() as usize; let static_count = reader.read_u32() as usize;
let mut entries: SmallVec<[AttrEntry; 4]> = SmallVec::with_capacity(count); let dynamic_count = reader.read_u32() as usize;
for _ in 0..count {
let key = reader.read_attr_key_data(ctx); for i in 0..dynamic_count {
let val = reader.read_operand_data(ctx); let depth = dynamic_count - 1 - i;
let _span_id = reader.read_u32(); m.force_slot_to_pc(depth, reader, mc, reader.inst_start_pc())?;
entries.push(AttrEntry { key, val });
}
let mut kv: SmallVec<[(crate::StringId, Value); 4]> = SmallVec::with_capacity(count);
for entry in &entries {
let key_sid = match &entry.key {
AttrKeyData::Static(sid) => *sid,
AttrKeyData::Dynamic(op) => {
let v = op.resolve(mc, self);
v.as_inline::<crate::StringId>()
.expect("dynamic attr key must be a string")
} }
let mut dyn_keys: SmallVec<[_; 2]> = SmallVec::with_capacity(dynamic_count);
for i in 0..dynamic_count {
let depth = dynamic_count - 1 - i;
let key_val = m.peek_forced(depth);
let key_sid = match ctx.get_string_id(key_val) {
Ok(id) => Some(id),
Err(NixType::Null) => None,
Err(got) => return m.finish_type_err(NixType::String, got),
}; };
let val = entry.val.resolve(mc, self); dyn_keys.push(key_sid);
kv.push((key_sid, val));
} }
m.drop_n(dynamic_count);
let mut kv: SmallVec<[(crate::StringId, Value); 4]> =
SmallVec::with_capacity(static_count + dynamic_count);
for _ in 0..static_count {
let key = reader.read_string_id();
let val = resolve_operand(&reader.read_operand_data(), mc, ctx, m);
let _span_id = reader.read_u32();
kv.push((key, val));
}
for key in dyn_keys {
let val = resolve_operand(&reader.read_operand_data(), mc, ctx, m);
let _span_id = reader.read_u32();
if let Some(key) = key {
kv.push((key, val))
}
}
kv.sort_by_key(|(k, _)| *k); kv.sort_by_key(|(k, _)| *k);
let attrs = Gc::new(mc, AttrSet::from_sorted_unchecked(kv)); let attrs = Gc::new(mc, AttrSet::from_sorted_unchecked(kv));
self.push(Value::new_gc(attrs)); m.push(Value::new_gc(attrs));
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_make_empty_attrs(&mut self) -> Step { pub(crate) fn op_make_empty_attrs<'gc, M: Machine<'gc>>(m: &mut M) -> Step {
self.push(self.empty_attrs); m.push(m.empty_attrs());
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_select_static( pub(crate) fn op_select_static<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
ctx: &mut impl crate::VmContext, ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>, mc: &gc_arena::Mutation<'gc>,
) -> Step { ) -> Step {
let _span_id = reader.read_u32(); let _span_id = reader.read_u32();
let key = reader.read_string_id(); let key = reader.read_string_id();
let attrset = self.try_force::<Gc<AttrSet>>(reader, mc)?; let attrset = m.force_and_retry::<Gc<AttrSet>>(reader, mc)?;
match attrset.lookup(key) { match attrset.lookup(key) {
Some(v) => { Some(v) => {
self.push(v); m.push(v);
} }
None => loop { None => return select_skip(m, key, ctx, reader),
let byte = reader.bytecode()[reader.pc()];
if byte == fix_codegen::Op::SelectStatic as u8 {
reader.set_pc(reader.pc() + 1 + 4 + 4);
} else if byte == fix_codegen::Op::SelectDynamic as u8 {
reader.set_pc(reader.pc() + 1 + 4);
} else if byte == fix_codegen::Op::JumpIfSelectSucceeded as u8 {
reader.set_pc(reader.pc() + 1 + 4);
break;
} else {
let name = ctx.resolve_string(key);
return self
.finish_err(Error::eval_error(format!("attribute '{name}' missing")));
}
},
} }
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_select_dynamic( pub(crate) fn op_select_dynamic<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
ctx: &mut impl crate::VmContext, ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>, mc: &gc_arena::Mutation<'gc>,
) -> Step { ) -> Step {
let _span_id = reader.read_u32(); let _span_id = reader.read_u32();
let (attrset, key_val) = self.try_force::<(Gc<AttrSet>, StrictValue)>(reader, mc)?; let (attrset, key_val) = m.force_and_retry::<(Gc<AttrSet>, StrictValue)>(reader, mc)?;
let key_sid = if let Some(sid) = key_val.as_inline::<crate::StringId>() { let key_sid = match ctx.get_string_id(key_val) {
sid Ok(id) => id,
} else if let Some(ns) = key_val.as_gc::<NixString>() { Err(got) => return m.finish_type_err(NixType::String, got),
ctx.intern_string(ns.as_str())
} else {
return self.finish_err(Error::eval_error("dynamic select key must be a string"));
}; };
match attrset.lookup(key_sid) { match attrset.lookup(key_sid) {
Some(v) => { Some(v) => {
self.push(v); m.push(v);
}
None => {
let name = ctx.resolve_string(key_sid);
return self.finish_err(Error::eval_error(format!("attribute '{name}' missing")));
} }
None => return select_skip(m, key_sid, ctx, reader),
} }
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] /// Skip the rest of a **Select** attrpath after a missing attribute.
pub(crate) fn op_jump_if_select_succeeded( /// Only recognises Select opcodes and jumps; encountering any other
&mut self, /// opcode means we've reached the end of the select sequence and
/// should report the missing-attribute error.
fn select_skip<'gc, M: Machine<'gc>>(
m: &mut M,
key: StringId,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
) -> Step { ) -> Step {
use fix_bytecode::Op::*;
loop {
match reader.read_op() {
SelectStatic => {
reader.set_pc(reader.pc() + 4 + 4);
}
SelectDynamic => {
reader.set_pc(reader.pc() + 4);
}
JumpIfSelectSucceeded => {
reader.set_pc(reader.pc() + 4);
break Step::Continue(());
}
JumpIfSelectFailed => {
let offset = reader.read_i32();
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
}
_ => {
let name = ctx.resolve_string(key);
return m.finish_err(Error::eval_error(format!("attribute '{name}' missing")));
}
}
}
}
/// Skip the rest of a **HasAttr** attrpath after an intermediate
/// lookup failed. Only recognises HasAttr opcodes and jumps.
fn has_attr_skip(reader: &mut BytecodeReader<'_>) -> Step {
use fix_bytecode::Op::*;
loop {
match reader.read_op() {
HasAttrPathStatic => {
reader.set_pc(reader.pc() + 4 + 4);
}
HasAttrPathDynamic => {
reader.set_pc(reader.pc() + 4);
}
HasAttrStatic => {
reader.set_pc(reader.pc() + 4);
break Step::Continue(());
}
JumpIfSelectFailed => {
let offset = reader.read_i32();
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
}
HasAttrDynamic => {
break Step::Continue(());
}
HasAttrResolve => {
reader.set_pc(reader.pc() - 1);
break Step::Continue(());
}
other => {
unreachable!("unexpected opcode {:?} in has_attr_skip", other as u8)
}
}
}
}
#[inline(always)]
pub(crate) fn op_has_attr_path_static<'gc, M: Machine<'gc>>(
m: &mut M,
_ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let _span_id = reader.read_u32();
let key = reader.read_string_id();
let current = m.force_and_retry::<StrictValue>(reader, mc)?;
match current
.as_gc::<AttrSet>()
.and_then(|attrs| attrs.lookup(key))
{
Some(v) => {
m.push(v);
}
None => return has_attr_skip(reader),
}
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_has_attr_path_dynamic<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let _span_id = reader.read_u32();
let (current, key_val) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
let key_sid = match ctx.get_string_id(key_val) {
Ok(id) => id,
Err(got) => return m.finish_type_err(NixType::String, got),
};
match current
.as_gc::<AttrSet>()
.and_then(|attrs| attrs.lookup(key_sid))
{
Some(v) => {
m.push(v);
}
None => return has_attr_skip(reader),
}
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_jump_if_select_failed<'gc, M: Machine<'gc>>(
_m: &mut M,
reader: &mut BytecodeReader<'_>,
) -> Step {
// No-op
let _offset = reader.read_i32();
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_jump_if_select_succeeded<'gc, M: Machine<'gc>>(
_m: &mut M,
reader: &mut BytecodeReader<'_>,
) -> Step {
let offset = reader.read_i32(); let offset = reader.read_i32();
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize); reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_has_attr(&mut self, reader: &mut BytecodeReader<'_>) -> Step { pub(crate) fn op_has_attr_static<'gc, M: Machine<'gc>>(
let _n = reader.read_u16() as usize; m: &mut M,
todo!("HasAttr");
}
#[inline(always)]
pub(crate) fn op_make_list(
&mut self,
ctx: &mut impl crate::VmContext,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>, mc: &gc_arena::Mutation<'gc>,
) -> Step { ) -> Step {
let key = reader.read_string_id();
let current = m.force_and_retry::<StrictValue>(reader, mc)?;
m.push(Value::new_inline(
current
.as_gc::<AttrSet>()
.and_then(|attrs| attrs.lookup(key))
.is_some(),
));
// Skip HasAttrResolve
reader.set_pc(reader.pc() + 1);
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_has_attr_dynamic<'gc, M: MachineExt<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let (current, dyn_key) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
let key_sid = match ctx.get_string_id(dyn_key) {
Ok(id) => id,
Err(got) => return m.finish_type_err(NixType::String, got),
};
m.push(Value::new_inline(
current
.as_gc::<AttrSet>()
.and_then(|attrs| attrs.lookup(key_sid))
.is_some(),
));
// Skip HasAttrResolve
reader.set_pc(reader.pc() + 1);
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_has_attr_resolve<'gc, M: Machine<'gc>>(m: &mut M) -> Step {
// If we reach here, has_attr check has failed, push false (AttrSet is already popped)
m.push(Value::new_inline(false));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_make_list<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let count = reader.read_u32() as usize; let count = reader.read_u32() as usize;
let mut items: SmallVec<[Value; 4]> = SmallVec::with_capacity(count); let mut items: SmallVec<[Value; 4]> = SmallVec::with_capacity(count);
for _ in 0..count { for _ in 0..count {
items.push(reader.read_operand_data(ctx).resolve(mc, self)); items.push(resolve_operand(&reader.read_operand_data(), mc, ctx, m));
} }
let list = Gc::new(mc, List { inner: items }); let list = Gc::new(
self.push(Value::new_gc(list)); mc,
List {
inner: RefLock::new(items),
},
);
m.push(Value::new_gc(list));
Step::Continue(()) Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_make_empty_list(&mut self) -> Step {
self.push(self.empty_list);
Step::Continue(())
}
} }
pub(crate) struct AttrEntry { #[inline(always)]
pub(crate) key: AttrKeyData, pub(crate) fn op_make_empty_list<'gc, M: Machine<'gc>>(m: &mut M) -> Step {
pub(crate) val: OperandData, m.push(m.empty_list());
Step::Continue(())
} }
+35 -23
View File
@@ -1,46 +1,58 @@
use crate::value::*; use fix_error::Error;
use crate::{BytecodeReader, Step}; use fix_runtime::*;
use gc_arena::Mutation;
impl<'gc> crate::Vm<'gc> { use crate::{BytecodeReader, Step, VmRuntimeCtx};
#[inline(always)]
pub(crate) fn op_jump_if_false( #[inline(always)]
&mut self, pub(crate) fn op_jump_if_false<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>, mc: &gc_arena::Mutation<'gc>,
) -> Step { ) -> Step {
let offset = reader.read_i32(); let offset = reader.read_i32();
let cond = self.try_force::<StrictValue>(reader, mc)?; let cond = m.force_and_retry::<StrictValue>(reader, mc)?;
if cond.as_inline::<bool>() == Some(false) { if cond.as_inline::<bool>() == Some(false) {
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize); reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
} }
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_jump_if_true( pub(crate) fn op_jump_if_true<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let offset = reader.read_i32(); let offset = reader.read_i32();
let cond = self.try_force::<StrictValue>(reader, mc)?; let cond = m.force_and_retry::<StrictValue>(reader, mc)?;
if cond.as_inline::<bool>() == Some(true) { if cond.as_inline::<bool>() == Some(true) {
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize); reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
} }
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_jump(&mut self, reader: &mut BytecodeReader<'_>) -> Step { pub(crate) fn op_jump<'gc, M: Machine<'gc>>(_m: &mut M, reader: &mut BytecodeReader<'_>) -> Step {
let offset = reader.read_i32(); let offset = reader.read_i32();
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize); reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_assert(&mut self, reader: &mut BytecodeReader<'_>) -> Step { pub(crate) fn op_assert<'gc, M: Machine<'gc>>(
let _raw_idx = reader.read_u32(); m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let raw_id = reader.read_string_id();
let raw = ctx.resolve_string(raw_id);
let _span_id = reader.read_u32(); let _span_id = reader.read_u32();
todo!("implement Assert (force TOS)"); let assertion = m.force_and_retry::<bool>(reader, mc)?;
if !assertion {
// FIXME: use catchable error
return m.finish_err(Error::eval_error(format!("assertion '{raw}' failed")));
} }
Step::Continue(())
} }
+51 -43
View File
@@ -1,55 +1,63 @@
use fix_runtime::Machine;
use gc_arena::{Gc, Mutation}; use gc_arena::{Gc, Mutation};
use crate::{BytecodeReader, Step, Value}; use crate::{BytecodeReader, Step, Value};
impl<'gc> crate::Vm<'gc> { #[inline(always)]
#[inline(always)] pub(crate) fn op_push_smi<'gc, M: Machine<'gc>>(
pub(crate) fn op_push_smi(&mut self, reader: &mut BytecodeReader<'_>) -> Step { m: &mut M,
reader: &mut BytecodeReader<'_>,
) -> Step {
let val = reader.read_i32(); let val = reader.read_i32();
self.push(Value::new_inline(val)); m.push(Value::new_inline(val));
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_push_bigint( pub(crate) fn op_push_bigint<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let val = reader.read_i64(); let val = reader.read_i64();
self.push(Value::new_gc(Gc::new(mc, val))); m.push(Value::new_gc(Gc::new(mc, val)));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_push_float<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
) -> Step {
let val = reader.read_f64();
m.push(Value::new_float(val));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_push_string<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
) -> Step {
let sid = reader.read_string_id();
m.push(Value::new_inline(sid));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_push_null<'gc, M: Machine<'gc>>(m: &mut M) -> Step {
m.push(Value::new_inline(crate::Null));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_push_true<'gc, M: Machine<'gc>>(m: &mut M) -> Step {
m.push(Value::new_inline(true));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_push_false<'gc, M: Machine<'gc>>(m: &mut M) -> Step {
m.push(Value::new_inline(false));
Step::Continue(()) Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_push_float(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
let val = reader.read_f64();
self.push(Value::new_float(val));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_push_string(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
let sid = reader.read_string_id();
self.push(Value::new_inline(sid));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_push_null(&mut self) -> Step {
self.push(Value::new_inline(crate::Null));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_push_true(&mut self) -> Step {
self.push(Value::new_inline(true));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_push_false(&mut self) -> Step {
self.push(Value::new_inline(false));
Step::Continue(())
}
} }
+179
View File
@@ -0,0 +1,179 @@
use std::path::PathBuf;
use fix_bytecode::PrimOpPhase;
use fix_error::Error;
use fix_lang::{BUILTINS, BuiltinId, StringId};
use fix_runtime::{
AttrSet, Machine, MachineExt, NixString, Path, StrictValue, StringContext, canon_path_str,
};
use crate::{BytecodeReader, PrimOp, Step, Value, VmRuntimeCtx, VmRuntimeCtxExt};
#[inline(always)]
pub(crate) fn op_load_builtins<'gc, M: Machine<'gc>>(m: &mut M) -> Step {
m.push(m.builtins());
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_load_builtin<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
) -> Step {
let Ok(id) = BuiltinId::try_from(reader.read_u8())
.map_err(|err| panic!("unknown builtin id: {}", err.number));
m.push(Value::new_inline(PrimOp {
id,
arity: BUILTINS[id as usize].1,
dispatch_ip: PrimOpPhase::entry_for_builtin(id).ip(),
}));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_load_repl_binding<'gc, M: Machine<'gc>>(
_m: &mut M,
reader: &mut BytecodeReader<'_>,
) -> Step {
let _name = reader.read_string_id();
todo!("LoadReplBinding");
}
#[inline(always)]
pub(crate) fn op_load_scoped_binding<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
_mc: &gc_arena::Mutation<'gc>,
) -> Step {
let slot_id = reader.read_u32();
let name = reader.read_string_id();
let scope = m.scope_slot(slot_id);
let Some(attrs) = scope.as_gc::<AttrSet>() else {
return m.finish_err(Error::eval_error("internal: scope slot is not an attrset"));
};
match attrs.lookup(name) {
Some(val) => {
m.push(val);
Step::Continue(())
}
None => m.finish_err(Error::eval_error(format!(
"scoped binding '{}' not found",
ctx.resolve_string(name)
))),
}
}
#[inline(always)]
pub(crate) fn op_coerce_to_string<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
if val.is::<StringId>() || val.is::<NixString>() {
m.push(val.relax());
} else if let Some(p) = val.as_inline::<Path>() {
// Coercing a path to a string yields the canonical path text.
// FIXME: copy to store
m.push(Value::new_inline(p.0));
} else {
todo!("coerce other types to string: {:?}", val.ty());
}
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_concat_strings<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let count = reader.read_u16() as usize;
let _force_string = reader.read_u8() != 0;
let mut total_len = 0;
let mut has_any_context = false;
for i in 0..count {
let val = m.peek_forced(count - 1 - i);
let s = ctx.get_string(val).expect("coerced");
total_len += s.len();
if !ctx.get_string_context(val).is_empty() {
has_any_context = true;
}
}
let mut result = String::with_capacity(total_len);
let mut merged = StringContext::new();
for i in 0..count {
let val = m.peek_forced(count - 1 - i);
let s = ctx.get_string(val).expect("coerced");
result.push_str(s);
if has_any_context {
let ctx = ctx.get_string_context(val);
if !ctx.is_empty() {
merged = merged.merge(ctx);
}
}
}
m.drop_n(count);
if merged.is_empty() {
let sid = ctx.intern_string(result);
m.push(Value::new_inline(sid));
} else {
let ns = gc_arena::Gc::new(mc, NixString::with_context(result, merged));
m.push(Value::new_gc(ns));
}
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_resolve_path<'gc, M: MachineExt<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let path_val = m.force_and_retry::<StrictValue>(reader, mc)?;
let dir_id = reader.read_string_id();
// Already a path: keep as-is. ResolvePath is idempotent on paths.
if let Some(p) = path_val.as_inline::<Path>() {
m.push(Value::new_inline(p));
return Step::Continue(());
}
let path = match ctx.get_string(path_val) {
Some(s) => s.to_owned(),
None => {
return m.finish_err(Error::eval_error(format!(
"expected a string for path, got {}",
path_val.ty()
)));
}
};
let resolved = match resolve_path_str(ctx.resolve_string(dir_id), &path) {
Ok(s) => s,
Err(e) => return m.finish_err(e),
};
let sid = ctx.intern_string(resolved);
m.push(Value::new_inline(Path(sid)));
Step::Continue(())
}
fn resolve_path_str(current_dir: &str, path: &str) -> Result<String, Box<Error>> {
let raw = if path.starts_with('/') {
return Ok(canon_path_str(path));
} else if let Some(rest) = path.strip_prefix("~/") {
let mut dir =
std::env::home_dir().ok_or_else(|| Error::eval_error("home dir not defined"))?;
dir.push(rest);
dir
} else {
let mut dir = PathBuf::from(current_dir);
dir.push(path);
dir
};
Ok(canon_path_str(&raw))
}
+11 -1
View File
@@ -1,9 +1,19 @@
pub(crate) mod arithmetic; pub(crate) mod arithmetic;
pub(crate) mod builtins_misc;
pub(crate) mod calls; pub(crate) mod calls;
pub(crate) mod closures; pub(crate) mod closures;
pub(crate) mod collections; pub(crate) mod collections;
pub(crate) mod control; pub(crate) mod control;
pub(crate) mod literals; pub(crate) mod literals;
pub(crate) mod misc;
pub(crate) mod variables; pub(crate) mod variables;
pub(crate) mod with_scope; pub(crate) mod with_scope;
pub(crate) use arithmetic::*;
pub(crate) use calls::*;
pub(crate) use closures::*;
pub(crate) use collections::*;
pub(crate) use control::*;
pub(crate) use literals::*;
pub(crate) use misc::*;
pub(crate) use variables::*;
pub(crate) use with_scope::*;
+29 -23
View File
@@ -1,50 +1,56 @@
use fix_runtime::Machine;
use crate::{BytecodeReader, Mutation, Step, Value}; use crate::{BytecodeReader, Mutation, Step, Value};
impl<'gc> crate::Vm<'gc> { #[inline(always)]
#[inline(always)] pub(crate) fn op_load_local<'gc, M: Machine<'gc>>(
pub(crate) fn op_load_local(&mut self, reader: &mut BytecodeReader<'_>) -> Step { m: &mut M,
reader: &mut BytecodeReader<'_>,
) -> Step {
let idx = reader.read_u32() as usize; let idx = reader.read_u32() as usize;
self.push(self.env.borrow().locals[idx]); m.push(m.env().borrow().locals[idx]);
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_load_outer(&mut self, reader: &mut BytecodeReader<'_>) -> Step { pub(crate) fn op_load_outer<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
) -> Step {
let layer = reader.read_u8(); let layer = reader.read_u8();
let idx = reader.read_u32() as usize; let idx = reader.read_u32() as usize;
let mut cur = self.env; let mut cur = m.env();
for _ in 0..layer { for _ in 0..layer {
let prev = cur.borrow().prev.expect("LoadOuter: env chain too short"); let prev = cur.borrow().prev.expect("LoadOuter: env chain too short");
cur = prev; cur = prev;
} }
let val = cur.borrow().locals[idx]; let val = cur.borrow().locals[idx];
self.push(val); m.push(val);
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_store_local( pub(crate) fn op_store_local<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let idx = reader.read_u32() as usize; let idx = reader.read_u32() as usize;
let val = self.pop(); let val = m.pop();
self.env.borrow_mut(mc).locals[idx] = val; m.env().borrow_mut(mc).locals[idx] = val;
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_alloc_locals( pub(crate) fn op_alloc_locals<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let count = reader.read_u32() as usize; let count = reader.read_u32() as usize;
self.env m.env()
.borrow_mut(mc) .borrow_mut(mc)
.locals .locals
.extend(std::iter::repeat_n(Value::default(), count)); .extend(std::iter::repeat_n(Value::default(), count));
Step::Continue(()) Step::Continue(())
}
} }
+61 -69
View File
@@ -1,83 +1,75 @@
use fix_common::Symbol;
use fix_error::Error; use fix_error::Error;
use gc_arena::Gc; use fix_lang::Symbol;
use fix_runtime::{resolve_operand, *};
use smallvec::SmallVec;
use crate::{BytecodeReader, CallFrame, Step, WithEnv}; use crate::{Break, BytecodeReader, CallFrame, Step, VmRuntimeCtx};
use crate::value::*;
impl<'gc> crate::Vm<'gc> { #[inline(always)]
#[inline(always)] pub(crate) fn op_lookup_with<'gc, M: Machine<'gc>>(
pub(crate) fn op_push_with( m: &mut M,
&mut self, ctx: &mut impl VmRuntimeCtx,
ctx: &mut impl crate::VmContext,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>, mc: &gc_arena::Mutation<'gc>,
) -> Step { ) -> Step {
let env = reader.read_operand_data(ctx).resolve(mc, self); #[allow(clippy::unwrap_used)]
let scope = Gc::new( let counter = m.peek_forced(0).as_inline::<i32>().unwrap();
mc,
WithEnv {
env,
prev: self.with_env,
},
);
self.with_env = Some(scope);
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_pop_with(&mut self) -> Step {
let Some(scope) = self.with_env else {
unreachable!("no with_scope to pop");
};
self.with_env = scope.prev;
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_prepare_with(&mut self) -> Step {
self.call_stack.push(CallFrame {
pc: usize::MAX,
stack_depth: 0,
thunk: None,
env: self.env,
with_env: self.with_env,
});
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_lookup_with(
&mut self,
ctx: &mut impl crate::VmContext,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let name = reader.read_string_id(); let name = reader.read_string_id();
let n = reader.read_u8();
let mut namespaces = SmallVec::<[_; 2]>::new();
for _ in 0..n {
namespaces.push(resolve_operand(&reader.read_operand_data(), mc, ctx, m));
}
let Some(&WithEnv { env, prev }) = self.with_env.as_deref() else { let resume_pc = reader.inst_start_pc();
let Some(CallFrame { with_env, .. }) = self.call_stack.pop() else { let namespace = match namespaces[counter as usize].restrict() {
unreachable!() Ok(val) => val,
Err(thunk) => {
let mut state = thunk.borrow_mut(mc);
match *state {
ThunkState::Pending { ip, env } => {
*state = ThunkState::Blackhole;
m.push_call_frame(CallFrame {
thunk: Some(thunk),
pc: resume_pc,
env: m.env(),
});
m.set_env(env);
reader.set_pc(ip);
return Step::Break(Break::Force);
}
ThunkState::Evaluated(v) => v,
ThunkState::Apply { func, arg } => {
m.push_call_frame(CallFrame {
thunk: Some(thunk),
pc: resume_pc,
env: m.env(),
});
m.push(func);
return m.call(reader, mc, arg, resume_pc);
}
ThunkState::Blackhole => {
return m.finish_err(Error::eval_error("infinite recursion encountered"));
}
}
}
}; };
self.with_env = with_env;
return self.finish_err(Error::eval_error(format!( if let Some(val) = namespace
.as_gc::<AttrSet>()
.and_then(|attrs| attrs.lookup(name))
{
m.replace(0, val);
} else if counter + 1 == n as i32 {
return m.finish_err(Error::eval_error(format!(
"undefined variable '{}'", "undefined variable '{}'",
Symbol::from(ctx.resolve_string(name)) Symbol::from(ctx.resolve_string(name))
))); )));
}; } else {
self.push(env); m.replace(0, Value::new_inline(counter + 1));
let env = self.try_force::<Gc<AttrSet>>(reader, mc)?; reader.set_pc(resume_pc);
let Some(val) = env.lookup(name) else {
reader.set_pc(reader.inst_start_pc());
self.with_env = prev;
return Step::Continue(());
};
self.push(val);
let Some(CallFrame { with_env, .. }) = self.call_stack.pop() else {
unreachable!()
};
self.with_env = with_env;
Step::Continue(())
} }
Step::Continue(())
} }
+317 -332
View File
@@ -7,203 +7,68 @@
use std::path::PathBuf; use std::path::PathBuf;
use fix_builtins::{BUILTINS, BuiltinId}; use fix_bytecode::{InstructionPtr, PrimOpPhase};
use fix_codegen::InstructionPtr;
use fix_common::StringId;
use fix_error::{Error, Result, Source}; use fix_error::{Error, Result, Source};
use gc_arena::arena::CollectionPhase; use fix_lang::{BUILTINS, BuiltinId, StringId};
use gc_arena::metrics::Pacing;
use gc_arena::{Arena, Collect, Gc, Mutation, RefLock, Rootable}; use gc_arena::{Arena, Collect, Gc, Mutation, RefLock, Rootable};
use hashbrown::HashMap; use hashbrown::HashMap;
use num_enum::TryFromPrimitive;
use smallvec::SmallVec; use smallvec::SmallVec;
mod boxing;
mod bytecode_reader;
#[cfg(feature = "tailcall")] #[cfg(feature = "tailcall")]
mod dispatch_tailcall; mod dispatch_tailcall;
mod forced; pub use fix_runtime::*;
mod value; mod instructions;
pub use value::StaticValue; mod primops;
use value::*;
mod helpers;
pub(crate) mod instructions;
pub(crate) use bytecode_reader::BytecodeReader;
pub(crate) use forced::Forced;
use helpers::*;
type VmResult<T> = std::result::Result<T, VmError>; type VmResult<T> = std::result::Result<T, VmError>;
#[allow(dead_code)]
enum VmError {
Catchable(String),
Uncatchable(Box<Error>),
}
impl From<Box<Error>> for VmError {
fn from(e: Box<Error>) -> Self {
VmError::Uncatchable(e)
}
}
impl VmError {
fn into_error(self) -> Box<Error> {
match self {
VmError::Catchable(_) => todo!("Check for tryEval catch frames"),
VmError::Uncatchable(e) => e,
}
}
}
#[derive(Collect, Clone, Copy, Debug, PartialEq, Eq, Default)]
#[collect(require_static)]
pub enum ForceMode {
#[default]
AsIs,
Shallow,
Deep,
}
pub trait VmContext {
fn intern_string(&mut self, s: impl AsRef<str>) -> StringId;
fn resolve_string(&self, id: StringId) -> &str;
fn bytecode(&self) -> &[u8];
fn get_const(&self, id: u32) -> StaticValue;
fn compile(&mut self, source: Source);
}
pub(crate) trait VmContextExt: VmContext {
fn get_string<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str>;
fn convert_value(&self, val: Value) -> fix_common::Value;
}
impl<T: VmContext> VmContextExt for T {
fn get_string<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str> {
if let Some(sid) = val.as_inline::<StringId>() {
Some(self.resolve_string(sid))
} else {
val.as_gc::<NixString>().map(|ns| ns.as_ref().as_str())
}
}
fn convert_value(&self, val: Value) -> fix_common::Value {
use fix_common::Value;
if let Some(i) = val.as_inline::<i32>() {
Value::Int(i as i64)
} else if let Some(gc_i) = val.as_gc::<i64>() {
Value::Int(*gc_i)
} else if let Some(f) = val.as_float() {
Value::Float(f)
} else if let Some(b) = val.as_inline::<bool>() {
Value::Bool(b)
} else if val.is::<Null>() {
Value::Null
} else if let Some(sid) = val.as_inline::<StringId>() {
let s = self.resolve_string(sid).to_owned();
Value::String(s)
} else if let Some(ns) = val.as_gc::<NixString>() {
Value::String(ns.as_str().to_owned())
} else if let Some(attrs) = val.as_gc::<AttrSet>() {
let mut map = std::collections::BTreeMap::new();
for &(key, val) in attrs.iter() {
let key = self.resolve_string(key).to_owned();
let converted = self.convert_value(val);
map.insert(fix_common::Symbol::from(key), converted);
}
Value::AttrSet(fix_common::AttrSet::new(map))
} else if let Some(list) = val.as_gc::<List>() {
let items: Vec<_> = list
.inner
.iter()
.copied()
.map(|v| self.convert_value(v))
.collect();
Value::List(fix_common::List::new(items))
} else if val.is::<Closure>() {
Value::Func
} else if val.is::<Thunk>() {
Value::Thunk
} else if val.as_inline::<PrimOp>().is_some() {
Value::PrimOp("primop".into())
} else if val.is::<PrimOpApp>() {
Value::PrimOpApp("primop-app".into())
} else {
Value::Null
}
}
}
#[repr(u8)]
pub(crate) enum Break {
Force,
Done,
}
pub(crate) type Step = std::ops::ControlFlow<Break>;
#[derive(Collect)] #[derive(Collect)]
#[collect(no_drop)] #[collect(no_drop)]
pub struct Vm<'gc> { pub struct Vm<'gc> {
pub(crate) stack: Vec<Value<'gc>>, stack: Vec<Value<'gc>>,
pub(crate) call_stack: Vec<CallFrame<'gc>>, call_stack: Vec<CallFrame<'gc>>,
pub(crate) call_depth: usize, call_depth: usize,
#[allow(dead_code)] #[allow(dead_code)]
#[collect(require_static)] #[collect(require_static)]
pub(crate) error_context: Vec<ErrorFrame>, error_context: Vec<ErrorFrame>,
pub(crate) env: GcEnv<'gc>, env: GcEnv<'gc>,
pub(crate) with_env: Option<GcWithEnv<'gc>>,
pub(crate) import_cache: HashMap<PathBuf, Value<'gc>>, import_cache: HashMap<PathBuf, Value<'gc>>,
scope_slots: Vec<Value<'gc>>,
pub(crate) builtins: Value<'gc>, builtins: Value<'gc>,
pub(crate) empty_list: Value<'gc>, empty_list: Value<'gc>,
pub(crate) empty_attrs: Value<'gc>, empty_attrs: Value<'gc>,
pub(crate) force_mode: ForceMode, force_mode: ForceMode,
#[collect(require_static)] #[collect(require_static)]
pub(crate) result: Option<Result<fix_common::Value>>, result: Option<Result<fix_lang::Value>>,
#[collect(require_static)]
pending_load: Option<PendingLoad>,
functor_sym: StringId,
} }
pub(crate) enum OperandData { fn init_builtins<'gc>(mc: &Mutation<'gc>, ctx: &mut impl VmRuntimeCtx) -> Value<'gc> {
Const(StaticValue),
Local { layer: u8, idx: u32 },
Builtins,
BigInt(i64),
}
impl OperandData {
pub(crate) fn resolve<'gc>(&self, mc: &Mutation<'gc>, root: &Vm<'gc>) -> Value<'gc> {
match *self {
OperandData::Const(sv) => sv.into(),
OperandData::Local { layer, idx } => {
let mut cur = root.env;
for _ in 0..layer {
let prev = cur.borrow().prev.expect("env chain too short");
cur = prev;
}
cur.borrow().locals[idx as usize]
}
OperandData::Builtins => root.builtins,
OperandData::BigInt(val) => Value::new_gc(Gc::new(mc, val)),
}
}
}
pub(crate) enum AttrKeyData {
Static(StringId),
Dynamic(OperandData),
}
fn init_builtins<'gc>(mc: &Mutation<'gc>, ctx: &mut impl VmContext) -> Value<'gc> {
let mut entries = SmallVec::with_capacity(BUILTINS.len()); let mut entries = SmallVec::with_capacity(BUILTINS.len());
for (idx, &(name, arity)) in BUILTINS.iter().enumerate() { for (idx, &(name, arity)) in BUILTINS.iter().enumerate() {
let id = BuiltinId::try_from_primitive(idx as u8).expect("infallible"); let id = BuiltinId::try_from(idx as u8).expect("infallible");
let name = name.strip_prefix("__").unwrap_or(name); let name = name.strip_prefix("__").unwrap_or(name);
let name = ctx.intern_string(name); let name = ctx.intern_string(name);
entries.push((name, Value::new_inline(PrimOp { id, arity }))); let dispatch_ip = PrimOpPhase::entry_for_builtin(id).ip();
entries.push((
name,
Value::new_inline(PrimOp {
id,
arity,
dispatch_ip,
}),
));
} }
let consts = [ let consts = [
@@ -220,15 +85,7 @@ fn init_builtins<'gc>(mc: &Mutation<'gc>, ctx: &mut impl VmContext) -> Value<'gc
"__storeDir", "__storeDir",
Value::new_inline(ctx.intern_string("/nix/store")), Value::new_inline(ctx.intern_string("/nix/store")),
), ),
( ("__nixPath", Value::new_gc(Gc::new(mc, List::default()))),
"__nixPath",
Value::new_gc(Gc::new(
mc,
List {
inner: SmallVec::new(),
},
)),
),
("null", Value::new_inline(Null)), ("null", Value::new_inline(Null)),
("true", Value::new_inline(true)), ("true", Value::new_inline(true)),
("false", Value::new_inline(false)), ("false", Value::new_inline(false)),
@@ -247,11 +104,14 @@ fn init_builtins<'gc>(mc: &Mutation<'gc>, ctx: &mut impl VmContext) -> Value<'gc
entries.sort_by_key(|(k, _)| *k); entries.sort_by_key(|(k, _)| *k);
let builtins_set = Gc::new(mc, AttrSet::from_sorted_unchecked(entries)); let builtins_set = Gc::new(mc, AttrSet::from_sorted_unchecked(entries));
Value::new_gc(builtins_set) let builtins_value = Value::new_gc(builtins_set);
*self_ref_thunk.borrow_mut(mc) =
ThunkState::Evaluated(builtins_value.restrict().expect("builtins is not a thunk"));
builtins_value
} }
impl<'gc> Vm<'gc> { impl<'gc> Vm<'gc> {
fn new(force_mode: ForceMode, mc: &Mutation<'gc>, ctx: &mut impl VmContext) -> Self { fn new(force_mode: ForceMode, mc: &Mutation<'gc>, ctx: &mut impl VmRuntimeCtx) -> Self {
let builtins = init_builtins(mc, ctx); let builtins = init_builtins(mc, ctx);
Vm { Vm {
stack: Vec::with_capacity(8192), stack: Vec::with_capacity(8192),
@@ -260,9 +120,9 @@ impl<'gc> Vm<'gc> {
error_context: Vec::with_capacity(1024), error_context: Vec::with_capacity(1024),
env: Gc::new(mc, RefLock::new(Env::empty())), env: Gc::new(mc, RefLock::new(Env::empty())),
with_env: None,
import_cache: HashMap::new(), import_cache: HashMap::new(),
scope_slots: Vec::new(),
builtins, builtins,
empty_list: Value::new_gc(Gc::new(mc, List::default())), empty_list: Value::new_gc(Gc::new(mc, List::default())),
@@ -271,46 +131,26 @@ impl<'gc> Vm<'gc> {
force_mode, force_mode,
result: None, result: None,
} pending_load: None,
}
#[inline(always)] functor_sym: ctx.intern_string("__functor"),
pub(crate) fn finish_ok(&mut self, val: fix_common::Value) -> Step {
self.result = Some(Ok(val));
Step::Break(Break::Done)
} }
#[inline(always)]
pub(crate) fn finish_err(&mut self, err: Box<Error>) -> Step {
self.result = Some(Err(err));
Step::Break(Break::Done)
} }
}
impl<'gc> Machine<'gc> for Vm<'gc> {
#[inline(always)] #[inline(always)]
pub(crate) fn finish_type_err(&mut self, expected: NixType, got: NixType) -> Step { fn push(&mut self, val: Value<'gc>) {
self.result = Some(Err(Error::eval_error(format!("expected {expected}, got {got}"))));
Step::Break(Break::Done)
}
#[inline(always)]
pub(crate) fn finish_vm_err(&mut self, err: VmError) -> Step {
self.finish_err(err.into_error())
}
#[inline(always)]
pub(crate) fn push(&mut self, val: Value<'gc>) {
self.stack.push(val); self.stack.push(val);
} }
#[inline(always)] #[inline(always)]
#[must_use] fn pop(&mut self) -> Value<'gc> {
pub(crate) fn pop(&mut self) -> Value<'gc> {
self.stack.pop().expect("stack underflow") self.stack.pop().expect("stack underflow")
} }
#[inline(always)] #[inline(always)]
#[must_use] fn peek(&self, depth: usize) -> Value<'gc> {
pub(crate) fn peek(&mut self, depth: usize) -> Value<'gc> {
*self *self
.stack .stack
.get(self.stack.len() - depth - 1) .get(self.stack.len() - depth - 1)
@@ -318,10 +158,8 @@ impl<'gc> Vm<'gc> {
} }
#[inline(always)] #[inline(always)]
#[must_use] fn peek_forced(&self, depth: usize) -> StrictValue<'gc> {
pub(crate) fn peek_forced(&mut self, depth: usize) -> StrictValue<'gc> { self.stack
self
.stack
.get(self.stack.len() - depth - 1) .get(self.stack.len() - depth - 1)
.expect("stack underflow") .expect("stack underflow")
.restrict() .restrict()
@@ -329,17 +167,7 @@ impl<'gc> Vm<'gc> {
} }
#[inline(always)] #[inline(always)]
pub(crate) fn replace(&mut self, depth: usize, val: Value<'gc>) { fn pop_forced(&mut self) -> StrictValue<'gc> {
let len = self.stack.len();
*self
.stack
.get_mut(len - depth - 1)
.expect("stack underflow") = val;
}
#[inline(always)]
#[cfg_attr(debug_assertions, track_caller)]
pub(crate) fn pop_forced(&mut self) -> StrictValue<'gc> {
self.stack self.stack
.pop() .pop()
.expect("stack underflow") .expect("stack underflow")
@@ -348,106 +176,249 @@ impl<'gc> Vm<'gc> {
} }
#[inline(always)] #[inline(always)]
pub(crate) fn try_force<T: Forced<'gc>>( fn replace(&mut self, depth: usize, val: Value<'gc>) {
&mut self, let len = self.stack.len();
reader: &mut BytecodeReader<'_>, *self
mc: &Mutation<'gc>, .stack
) -> std::ops::ControlFlow<Break, T> { .get_mut(len - depth - 1)
T::force_and_check(self, reader, mc, 0)?; .expect("stack underflow") = val;
std::ops::ControlFlow::Continue(T::pop_converted(self))
} }
#[inline(always)] #[inline(always)]
pub(crate) fn force_slot( fn drop_n(&mut self, depth: usize) {
self.stack.truncate(self.stack.len() - depth);
}
#[inline(always)]
fn stack_len(&self) -> usize {
self.stack.len()
}
#[inline(always)]
fn force_slot_to_pc(
&mut self, &mut self,
depth: usize, depth: usize,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
resume_pc: usize,
) -> Step { ) -> Step {
let Some(thunk) = self.peek(depth).as_gc::<Thunk>() else { let Some(thunk) = self.peek(depth).as_gc::<Thunk>() else {
return Step::Continue(()); return Step::Continue(());
}; };
let mut state = thunk.borrow_mut(mc); let mut state = thunk.borrow_mut(mc);
match *state { match *state {
ThunkState::Pending { ip, env, with_env } => { ThunkState::Pending { ip, env } => {
*state = ThunkState::Blackhole; *state = ThunkState::Blackhole;
drop(state);
self.call_stack.push(CallFrame { self.call_stack.push(CallFrame {
thunk: Some(thunk), thunk: Some(thunk),
stack_depth: depth, pc: resume_pc,
pc: reader.inst_start_pc(),
env: self.env, env: self.env,
with_env: self.with_env,
}); });
self.env = env; self.env = env;
self.with_env = with_env;
reader.set_pc(ip); reader.set_pc(ip);
Step::Break(Break::Force) Step::Break(Break::Force)
} }
ThunkState::Evaluated(v) => { ThunkState::Evaluated(v) => {
drop(state);
self.replace(depth, v.relax()); self.replace(depth, v.relax());
Step::Continue(()) Step::Continue(())
} }
ThunkState::Apply { .. } => todo!("force apply"), ThunkState::Apply { func, arg } => {
self.call_stack.push(CallFrame {
thunk: Some(thunk),
pc: resume_pc,
env: self.env,
});
self.push(func);
self.call(reader, mc, arg, resume_pc)
}
ThunkState::Blackhole => { ThunkState::Blackhole => {
drop(state);
self.finish_err(Error::eval_error("infinite recursion encountered")) self.finish_err(Error::eval_error("infinite recursion encountered"))
} }
} }
} }
#[inline(always)]
fn call(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
arg: Value<'gc>,
resume_pc: usize,
) -> Step {
instructions::call(self, reader, mc, arg, resume_pc)
}
#[inline(always)]
fn push_call_frame(&mut self, frame: CallFrame<'gc>) {
self.call_stack.push(frame);
}
#[inline(always)]
fn pop_call_frame(&mut self) -> Option<CallFrame<'gc>> {
self.call_stack.pop()
}
#[inline(always)]
fn call_depth(&self) -> usize {
self.call_depth
}
#[inline(always)]
fn inc_call_depth(&mut self) {
self.call_depth += 1;
}
#[inline(always)]
fn dec_call_depth(&mut self) {
self.call_depth -= 1;
}
#[inline(always)]
fn env(&self) -> GcEnv<'gc> {
self.env
}
#[inline(always)]
fn set_env(&mut self, env: GcEnv<'gc>) {
self.env = env;
}
#[inline(always)]
fn finish_ok(&mut self, val: fix_lang::Value) -> Step {
self.result = Some(Ok(val));
Step::Break(Break::Done)
}
#[inline(always)]
fn finish_err(&mut self, err: Box<Error>) -> Step {
self.result = Some(Err(err));
Step::Break(Break::Done)
}
#[inline(always)]
fn finish_type_err(&mut self, expected: NixType, got: NixType) -> Step {
self.result = Some(Err(Error::eval_error(format!(
"expected {expected}, got {got}"
))));
Step::Break(Break::Done)
}
#[inline(always)]
fn builtins(&self) -> Value<'gc> {
self.builtins
}
#[inline(always)]
fn functor_sym(&self) -> StringId {
self.functor_sym
}
#[inline(always)]
fn empty_list(&self) -> Value<'gc> {
self.empty_list
}
#[inline(always)]
fn empty_attrs(&self) -> Value<'gc> {
self.empty_attrs
}
#[inline(always)]
fn force_mode(&self) -> ForceMode {
self.force_mode
}
#[inline(always)]
fn import_cache_get(&self, path: &std::path::Path) -> Option<Value<'gc>> {
self.import_cache.get(path).copied()
}
#[inline(always)]
fn import_cache_insert(&mut self, path: PathBuf, val: Value<'gc>) {
self.import_cache.insert(path, val);
}
#[inline(always)]
fn scope_slot(&self, idx: u32) -> Value<'gc> {
*self
.scope_slots
.get(idx as usize)
.expect("invalid scope slot")
}
#[inline(always)]
fn scope_slots_push(&mut self, val: Value<'gc>) -> u32 {
let idx = self.scope_slots.len() as u32;
self.scope_slots.push(val);
idx
}
#[inline(always)]
fn set_pending_load(&mut self, load: PendingLoad) {
self.pending_load = Some(load);
}
} }
#[allow(dead_code)] enum Action {
struct ErrorFrame {
span_id: u32,
message: Option<String>,
}
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub(crate) struct CallFrame<'gc> {
pub(crate) pc: usize,
pub(crate) stack_depth: usize,
pub(crate) thunk: Option<Gc<'gc, Thunk<'gc>>>,
pub(crate) env: Gc<'gc, RefLock<Env<'gc>>>,
pub(crate) with_env: Option<Gc<'gc, WithEnv<'gc>>>,
}
pub(crate) enum Action {
Continue { pc: usize }, Continue { pc: usize },
Done(Result<fix_common::Value>), Done(Result<fix_lang::Value>),
LoadFile(PendingLoad),
} }
pub(crate) enum NixNum { /// Compute initial heap size mirroring CppNix's strategy: 25% of physical RAM,
Int(i64), /// clamped to [32 MiB, 384 MiB]. Used as `Pacing::min_sleep` so the collector
Float(f64), /// defers the first cycle until the heap reaches this size.
fn initial_heap_size() -> usize {
const MIN_SIZE: usize = 32 * 1024 * 1024;
const MAX_SIZE: usize = 384 * 1024 * 1024;
let mut sys = sysinfo::System::new();
sys.refresh_memory();
let total = sys.total_memory() as usize;
let quarter = total / 4;
quarter.clamp(MIN_SIZE, MAX_SIZE)
} }
impl Vm<'_> { impl Vm<'_> {
pub fn run<C: VmContext>( pub fn run<C: VmContext>(
mut ctx: C, ctx: &mut C,
ip: InstructionPtr, ip: InstructionPtr,
force_mode: ForceMode, force_mode: ForceMode,
) -> Result<fix_common::Value> { ) -> Result<fix_lang::Value> {
let mut arena: Arena<Rootable![Vm<'_>]> = let (code, runtime) = ctx.split();
Arena::new(|mc| Vm::new(force_mode, mc, &mut ctx)); let mut arena: Arena<Rootable![Vm<'_>]> = Arena::new(|mc| Vm::new(force_mode, mc, runtime));
arena.metrics().set_pacing(Pacing {
const COLLECTOR_GRANULARITY: f64 = 1024.0; min_sleep: initial_heap_size(),
..Pacing::STOP_THE_WORLD
});
let mut pc = ip.0; let mut pc = ip.0;
let bytecode: Vec<u8> = ctx.bytecode().to_vec();
loop { loop {
match arena.mutate_root(|mc, root| root.dispatch_batch(&bytecode, &mut ctx, pc, mc)) { let bytecode = code.bytecode();
match arena.mutate_root(|mc, root| root.dispatch_batch(bytecode, runtime, pc, mc)) {
Action::Continue { pc: new_pc } => { Action::Continue { pc: new_pc } => {
pc = new_pc; pc = new_pc;
if arena.metrics().allocation_debt() > COLLECTOR_GRANULARITY { if arena.metrics().allocation_debt() > 0.0 {
if arena.collection_phase() == CollectionPhase::Sweeping { arena.finish_cycle();
arena.collect_debt();
} else if let Some(marked) = arena.mark_debt() {
marked.start_sweeping();
} }
} }
Action::LoadFile(load) => {
let source = match Source::new_file(load.path) {
Ok(src) => src,
Err(err) => break Err(Error::eval_error(format!("import failed: {err}"))),
};
let extra_scope = load.scope.map(|s| ExtraScope::ScopedImport {
keys: s.keys,
slot_id: s.slot_id,
});
let new_ip = match code.compile_with_scope(source, extra_scope, runtime) {
Ok(ip) => ip,
Err(err) => break Err(err),
};
pc = new_ip.0;
arena.mutate_root(|mc, root| {
root.env = Gc::new(mc, RefLock::new(Env::empty()));
});
} }
Action::Done(done) => break done, Action::Done(done) => break done,
} }
@@ -459,7 +430,7 @@ impl<'gc> Vm<'gc> {
const DEFAULT_FUEL_AMOUNT: u32 = 1024; const DEFAULT_FUEL_AMOUNT: u32 = 1024;
#[inline(always)] #[inline(always)]
fn dispatch_batch<C: VmContext>( fn dispatch_batch<C: VmRuntimeCtx>(
&mut self, &mut self,
bytecode: &[u8], bytecode: &[u8],
ctx: &mut C, ctx: &mut C,
@@ -480,6 +451,11 @@ impl<'gc> Vm<'gc> {
TailResult::Done => { TailResult::Done => {
Action::Done(self.result.take().expect("TailResult::Done without result")) Action::Done(self.result.take().expect("TailResult::Done without result"))
} }
TailResult::LoadFile => Action::LoadFile(
self.pending_load
.take()
.expect("TailResult::LoadFile without pending_load"),
),
} }
} }
} }
@@ -489,11 +465,12 @@ impl<'gc> Vm<'gc> {
fn execute_batch( fn execute_batch(
&mut self, &mut self,
bytecode: &[u8], bytecode: &[u8],
ctx: &mut impl VmContext, ctx: &mut impl VmRuntimeCtx,
pc: usize, pc: usize,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Action { ) -> Action {
use fix_codegen::Op::*; use fix_bytecode::Op::*;
use instructions::*;
let mut reader = BytecodeReader::new(bytecode, pc); let mut reader = BytecodeReader::new(bytecode, pc);
let mut fuel = Self::DEFAULT_FUEL_AMOUNT; let mut fuel = Self::DEFAULT_FUEL_AMOUNT;
@@ -507,72 +484,75 @@ impl<'gc> Vm<'gc> {
let op = reader.read_op(); let op = reader.read_op();
let result = match op { let result = match op {
PushSmi => self.op_push_smi(&mut reader), PushSmi => op_push_smi(self, &mut reader),
PushBigInt => self.op_push_bigint(&mut reader, mc), PushBigInt => op_push_bigint(self, &mut reader, mc),
PushFloat => self.op_push_float(&mut reader), PushFloat => op_push_float(self, &mut reader),
PushString => self.op_push_string(&mut reader), PushString => op_push_string(self, &mut reader),
PushNull => self.op_push_null(), PushNull => op_push_null(self),
PushTrue => self.op_push_true(), PushTrue => op_push_true(self),
PushFalse => self.op_push_false(), PushFalse => op_push_false(self),
LoadLocal => self.op_load_local(&mut reader), LoadLocal => op_load_local(self, &mut reader),
LoadOuter => self.op_load_outer(&mut reader), LoadOuter => op_load_outer(self, &mut reader),
StoreLocal => self.op_store_local(&mut reader, mc), StoreLocal => op_store_local(self, &mut reader, mc),
AllocLocals => self.op_alloc_locals(&mut reader, mc), AllocLocals => op_alloc_locals(self, &mut reader, mc),
MakeThunk => self.op_make_thunk(&mut reader, mc), MakeThunk => op_make_thunk(self, &mut reader, mc),
MakeClosure => self.op_make_closure(&mut reader, mc), MakeClosure => op_make_closure(self, &mut reader, mc),
MakePatternClosure => self.op_make_pattern_closure(&mut reader, mc), MakePatternClosure => op_make_pattern_closure(self, &mut reader, mc),
Call => self.op_call(ctx, &mut reader, mc), Call => op_call(self, ctx, &mut reader, mc),
Return => self.op_return(ctx, &mut reader, mc), DispatchPrimOp => op_dispatch_primop(self, ctx, &mut reader, mc),
Return => op_return(self, ctx, &mut reader, mc),
MakeAttrs => self.op_make_attrs(ctx, &mut reader, mc), MakeAttrs => op_make_attrs(self, ctx, &mut reader, mc),
MakeEmptyAttrs => self.op_make_empty_attrs(), MakeEmptyAttrs => op_make_empty_attrs(self),
SelectStatic => self.op_select_static(ctx, &mut reader, mc), SelectStatic => op_select_static(self, ctx, &mut reader, mc),
SelectDynamic => self.op_select_dynamic(ctx, &mut reader, mc), SelectDynamic => op_select_dynamic(self, ctx, &mut reader, mc),
JumpIfSelectSucceeded => self.op_jump_if_select_succeeded(&mut reader), HasAttrPathStatic => op_has_attr_path_static(self, ctx, &mut reader, mc),
HasAttr => self.op_has_attr(&mut reader), HasAttrPathDynamic => op_has_attr_path_dynamic(self, ctx, &mut reader, mc),
HasAttrStatic => op_has_attr_static(self, &mut reader, mc),
HasAttrDynamic => op_has_attr_dynamic(self, ctx, &mut reader, mc),
HasAttrResolve => op_has_attr_resolve(self),
JumpIfSelectFailed => op_jump_if_select_failed(self, &mut reader),
JumpIfSelectSucceeded => op_jump_if_select_succeeded(self, &mut reader),
MakeList => self.op_make_list(ctx, &mut reader, mc), MakeList => op_make_list(self, ctx, &mut reader, mc),
MakeEmptyList => self.op_make_empty_list(), MakeEmptyList => op_make_empty_list(self),
OpAdd => self.op_add(ctx, &mut reader, mc), OpAdd => op_add(self, ctx, &mut reader, mc),
OpSub => self.op_sub(&mut reader, mc), OpSub => op_sub(self, &mut reader, mc),
OpMul => self.op_mul(&mut reader, mc), OpMul => op_mul(self, &mut reader, mc),
OpDiv => self.op_div(&mut reader, mc), OpDiv => op_div(self, &mut reader, mc),
OpEq => self.op_eq(ctx, &mut reader, mc), OpEq => op_eq(self, ctx, &mut reader, mc),
OpNeq => self.op_neq(ctx, &mut reader, mc), OpNeq => op_neq(self, ctx, &mut reader, mc),
OpLt => self.op_lt(ctx, &mut reader, mc), OpLt => op_lt(self, ctx, &mut reader, mc),
OpGt => self.op_gt(ctx, &mut reader, mc), OpGt => op_gt(self, ctx, &mut reader, mc),
OpLeq => self.op_leq(ctx, &mut reader, mc), OpLeq => op_leq(self, ctx, &mut reader, mc),
OpGeq => self.op_geq(ctx, &mut reader, mc), OpGeq => op_geq(self, ctx, &mut reader, mc),
OpConcat => self.op_concat(&mut reader, mc), OpConcat => op_concat(self, &mut reader, mc),
OpUpdate => self.op_update(&mut reader, mc), OpUpdate => op_update(self, &mut reader, mc),
OpNeg => self.op_neg(), OpNeg => op_neg(self, &mut reader, mc),
OpNot => self.op_not(), OpNot => op_not(self, &mut reader, mc),
JumpIfFalse => self.op_jump_if_false(&mut reader, mc), JumpIfFalse => op_jump_if_false(self, &mut reader, mc),
JumpIfTrue => self.op_jump_if_true(&mut reader, mc), JumpIfTrue => op_jump_if_true(self, &mut reader, mc),
Jump => self.op_jump(&mut reader), Jump => op_jump(self, &mut reader),
ConcatStrings => self.op_concat_strings(ctx, &mut reader, mc), ConcatStrings => op_concat_strings(self, ctx, &mut reader, mc),
ResolvePath => self.op_resolve_path(ctx), CoerceToString => op_coerce_to_string(self, &mut reader, mc),
ResolvePath => op_resolve_path(self, ctx, &mut reader, mc),
Assert => self.op_assert(&mut reader), Assert => op_assert(self, ctx, &mut reader, mc),
PushWith => self.op_push_with(ctx, &mut reader, mc), LookupWith => op_lookup_with(self, ctx, &mut reader, mc),
PopWith => self.op_pop_with(),
LookupWith => self.op_lookup_with(ctx, &mut reader, mc),
PrepareWith => self.op_prepare_with(),
LoadBuiltins => self.op_load_builtins(), LoadBuiltins => op_load_builtins(self),
LoadBuiltin => self.op_load_builtin(&mut reader), LoadBuiltin => op_load_builtin(self, &mut reader),
MkPos => self.op_mk_pos(&mut reader), LoadReplBinding => op_load_repl_binding(self, &mut reader),
LoadReplBinding => self.op_load_repl_binding(&mut reader), LoadScopedBinding => op_load_scoped_binding(self, ctx, &mut reader, mc),
LoadScopedBinding => self.op_load_scoped_binding(&mut reader),
Illegal => unreachable!(), Illegal => unreachable!(),
}; };
@@ -580,8 +560,13 @@ impl<'gc> Vm<'gc> {
match result { match result {
Step::Continue(()) | Step::Break(Break::Force) => {} Step::Continue(()) | Step::Break(Break::Force) => {}
Step::Break(Break::Done) => { Step::Break(Break::Done) => {
return Action::Done( return Action::Done(self.result.take().expect("Break::Done without result"));
self.result.take().expect("Break::Done without result"), }
Step::Break(Break::LoadFile) => {
return Action::LoadFile(
self.pending_load
.take()
.expect("Break::LoadFile without pending_load"),
); );
} }
} }
+447
View File
@@ -0,0 +1,447 @@
//! `builtins.hasContext`, `builtins.getContext`, `builtins.appendContext`,
//! `builtins.unsafeDiscardStringContext`,
//! `builtins.unsafeDiscardOutputDependency`.
//!
//! See `fix-runtime/src/string_context.rs` for the
//! `StringContextElem` type.
use fix_bytecode::PrimOpPhase;
use fix_error::Error;
use fix_lang::StringId;
use fix_runtime::{
AttrSet, BytecodeReader, List as VmList, Machine, MachineExt, NixString, NixType, Step,
StrictValue, StringContext, StringContextElem, Value, VmRuntimeCtx, VmRuntimeCtxExt,
};
use gc_arena::{Gc, Mutation};
use smallvec::SmallVec;
pub fn has_context<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
if !val.is::<StringId>() && val.as_gc::<NixString>().is_none() {
return m.finish_type_err(NixType::String, val.ty());
}
let has_ctx = !ctx.get_string_context(val).is_empty();
m.return_from_primop(Value::new_inline(has_ctx), reader)
}
pub fn unsafe_discard_string_context<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
if let Some(sid) = val.as_inline::<StringId>() {
return m.return_from_primop(Value::new_inline(sid), reader);
}
let Some(ns) = val.as_gc::<NixString>() else {
return m.finish_type_err(NixType::String, val.ty());
};
let sid = ctx.intern_string(ns.as_str());
m.return_from_primop(Value::new_inline(sid), reader)
}
pub fn unsafe_discard_output_dependency<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
if let Some(sid) = val.as_inline::<StringId>() {
return m.return_from_primop(Value::new_inline(sid), reader);
}
let Some(ns) = val.as_gc::<NixString>() else {
return m.finish_type_err(NixType::String, val.ty());
};
if ns.context().is_empty() {
let sid = ctx.intern_string(ns.as_str());
return m.return_from_primop(Value::new_inline(sid), reader);
}
let mut new_ctx = StringContext::new();
for elem in ns.context() {
let replacement = match elem {
StringContextElem::DrvDeep { drv_path } => StringContextElem::Opaque {
path: drv_path.clone(),
},
other => other.clone(),
};
new_ctx.insert(replacement);
}
let s: Box<str> = ns.as_str().into();
let new_ns = Gc::new(mc, NixString::with_context(s, new_ctx));
m.return_from_primop(Value::new_gc(new_ns), reader)
}
pub fn get_context<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
if !val.is::<StringId>() && val.as_gc::<NixString>().is_none() {
return m.finish_type_err(NixType::String, val.ty());
}
let elems = ctx.get_string_context(val);
struct Info {
path: bool,
all_outputs: bool,
outputs: SmallVec<[Box<str>; 2]>,
}
impl Info {
fn new() -> Self {
Self {
path: false,
all_outputs: false,
outputs: SmallVec::new(),
}
}
}
let mut by_path: std::collections::BTreeMap<Box<str>, Info> = std::collections::BTreeMap::new();
for elem in elems {
match elem {
StringContextElem::Opaque { path } => {
by_path.entry(path.clone()).or_insert_with(Info::new).path = true;
}
StringContextElem::DrvDeep { drv_path } => {
by_path
.entry(drv_path.clone())
.or_insert_with(Info::new)
.all_outputs = true;
}
StringContextElem::Built { drv_path, output } => {
by_path
.entry(drv_path.clone())
.or_insert_with(Info::new)
.outputs
.push(output.clone());
}
}
}
let mut outer_entries: SmallVec<[(StringId, Value<'gc>); 4]> = SmallVec::new();
for (path, mut info) in by_path {
info.outputs.sort();
info.outputs.dedup();
let mut sub: SmallVec<[(StringId, Value<'gc>); 4]> = SmallVec::new();
if info.all_outputs {
sub.push((ctx.intern_string("allOutputs"), Value::new_inline(true)));
}
if !info.outputs.is_empty() {
let items: smallvec::SmallVec<[Value<'gc>; 4]> = info
.outputs
.iter()
.map(|o| Value::new_inline(ctx.intern_string(o)))
.collect();
let list = VmList::new(mc, items);
sub.push((ctx.intern_string("outputs"), Value::new_gc(list)));
}
if info.path {
sub.push((ctx.intern_string("path"), Value::new_inline(true)));
}
sub.sort_by_key(|(k, _)| *k);
let sub_attrs = Gc::new(mc, AttrSet::from_sorted_unchecked(sub));
outer_entries.push((ctx.intern_string(&path), Value::new_gc(sub_attrs)));
}
outer_entries.sort_by_key(|(k, _)| *k);
let outer = Gc::new(mc, AttrSet::from_sorted_unchecked(outer_entries));
m.return_from_primop(Value::new_gc(outer), reader)
}
/// appendContext :: String -> AttrSet -> String
/// The context AttrSet maps store-path strings to `{ path?: Bool, allOutputs?:
/// Bool, outputs?: [String] }`. Each present field contributes one
/// StringContextElem to the result.
///
/// Requires forcing nested attrset values and list elements lazily, so it's
/// structured as a state machine with the following stack layout:
///
/// [strVal, attrs, idx, acc] - outer loop
/// [strVal, attrs, idx, acc, entryAttrs] - after entry forced
/// [strVal, attrs, idx, acc, list] - after `outputs` forced
/// [strVal, attrs, idx, acc, list, oidx] - output-element loop
/// [strVal, attrs, idx, acc, list, oidx, outElem] - after element forced
///
/// `acc` is a sentinel `NixString` whose `data` is empty and whose `context`
/// is the accumulator. The string value itself is preserved in `strVal` and
/// retrieved at finalization.
///
// TODO: handle thunk-valued `path` and `allOutputs` sub-attrs; currently they
// must be already-evaluated booleans.
pub fn append_context<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let (str_val, attrs) = m.force_and_retry::<(StrictValue, Gc<AttrSet>)>(reader, mc)?;
let initial_ctx: StringContext = ctx.get_string_context(str_val).clone();
let acc = Gc::new(mc, NixString::with_context("", initial_ctx));
m.push(str_val.relax());
m.push(Value::new_gc(attrs));
m.push(Value::new_inline(0i32));
m.push(Value::new_gc(acc));
reader.set_pc(PrimOpPhase::AppendContextLoop.ip() as usize);
Step::Continue(())
}
pub fn append_context_loop<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
#[allow(clippy::unwrap_used)]
let idx = m.peek(1).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let attrs = m.peek_forced(2).as_gc::<AttrSet>().unwrap();
if idx as usize >= attrs.entries.len() {
return append_context_finalize(m, ctx, reader, mc);
}
let entry_val = attrs.entries[idx as usize].1;
m.push(entry_val);
m.force_slot_to_pc(
0,
reader,
mc,
PrimOpPhase::AppendContextEntryForced.ip() as usize,
)?;
reader.set_pc(PrimOpPhase::AppendContextEntryForced.ip() as usize);
Step::Continue(())
}
pub fn append_context_entry_forced<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// Stack: [strVal, attrs, idx, acc, entryAttrs(thunk)]
// The slot still holds the Thunk pointer; re-force to extract the now-
// Evaluated value into the slot.
m.force_slot(0, reader, mc)?;
let entry_val = m.peek_forced(0);
let Some(entry_attrs) = entry_val.as_gc::<AttrSet>() else {
return m.finish_type_err(NixType::AttrSet, entry_val.ty());
};
#[allow(clippy::unwrap_used)]
let idx = m.peek(2).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let outer = m.peek_forced(3).as_gc::<AttrSet>().unwrap();
let path_key = outer.entries[idx as usize].0;
let path_str_owned: Box<str> = ctx.resolve_string(path_key).into();
if !path_str_owned.starts_with("/nix/store/") {
return m.finish_err(Error::eval_error(format!(
"context key '{path_str_owned}' is not a store path"
)));
}
// Eagerly handle `path` and `allOutputs` (assumed already-forced
// booleans - most callers either set them to literal `true` or omit
// them entirely).
// TODO: force these two attributes correctly
let path_id = ctx.intern_string("path");
let all_outputs_id = ctx.intern_string("allOutputs");
let outputs_id = ctx.intern_string("outputs");
#[allow(clippy::unwrap_used)]
let acc_gc = m.peek(1).as_gc::<NixString>().unwrap();
let mut new_acc: StringContext = acc_gc.context().iter().cloned().collect();
if let Some(v) = entry_attrs.lookup(path_id)
&& v.as_inline::<bool>() == Some(true)
{
new_acc.insert(StringContextElem::Opaque {
path: path_str_owned.clone(),
});
}
if let Some(v) = entry_attrs.lookup(all_outputs_id)
&& v.as_inline::<bool>() == Some(true)
{
if !path_str_owned.ends_with(".drv") {
return m.finish_err(Error::eval_error(format!(
"tried to add all-outputs context of {path_str_owned}, which is not a derivation, to a string"
)));
}
new_acc.insert(StringContextElem::DrvDeep {
drv_path: path_str_owned.clone(),
});
}
let new_acc_gc = Gc::new(mc, NixString::with_context("", new_acc));
m.replace(1, Value::new_gc(new_acc_gc));
if let Some(outputs_val) = entry_attrs.lookup(outputs_id) {
m.replace(0, outputs_val);
m.force_slot_to_pc(
0,
reader,
mc,
PrimOpPhase::AppendContextOutputsForced.ip() as usize,
)?;
reader.set_pc(PrimOpPhase::AppendContextOutputsForced.ip() as usize);
return Step::Continue(());
}
let _ = m.pop();
#[allow(clippy::unwrap_used)]
let idx_back = m.peek(1).as_inline::<i32>().unwrap();
m.replace(1, Value::new_inline(idx_back + 1));
reader.set_pc(PrimOpPhase::AppendContextLoop.ip() as usize);
Step::Continue(())
}
pub fn append_context_outputs_forced<'gc, M: Machine<'gc>>(
m: &mut M,
_ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
m.force_slot(0, reader, mc)?;
let list_val = m.peek_forced(0);
let Some(list) = list_val.as_gc::<VmList>() else {
return m.finish_type_err(NixType::List, list_val.ty());
};
if list.inner.borrow().is_empty() {
// Stack: [strVal, attrs, idx, acc, list] -> drop list, bump idx.
let _ = m.pop();
#[allow(clippy::unwrap_used)]
let idx_back = m.peek(1).as_inline::<i32>().unwrap();
m.replace(1, Value::new_inline(idx_back + 1));
reader.set_pc(PrimOpPhase::AppendContextLoop.ip() as usize);
return Step::Continue(());
}
m.push(Value::new_inline(0i32));
reader.set_pc(PrimOpPhase::AppendContextOutputElementLoop.ip() as usize);
Step::Continue(())
}
pub fn append_context_output_element_loop<'gc, M: Machine<'gc>>(
m: &mut M,
_ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
#[allow(clippy::unwrap_used)]
let oidx = m.peek(0).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let list = m.peek_forced(1).as_gc::<VmList>().unwrap();
let len = list.inner.borrow().len();
if oidx as usize >= len {
// Stack: [strVal, attrs, idx, acc, list, oidx] -> drop oidx & list,
// bump idx in place.
let _ = m.pop();
let _ = m.pop();
#[allow(clippy::unwrap_used)]
let idx_back = m.peek(1).as_inline::<i32>().unwrap();
m.replace(1, Value::new_inline(idx_back + 1));
reader.set_pc(PrimOpPhase::AppendContextLoop.ip() as usize);
return Step::Continue(());
}
let elem = list.inner.borrow()[oidx as usize];
m.push(elem);
m.force_slot_to_pc(
0,
reader,
mc,
PrimOpPhase::AppendContextOutputElementForced.ip() as usize,
)?;
reader.set_pc(PrimOpPhase::AppendContextOutputElementForced.ip() as usize);
Step::Continue(())
}
pub fn append_context_output_element_forced<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
m.force_slot(0, reader, mc)?;
let elem = m.peek_forced(0);
let Some(output_name) = ctx.get_string(elem) else {
return m.finish_type_err(NixType::String, elem.ty());
};
let output_name: Box<str> = output_name.into();
#[allow(clippy::unwrap_used)]
let idx = m.peek(4).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let outer = m.peek_forced(5).as_gc::<AttrSet>().unwrap();
let path_key = outer.entries[idx as usize].0;
let path_str: Box<str> = ctx.resolve_string(path_key).into();
if !path_str.ends_with(".drv") {
return m.finish_err(Error::eval_error(format!(
"tried to add derivation output context of {path_str}, which is not a derivation, to a string"
)));
}
#[allow(clippy::unwrap_used)]
let acc_gc = m.peek(3).as_gc::<NixString>().unwrap();
let mut new_acc: StringContext = acc_gc.context().iter().cloned().collect();
new_acc.insert(StringContextElem::Built {
drv_path: path_str,
output: output_name,
});
let new_acc_gc = Gc::new(mc, NixString::with_context("", new_acc));
m.replace(3, Value::new_gc(new_acc_gc));
// Stack: [strVal, attrs, idx, acc, list, oidx, outElem] -> drop outElem,
// bump oidx in place.
let _ = m.pop();
#[allow(clippy::unwrap_used)]
let oidx = m.peek(0).as_inline::<i32>().unwrap();
m.replace(0, Value::new_inline(oidx + 1));
reader.set_pc(PrimOpPhase::AppendContextOutputElementLoop.ip() as usize);
Step::Continue(())
}
fn append_context_finalize<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// Stack: [strVal, attrs, idx, acc]
#[allow(clippy::unwrap_used)]
let acc_gc = m.pop().as_gc::<NixString>().unwrap();
let _ = m.pop(); // idx
let _ = m.pop(); // attrs
let str_val_raw = m.pop();
// The strVal was already forced at entry; restrict() is infallible here.
let str_val = str_val_raw
.restrict()
.unwrap_or_else(|_| panic!("appendContext: strVal unexpectedly a thunk"));
let s_str = ctx.get_string(str_val).unwrap_or("").to_owned();
let context: StringContext = acc_gc.context().iter().cloned().collect();
let result = if context.is_empty() {
let sid = ctx.intern_string(s_str);
Value::new_inline(sid)
} else {
let ns = Gc::new(mc, NixString::with_context(s_str, context));
Value::new_gc(ns)
};
m.return_from_primop(result, reader)
}
+362
View File
@@ -0,0 +1,362 @@
use fix_bytecode::PrimOpPhase;
use fix_error::Error;
use fix_runtime::{
AttrSet, BytecodeReader, Closure, Env, List, Machine, MachineExt, Step, StrictValue, Value,
VmRuntimeCtx, VmRuntimeCtxExt,
};
use gc_arena::{Gc, Mutation, RefLock};
use smallvec::SmallVec;
pub fn seq<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// stack: [e1, e2] - force e1, return e2
m.force_slot(1, reader, mc)?;
let e2 = m.pop();
let _ = m.pop();
m.return_from_primop(e2, reader)
}
pub fn abort<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// stack: [msg] - force msg, then abort with it
m.force_slot(0, reader, mc)?;
let msg_val = m.peek_forced(0);
let msg = ctx.get_string(msg_val).unwrap_or("<non-string-value>");
m.finish_err(Error::eval_error(format!(
"evaluation aborted with the following error message: '{msg}'"
)))
}
pub fn deep_seq_force_top<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// stack: [e1, e2] - force e1, return e2
m.force_slot(1, reader, mc)?;
let e1 = m.peek_forced(1);
let children: SmallVec<_> = if let Some(attrs) = e1.as_gc::<AttrSet>() {
let attrs = &attrs.entries;
if attrs.is_empty() {
SmallVec::new()
} else {
attrs.iter().map(|&(_, v)| v).collect()
}
} else if let Some(list) = e1.as_gc::<List<'gc>>() {
let inner = list.inner.borrow();
if inner.is_empty() {
SmallVec::new()
} else {
inner.iter().copied().collect()
}
} else {
SmallVec::new()
};
if children.is_empty() {
let e2 = m.pop();
let _ = m.pop();
return m.return_from_primop(e2, reader);
}
let count = children.len() as i32;
let seen: Gc<'gc, List<'gc>> = Gc::new(mc, List::default());
let worklist: Gc<'gc, List<'gc>> = List::new(mc, children);
let e2 = m.pop();
let _ = m.pop();
m.push(e2);
m.push(Value::new_gc(seen));
m.push(Value::new_gc(worklist));
m.push(Value::new_inline(count));
reader.set_pc(PrimOpPhase::DeepSeqPush.ip() as usize);
Step::Continue(())
}
pub fn deep_seq_push<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// stack: [e2, seen, worklist, counter]
#[allow(clippy::unwrap_used)]
let counter = m.peek(0).as_inline::<i32>().unwrap();
if counter == 0 {
let _ = m.pop(); // counter
let _ = m.pop(); // worklist
let _ = m.pop(); // seen
let val = m.pop();
return m.return_from_primop(val, reader);
}
#[allow(clippy::unwrap_used)]
let worklist = m.peek_forced(1).as_gc::<List<'gc>>().unwrap();
#[allow(clippy::unwrap_used)]
let item = worklist.unlock(mc).borrow_mut().pop().unwrap();
m.replace(0, Value::new_inline(counter - 1));
m.push(item);
// force item at TOS, resume at DeepSeqLoop after force
m.force_slot_to_pc(0, reader, mc, PrimOpPhase::DeepSeqLoop.ip() as usize)?;
reader.set_pc(PrimOpPhase::DeepSeqLoop.ip() as usize);
Step::Continue(())
}
pub fn deep_seq_loop<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// stack after pop: [e2, seen, worklist, counter]
let item = m.pop();
#[allow(clippy::unwrap_used)]
let counter = m.peek(0).as_inline::<i32>().unwrap();
let mut added: usize = 0;
if let Some(attrs) = item.as_gc::<AttrSet>() {
let attrs = &attrs.entries;
#[allow(clippy::unwrap_used)]
let seen = m.peek_forced(2).as_gc::<List<'gc>>().unwrap();
if !is_value_in_seen(seen, item) {
add_value_to_seen(seen, mc, item);
#[allow(clippy::unwrap_used)]
let worklist = m.peek_forced(1).as_gc::<List<'gc>>().unwrap();
{
let mut wl = worklist.unlock(mc).borrow_mut();
for &(_, v) in attrs.iter() {
wl.push(v);
}
added = attrs.len();
}
}
} else if let Some(list) = item.as_gc::<List<'gc>>() {
#[allow(clippy::unwrap_used)]
let seen = m.peek_forced(2).as_gc::<List<'gc>>().unwrap();
if !is_value_in_seen(seen, item) {
add_value_to_seen(seen, mc, item);
#[allow(clippy::unwrap_used)]
let worklist = m.peek_forced(1).as_gc::<List<'gc>>().unwrap();
{
let inner = list.inner.borrow();
let mut wl = worklist.unlock(mc).borrow_mut();
for &v in inner.iter() {
wl.push(v);
}
added = inner.len();
}
}
}
m.replace(0, Value::new_inline(counter + added as i32));
reader.set_pc(PrimOpPhase::DeepSeqPush.ip() as usize);
Step::Continue(())
}
pub fn force_result_shallow<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
m.force_slot(0, reader, mc)?;
let val = m.peek_forced(0);
let (count, has_children) = if let Some(attrs) = val.as_gc::<AttrSet>() {
let len = attrs.entries.len();
(len, len > 0)
} else if let Some(list) = val.as_gc::<List<'gc>>() {
let len = list.inner.borrow().len();
(len, len > 0)
} else {
(0, false)
};
if !has_children {
let val = m.pop();
return m.finish_ok(ctx.convert_value(val));
}
m.push(Value::new_inline(0i32));
m.push(Value::new_inline(count as i32));
reader.set_pc(PrimOpPhase::ForceResultShallowPush.ip() as usize);
Step::Continue(())
}
pub fn force_result_shallow_push<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
#[allow(clippy::unwrap_used)]
let idx = m.peek(1).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let len = m.peek(0).as_inline::<i32>().unwrap();
if idx == len {
let _ = m.pop(); // len
let _ = m.pop(); // idx
let val = m.pop();
return m.finish_ok(ctx.convert_value(val));
}
let val = m.peek_forced(2);
let child = if let Some(attrs) = val.as_gc::<AttrSet>() {
attrs.entries.get(idx as usize).map(|&(_, v)| v)
} else if let Some(list) = val.as_gc::<List<'gc>>() {
list.inner.borrow().get(idx as usize).copied()
} else {
None
};
if let Some(child) = child {
m.replace(1, Value::new_inline(idx + 1));
m.push(child);
m.force_slot_to_pc(
0,
reader,
mc,
PrimOpPhase::ForceResultShallowLoop.ip() as usize,
)?;
reader.set_pc(PrimOpPhase::ForceResultShallowLoop.ip() as usize);
}
Step::Continue(())
}
pub fn force_result_shallow_loop<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
_mc: &Mutation<'gc>,
) -> Step {
let _ = m.pop(); // forced child
reader.set_pc(PrimOpPhase::ForceResultShallowPush.ip() as usize);
Step::Continue(())
}
pub fn force_result_deep_finish<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
m.finish_ok(ctx.convert_value(val.relax()))
}
fn is_value_in_seen<'gc>(seen: Gc<'gc, List<'gc>>, val: Value<'gc>) -> bool {
if !is_container(val) {
return false;
}
let target = val.to_bits();
for &v in seen.inner.borrow().iter() {
if v.to_bits() == target {
return true;
}
}
false
}
fn add_value_to_seen<'gc>(seen: Gc<'gc, List<'gc>>, mc: &Mutation<'gc>, val: Value<'gc>) {
if is_container(val) {
seen.unlock(mc).borrow_mut().push(val);
}
}
pub fn call_functor_1<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// Stack invariant on every (re-)entry: [..., orig_arg, self, functor]
// where `functor` is TOS. Retries during force land back here safely.
let functor = m.force_and_retry::<StrictValue>(reader, mc)?;
// Stack now: [..., orig_arg, self]
let self_val = m.pop();
m.push(functor.relax());
// Stack: [..., orig_arg, functor]
// Call 1: functor(self). Resume into CallFunctor2 once it returns.
m.call(
reader,
mc,
self_val,
PrimOpPhase::CallFunctor2.ip() as usize,
)
}
pub fn call_functor_2<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// Stack on entry: [..., orig_arg, intermediate]
// call_stack top: synthetic frame with caller's resume_pc.
let intermediate = m.pop();
let orig_arg = m.pop();
let saved = m.pop_call_frame().expect("functor outer frame missing");
m.set_env(saved.env);
m.push(intermediate);
// Call 2: intermediate(orig_arg). Resume to caller.
m.call(reader, mc, orig_arg, saved.pc)
}
pub fn call_pattern<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let (func, attrset) = m.force_and_retry::<(Gc<Closure>, Gc<AttrSet>)>(reader, mc)?;
let Closure {
ip,
n_locals,
env,
pattern,
} = *func;
let Some(pattern) = pattern else {
unreachable!()
};
// TODO: get function name
// TODO: param spans
if !pattern.ellipsis {
for key in pattern.required.iter().copied() {
if attrset.lookup(key).is_none() {
let name = ctx.resolve_string(key);
return m.finish_err(Error::eval_error(format!(
"function 'anonymous lambda' called without required argument '{name}'"
)));
}
}
for &(key, _) in attrset.entries.iter() {
let is_expected = pattern.required.contains(&key) || pattern.optional.contains(&key);
if !is_expected {
let name = ctx.resolve_string(key);
return m.finish_err(Error::eval_error(format!(
"function 'anonymous lambda' called with unexpected argument '{name}'"
)));
}
}
}
let new_env = Gc::new(
mc,
RefLock::new(Env::with_arg(Value::new_gc(attrset), n_locals, env)),
);
reader.set_pc(ip as usize);
m.set_env(new_env);
Step::Continue(())
}
fn is_container(val: Value<'_>) -> bool {
val.is::<AttrSet>() || val.is::<List<'_>>()
}
+51
View File
@@ -0,0 +1,51 @@
use fix_error::Error;
use fix_lang::StringId;
use fix_runtime::{
BytecodeReader, Machine, MachineExt, NixString, NixType, Path, Step, StrictValue, Value,
VmRuntimeCtx,
};
use gc_arena::Mutation;
pub fn to_string<'gc, M: Machine<'gc>>(
m: &mut M,
_ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
if val.is::<StringId>() || val.is::<NixString>() {
return m.return_from_primop(val.relax(), reader);
}
if let Some(p) = val.as_inline::<Path>() {
return m.return_from_primop(Value::new_inline(p.0), reader);
}
// TODO: derivations / `__toString` / `outPath`,
// numbers, lists.
m.finish_err(Error::eval_error(format!(
"cannot coerce {} to a string",
val.ty()
)))
}
pub fn type_of<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
let name: &str = match val.ty() {
NixType::Int => "int",
NixType::Float => "float",
NixType::Bool => "bool",
NixType::Null => "null",
NixType::String => "string",
NixType::Path => "path",
NixType::AttrSet => "set",
NixType::List => "list",
NixType::Closure | NixType::PrimOp | NixType::PrimOpApp => "lambda",
NixType::Thunk => unreachable!("forced"),
};
let sid = ctx.intern_string(name);
m.return_from_primop(Value::new_inline(sid), reader)
}
+237
View File
@@ -0,0 +1,237 @@
use fix_bytecode::PrimOpPhase;
use fix_runtime::{
AttrSet, BytecodeReader, CallFrame, List, Machine, MachineExt, NixNum, Null, Path, Step,
StrictValue, Value, VmRuntimeCtx, VmRuntimeCtxExt,
};
use gc_arena::{Gc, Mutation};
use smallvec::SmallVec;
pub fn start_eq<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
lhs: StrictValue<'gc>,
rhs: StrictValue<'gc>,
negate: bool,
) -> Step {
match shallow_eq(ctx, lhs, rhs) {
ShallowEq::True => {
m.push(Value::new_inline(!negate));
Step::Continue(())
}
ShallowEq::False => {
m.push(Value::new_inline(negate));
Step::Continue(())
}
ShallowEq::RecurseList(la, lb) => {
let lhs_init: SmallVec<[Value<'gc>; 4]> = la.inner.borrow().iter().copied().collect();
let rhs_init: SmallVec<[Value<'gc>; 4]> = lb.inner.borrow().iter().copied().collect();
enter_eq_machine(m, reader, mc, negate, lhs_init, rhs_init)
}
ShallowEq::RecurseAttrs(a, b) => {
let lhs_init: SmallVec<[Value<'gc>; 4]> = a.entries.iter().map(|&(_, v)| v).collect();
let rhs_init: SmallVec<[Value<'gc>; 4]> = b.entries.iter().map(|&(_, v)| v).collect();
enter_eq_machine(m, reader, mc, negate, lhs_init, rhs_init)
}
}
}
pub fn eq_step<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let rhs_q = m
.peek(0)
.as_gc::<List<'gc>>()
.expect("eq state corrupted: rhs_queue");
let lhs_q = m
.peek(1)
.as_gc::<List<'gc>>()
.expect("eq state corrupted: lhs_queue");
let result = m
.peek(2)
.as_inline::<bool>()
.expect("eq state corrupted: result");
if !result || lhs_q.inner.borrow().is_empty() {
return finalize(m, reader);
}
let lhs = lhs_q
.unlock(mc)
.borrow_mut()
.pop()
.expect("non-empty lhs queue");
let rhs = rhs_q
.unlock(mc)
.borrow_mut()
.pop()
.expect("non-empty rhs queue");
m.push(lhs);
m.push(rhs);
reader.set_pc(PrimOpPhase::EqForce.ip() as usize);
Step::Continue(())
}
pub fn eq_force<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let (lhs, rhs) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
apply_pair(m, ctx, mc, lhs, rhs);
reader.set_pc(PrimOpPhase::EqStep.ip() as usize);
Step::Continue(())
}
fn finalize<'gc, M: Machine<'gc>>(m: &mut M, reader: &mut BytecodeReader<'_>) -> Step {
let _ = m.pop();
let _ = m.pop();
let result = m
.pop()
.as_inline::<bool>()
.expect("eq state corrupted: result");
let negate = m
.pop()
.as_inline::<bool>()
.expect("eq state corrupted: negate");
m.return_from_primop(Value::new_inline(result ^ negate), reader)
}
fn apply_pair<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &impl VmRuntimeCtx,
mc: &Mutation<'gc>,
lhs: StrictValue<'gc>,
rhs: StrictValue<'gc>,
) {
match shallow_eq(ctx, lhs, rhs) {
ShallowEq::True => {}
ShallowEq::False => {
m.replace(2, Value::new_inline(false));
}
ShallowEq::RecurseList(la, lb) => {
extend_queues(
m,
mc,
la.inner.borrow().iter().copied(),
lb.inner.borrow().iter().copied(),
);
}
ShallowEq::RecurseAttrs(a, b) => {
extend_queues(
m,
mc,
a.entries.iter().map(|&(_, v)| v),
b.entries.iter().map(|&(_, v)| v),
);
}
}
}
fn extend_queues<'gc, M, L, R>(m: &mut M, mc: &Mutation<'gc>, lhs_iter: L, rhs_iter: R)
where
M: Machine<'gc>,
L: IntoIterator<Item = Value<'gc>>,
R: IntoIterator<Item = Value<'gc>>,
{
let rhs_q = m
.peek(0)
.as_gc::<List<'gc>>()
.expect("eq state corrupted: rhs_queue");
let lhs_q = m
.peek(1)
.as_gc::<List<'gc>>()
.expect("eq state corrupted: lhs_queue");
let mut lq = lhs_q.unlock(mc).borrow_mut();
let mut rq = rhs_q.unlock(mc).borrow_mut();
for (x, y) in lhs_iter.into_iter().zip(rhs_iter) {
lq.push(x);
rq.push(y);
}
}
fn enter_eq_machine<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
negate: bool,
lhs_init: SmallVec<[Value<'gc>; 4]>,
rhs_init: SmallVec<[Value<'gc>; 4]>,
) -> Step {
let resume_pc = reader.pc();
m.push_call_frame(CallFrame {
pc: resume_pc,
thunk: None,
env: m.env(),
});
m.inc_call_depth();
m.push(Value::new_inline(negate));
m.push(Value::new_inline(true));
m.push(Value::new_gc(List::new(mc, lhs_init)));
m.push(Value::new_gc(List::new(mc, rhs_init)));
reader.set_pc(PrimOpPhase::EqStep.ip() as usize);
Step::Continue(())
}
enum ShallowEq<'gc> {
True,
False,
RecurseList(Gc<'gc, List<'gc>>, Gc<'gc, List<'gc>>),
RecurseAttrs(Gc<'gc, AttrSet<'gc>>, Gc<'gc, AttrSet<'gc>>),
}
fn shallow_eq<'gc>(
ctx: &impl VmRuntimeCtx,
lhs: StrictValue<'gc>,
rhs: StrictValue<'gc>,
) -> ShallowEq<'gc> {
if let (Some(a), Some(b)) = (lhs.as_num(), rhs.as_num()) {
let eq = match (a, b) {
(NixNum::Int(a), NixNum::Int(b)) => a == b,
(NixNum::Float(a), NixNum::Float(b)) => a == b,
(NixNum::Int(a), NixNum::Float(b)) => a as f64 == b,
(NixNum::Float(a), NixNum::Int(b)) => a == b as f64,
};
return bool_outcome(eq);
}
if let (Some(a), Some(b)) = (lhs.as_inline::<bool>(), rhs.as_inline::<bool>()) {
return bool_outcome(a == b);
}
if lhs.is::<Null>() && rhs.is::<Null>() {
return ShallowEq::True;
}
if let (Some(a), Some(b)) = (lhs.as_inline::<Path>(), rhs.as_inline::<Path>()) {
return bool_outcome(a.0 == b.0);
}
if let (Some(a), Some(b)) = (ctx.get_string(lhs), ctx.get_string(rhs)) {
return bool_outcome(a == b);
}
if let (Some(a), Some(b)) = (lhs.as_gc::<List<'gc>>(), rhs.as_gc::<List<'gc>>()) {
if a.inner.borrow().len() != b.inner.borrow().len() {
return ShallowEq::False;
}
return ShallowEq::RecurseList(a, b);
}
if let (Some(a), Some(b)) = (lhs.as_gc::<AttrSet<'gc>>(), rhs.as_gc::<AttrSet<'gc>>()) {
let ae = &a.entries;
let be = &b.entries;
if ae.len() != be.len() {
return ShallowEq::False;
}
for (l, r) in ae.iter().zip(be.iter()) {
if l.0 != r.0 {
return ShallowEq::False;
}
}
return ShallowEq::RecurseAttrs(a, b);
}
ShallowEq::False
}
fn bool_outcome<'gc>(b: bool) -> ShallowEq<'gc> {
if b { ShallowEq::True } else { ShallowEq::False }
}
+190
View File
@@ -0,0 +1,190 @@
use std::path::PathBuf;
use fix_bytecode::PrimOpPhase;
use fix_error::Error;
use fix_lang::StringId;
use fix_runtime::{
AttrSet, Break, BytecodeReader, CallFrame, Machine, MachineExt, Path, PendingLoad,
PendingScope, Step, StrictValue, Value, VmRuntimeCtx, VmRuntimeCtxExt, canon_path_str,
};
use gc_arena::{Gc, Mutation};
use hashbrown::HashSet;
pub fn import<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// stack: [path]
let path_val = m.force_and_retry::<StrictValue>(reader, mc)?;
let path_str = match ctx.get_string_or_path(path_val) {
Some(s) => s.to_owned(),
None => {
return m.finish_err(Error::eval_error(format!(
"expected a path or string, got {}",
path_val.ty()
)));
}
};
let abs = match resolve_import_target(&path_str) {
Ok(p) => p,
Err(e) => return m.finish_err(e),
};
if let Some(cached) = m.import_cache_get(&abs) {
return m.return_from_primop(cached, reader);
}
// Stash the resolved path on the stack as a string-id so the
// finalizer can use it as the cache key. The slot we pop here was
// freed by `force_and_retry`, so we simply push.
let path_sid = ctx.intern_string(abs.to_string_lossy());
m.push(Value::new_inline(path_sid));
let env = m.env();
m.push_call_frame(CallFrame {
pc: PrimOpPhase::ImportFinalize.ip() as usize,
thunk: None,
env,
});
m.set_pending_load(PendingLoad {
path: abs,
scope: None,
});
Step::Break(Break::LoadFile)
}
pub fn import_finalize<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
) -> Step {
// stack: [path_sid, return_value]
let val = m.pop();
#[allow(clippy::unwrap_used)]
let path_sid = m.pop().as_inline::<StringId>().unwrap();
// The cache key is keyed by the absolute path string we interned in
// `import`. Resolve it back to the host PathBuf.
let path_str = ctx.resolve_string(path_sid).to_owned();
m.import_cache_insert(PathBuf::from(path_str), val);
m.push(val);
let Some(CallFrame {
pc: ret_pc,
thunk: _,
env,
}) = m.pop_call_frame()
else {
unreachable!()
};
reader.set_pc(ret_pc);
// FIXME:
// m.dec_call_depth();
m.set_env(env);
Step::Continue(())
}
pub fn scoped_import<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// stack: [scope, path]
let (scope_attrs, path_val) = m.force_and_retry::<(Gc<AttrSet>, StrictValue)>(reader, mc)?;
let path_str = match ctx.get_string_or_path(path_val) {
Some(s) => s.to_owned(),
None => {
return m.finish_err(Error::eval_error(format!(
"expected a path or string, got {}",
path_val.ty()
)));
}
};
let abs = match resolve_import_target(&path_str) {
Ok(p) => p,
Err(e) => return m.finish_err(e),
};
let keys: HashSet<StringId> = scope_attrs.entries.iter().map(|&(k, _)| k).collect();
let slot_id = m.scope_slots_push(Value::new_gc(scope_attrs));
let env = m.env();
m.push_call_frame(CallFrame {
pc: PrimOpPhase::ScopedImportFinalize.ip() as usize,
thunk: None,
env,
});
m.set_pending_load(PendingLoad {
path: abs,
scope: Some(PendingScope { keys, slot_id }),
});
Step::Break(Break::LoadFile)
}
pub fn scoped_import_finalize<'gc, M: Machine<'gc>>(
m: &mut M,
_ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
_mc: &Mutation<'gc>,
) -> Step {
// stack: [return_value]
// We intentionally do NOT pop the slot from `scope_slots` so that
// closures or thunks created inside the imported file can still
// resolve their scope after `scopedImport` returns.
let val = m.pop();
m.return_from_primop(val, reader)
}
pub fn path_exists<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let path_val = m.force_and_retry::<StrictValue>(reader, mc)?;
// pathExists requires an absolute path. A `Path` value is
// always absolute; a string is accepted only if it starts with `/`.
let (path, is_path_value) = if let Some(p) = path_val.as_inline::<Path>() {
(ctx.resolve_string(p.0).to_owned(), true)
} else if let Some(s) = ctx.get_string(path_val) {
(s.to_owned(), false)
} else {
return m.finish_err(Error::eval_error(format!(
"expected a path or string, got {}",
path_val.ty()
)));
};
if !is_path_value && !path.starts_with('/') {
return m.finish_err(Error::eval_error(format!(
"string '{path}' doesn't represent an absolute path"
)));
}
// CppNix collapses consecutive slashes and resolves `.` / `..` lexically
// before checking. Trailing-slash / trailing-dot mean "must be a directory".
let must_be_dir = path.ends_with('/') || path.ends_with("/.");
let canon = canon_path_str(&path);
let p = std::path::Path::new(&canon);
let exists = if must_be_dir {
std::fs::metadata(p).map(|m| m.is_dir()).unwrap_or(false)
} else {
std::fs::symlink_metadata(p).is_ok()
};
m.return_from_primop(Value::new_inline(exists), reader)
}
/// Convert the user-supplied path string into an absolute, dotted-segment
/// resolved `PathBuf` and append `default.nix` if the target is a directory.
fn resolve_import_target(path: &str) -> Result<PathBuf, Box<Error>> {
let mut abs = PathBuf::from(path);
if !abs.is_absolute() {
return Err(Error::eval_error(format!(
"import: expected an absolute path, got '{path}'"
)));
}
if abs.is_dir() {
abs.push("default.nix");
}
Ok(abs)
}
+283
View File
@@ -0,0 +1,283 @@
use fix_bytecode::PrimOpPhase;
use fix_runtime::{BytecodeReader, List, Machine, MachineExt, NixType, Step, StrictValue, Value};
use gc_arena::Mutation;
pub fn filter_force_list<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
m.force_slot(0, reader, mc)?;
let list = match m.peek_forced(0).expect_gc::<List>() {
Ok(list) => list,
Err(got) => return m.finish_type_err(NixType::List, got),
};
if list.inner.borrow().is_empty() {
let val = m.pop();
let _pred = m.pop();
return m.return_from_primop(val, reader);
}
// prepare stack layout: [ pred list idx acc ]
m.push(Value::new_inline(0));
m.push(Value::new_gc(List::new_gc(mc)));
reader.set_pc(PrimOpPhase::FilterCallPred.ip() as usize);
Step::Continue(())
}
pub fn filter_call_pred<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
m.force_slot(3, reader, mc)?;
let pred = m.peek_forced(3);
#[allow(clippy::unwrap_used)]
let idx = m.peek(1).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let elem = m.peek_forced(2).as_gc::<List>().unwrap().inner.borrow()[idx as usize];
m.push(pred.relax());
m.call(reader, mc, elem, PrimOpPhase::FilterCheck.ip() as usize)
}
pub fn filter_check<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let ret = m.force_and_retry::<bool>(reader, mc)?;
#[allow(clippy::unwrap_used)]
let idx = m.peek(1).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let list = m.peek_forced(2).as_gc::<List>().unwrap();
let list = list.inner.borrow();
#[allow(clippy::unwrap_used)]
let acc = m.peek_forced(0).as_gc::<List>().unwrap();
if ret {
let mut acc = acc.unlock(mc).borrow_mut();
acc.push(list[idx as usize]);
}
if idx as usize == list.len() - 1 {
let acc = m.pop();
let _ = m.pop(); // idx
let _ = m.pop(); // list
let _ = m.pop(); // pred
return m.return_from_primop(acc, reader);
}
m.replace(1, Value::new_inline(idx + 1));
reader.set_pc(PrimOpPhase::FilterCallPred.ip() as usize);
Step::Continue(())
}
// foldl' op nul list
//
// Stack layouts across phases:
// Entry: [op, nul, list]
// Empty: [op, nul]
// Call1: [op, list, idx, acc]
// Call2: [op, list, idx, acc, intermediate]
// Update: [op, list, idx, acc, result]
pub fn foldl_strict_entry<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
m.force_slot(0, reader, mc)?;
let list_val = m.peek_forced(0);
let Some(list) = list_val.as_gc::<List>() else {
return m.finish_type_err(NixType::List, list_val.ty());
};
if list.inner.borrow().is_empty() {
let _ = m.pop(); // list
reader.set_pc(PrimOpPhase::FoldlStrictEmpty.ip() as usize);
return Step::Continue(());
}
let list_val = m.pop();
let nul_val = m.pop();
m.push(list_val);
m.push(Value::new_inline(0i32));
m.push(nul_val);
reader.set_pc(PrimOpPhase::FoldlStrictCall1.ip() as usize);
Step::Continue(())
}
pub fn foldl_strict_empty<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let nul = m.force_and_retry::<StrictValue>(reader, mc)?;
let _ = m.pop(); // op
m.return_from_primop(nul.relax(), reader)
}
pub fn foldl_strict_call1<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
m.force_slot(3, reader, mc)?;
let op = m.peek_forced(3);
let acc = m.peek(0);
m.push(op.relax());
m.call(reader, mc, acc, PrimOpPhase::FoldlStrictCall2.ip() as usize)
}
pub fn foldl_strict_call2<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
#[allow(clippy::unwrap_used)]
let idx = m.peek(2).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let list = m.peek_forced(3).as_gc::<List>().unwrap();
let elem = list.inner.borrow()[idx as usize];
m.call(
reader,
mc,
elem,
PrimOpPhase::FoldlStrictUpdate.ip() as usize,
)
}
pub fn foldl_strict_update<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
_mc: &Mutation<'gc>,
) -> Step {
let result = m.pop();
m.replace(0, result);
#[allow(clippy::unwrap_used)]
let idx = m.peek(1).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let list = m.peek_forced(2).as_gc::<List>().unwrap();
let len = list.inner.borrow().len();
if (idx as usize) + 1 == len {
let acc = m.pop();
let _ = m.pop(); // idx
let _ = m.pop(); // list
let _ = m.pop(); // op
return m.return_from_primop(acc, reader);
}
m.replace(1, Value::new_inline(idx + 1));
reader.set_pc(PrimOpPhase::FoldlStrictCall1.ip() as usize);
Step::Continue(())
}
pub fn all_entry<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
m.force_slot(0, reader, mc)?;
let list = match m.peek_forced(0).expect_gc::<List>() {
Ok(list) => list,
Err(got) => return m.finish_type_err(NixType::List, got),
};
// FIXME: force callable
m.force_slot(1, reader, mc)?;
if list.inner.borrow().is_empty() {
let _list = m.pop();
let _pred = m.pop();
return m.return_from_primop(Value::new_inline(true), reader);
}
// prepare stack layout: [ pred list idx ]
m.push(Value::new_inline(0));
reader.set_pc(PrimOpPhase::AllCallPred.ip() as usize);
Step::Continue(())
}
pub fn all_call_pred<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let pred = m.peek_forced(2);
#[allow(clippy::unwrap_used)]
let idx = m.peek(0).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let elem = m.peek_forced(1).as_gc::<List>().unwrap().inner.borrow()[idx as usize];
m.push(pred.relax());
m.call(reader, mc, elem, PrimOpPhase::AllCheck.ip() as usize)
}
pub fn all_check<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let ret = m.force_and_retry::<bool>(reader, mc)?;
#[allow(clippy::unwrap_used)]
let idx = m.peek(0).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let list = m.peek_forced(1).as_gc::<List>().unwrap();
let list = list.inner.borrow();
if idx as usize == list.len() - 1 || !ret {
let _ = m.pop(); // idx
let _ = m.pop(); // list
let _ = m.pop(); // pred
return m.return_from_primop(Value::new_inline(ret), reader);
}
m.replace(0, Value::new_inline(idx + 1));
reader.set_pc(PrimOpPhase::AllCallPred.ip() as usize);
Step::Continue(())
}
pub fn any_entry<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
m.force_slot(0, reader, mc)?;
let list = match m.peek_forced(0).expect_gc::<List>() {
Ok(list) => list,
Err(got) => return m.finish_type_err(NixType::List, got),
};
// FIXME: force callable
m.force_slot(1, reader, mc)?;
if list.inner.borrow().is_empty() {
let _list = m.pop();
let _pred = m.pop();
return m.return_from_primop(Value::new_inline(false), reader);
}
// prepare stack layout: [ pred list idx ]
m.push(Value::new_inline(0));
reader.set_pc(PrimOpPhase::AnyCallPred.ip() as usize);
Step::Continue(())
}
pub fn any_call_pred<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let pred = m.peek_forced(2);
#[allow(clippy::unwrap_used)]
let idx = m.peek(0).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let elem = m.peek_forced(1).as_gc::<List>().unwrap().inner.borrow()[idx as usize];
m.push(pred.relax());
m.call(reader, mc, elem, PrimOpPhase::AnyCheck.ip() as usize)
}
pub fn any_check<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let ret = m.force_and_retry::<bool>(reader, mc)?;
#[allow(clippy::unwrap_used)]
let idx = m.peek(0).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let list = m.peek_forced(1).as_gc::<List>().unwrap();
let list = list.inner.borrow();
if idx as usize == list.len() - 1 || ret {
let _ = m.pop(); // idx
let _ = m.pop(); // list
let _ = m.pop(); // pred
return m.return_from_primop(Value::new_inline(ret), reader);
}
m.replace(0, Value::new_inline(idx + 1));
reader.set_pc(PrimOpPhase::AnyCallPred.ip() as usize);
Step::Continue(())
}
+97
View File
@@ -0,0 +1,97 @@
mod context;
mod control;
mod conv;
mod eq;
mod io;
mod list;
mod path;
pub use context::*;
pub use control::*;
pub use conv::*;
pub use eq::*;
use fix_bytecode::PrimOpPhase;
use fix_error::Error;
use fix_runtime::{BytecodeReader, Machine, Step, VmRuntimeCtx};
use gc_arena::Mutation;
pub use io::*;
pub use list::*;
pub use path::*;
#[allow(clippy::too_many_lines)]
pub fn dispatch_primop<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
use PrimOpPhase::*;
let phase_disc = reader.read_u8();
let Ok(phase) = PrimOpPhase::try_from(phase_disc) else {
return m.finish_err(Error::eval_error("invalid primop phase"));
};
match phase {
Abort => abort(m, ctx, reader, mc),
All => all_entry(m, reader, mc),
AllCallPred => all_call_pred(m, reader, mc),
AllCheck => all_check(m, reader, mc),
Any => any_entry(m, reader, mc),
AnyCallPred => any_call_pred(m, reader, mc),
AnyCheck => any_check(m, reader, mc),
DeepSeq => deep_seq_force_top(m, reader, mc),
DeepSeqPush => deep_seq_push(m, reader, mc),
DeepSeqLoop => deep_seq_loop(m, reader, mc),
Seq => seq(m, reader, mc),
FilterForceList => filter_force_list(m, reader, mc),
FilterCallPred => filter_call_pred(m, reader, mc),
FilterCheck => filter_check(m, reader, mc),
FoldlStrict => foldl_strict_entry(m, reader, mc),
FoldlStrictEmpty => foldl_strict_empty(m, reader, mc),
FoldlStrictCall1 => foldl_strict_call1(m, reader, mc),
FoldlStrictCall2 => foldl_strict_call2(m, reader, mc),
FoldlStrictUpdate => foldl_strict_update(m, reader, mc),
ForceResultShallow => force_result_shallow(m, ctx, reader, mc),
ForceResultShallowPush => force_result_shallow_push(m, ctx, reader, mc),
ForceResultShallowLoop => force_result_shallow_loop(m, reader, mc),
ForceResultDeepFinish => force_result_deep_finish(m, ctx, reader, mc),
EqStep => eq_step(m, reader, mc),
EqForce => eq_force(m, ctx, reader, mc),
CallPattern => call_pattern(m, ctx, reader, mc),
CallFunctor1 => call_functor_1(m, reader, mc),
CallFunctor2 => call_functor_2(m, reader, mc),
Import => import(m, ctx, reader, mc),
ImportFinalize => import_finalize(m, ctx, reader),
ScopedImport => scoped_import(m, ctx, reader, mc),
ScopedImportFinalize => scoped_import_finalize(m, ctx, reader, mc),
PathExists => path_exists(m, ctx, reader, mc),
ToPath => to_path(m, ctx, reader, mc),
IsPath => is_path(m, reader, mc),
ToString => to_string(m, ctx, reader, mc),
TypeOf => type_of(m, ctx, reader, mc),
HasContext => has_context(m, ctx, reader, mc),
GetContext => get_context(m, ctx, reader, mc),
AppendContext => append_context(m, ctx, reader, mc),
AppendContextLoop => append_context_loop(m, ctx, reader, mc),
AppendContextEntryForced => append_context_entry_forced(m, ctx, reader, mc),
AppendContextOutputsForced => append_context_outputs_forced(m, ctx, reader, mc),
AppendContextOutputElementLoop => append_context_output_element_loop(m, ctx, reader, mc),
AppendContextOutputElementForced => {
append_context_output_element_forced(m, ctx, reader, mc)
}
UnsafeDiscardStringContext => unsafe_discard_string_context(m, ctx, reader, mc),
UnsafeDiscardOutputDependency => unsafe_discard_output_dependency(m, ctx, reader, mc),
phase => todo!("primop phase {phase:?}"),
}
}
+43
View File
@@ -0,0 +1,43 @@
use fix_error::Error;
use fix_runtime::{
BytecodeReader, Machine, MachineExt, Path, Step, StrictValue, Value, VmRuntimeCtx,
VmRuntimeCtxExt, canon_path_str,
};
use gc_arena::Mutation;
pub fn to_path<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// coerce to path THEN TO STRING
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
if let Some(Path(s)) = val.as_inline::<Path>() {
return m.return_from_primop(Value::new_inline(s), reader);
}
let Some(s) = ctx.get_string(val) else {
return m.finish_err(Error::eval_error(format!(
"cannot coerce {} to a path",
val.ty()
)));
};
if !s.starts_with('/') {
return m.finish_err(Error::eval_error(format!(
"string '{s}' doesn't represent an absolute path"
)));
}
let canon = canon_path_str(s);
let sid = ctx.intern_string(canon);
m.return_from_primop(Value::new_inline(sid), reader)
}
pub fn is_path<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
let is_path = val.is::<Path>();
m.return_from_primop(Value::new_inline(is_path), reader)
}
+17 -22
View File
@@ -3,6 +3,18 @@ name = "fix"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[[bench]]
harness = false
name = "basic_ops"
[[bench]]
harness = false
name = "builtins"
[[bench]]
harness = false
name = "thunk_scope"
[dependencies] [dependencies]
mimalloc = "0.1" mimalloc = "0.1"
@@ -17,41 +29,24 @@ clap = { version = "4", features = ["derive"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
miette = { version = "7.4", features = ["fancy"] }
# Error Reporting # Error Reporting
thiserror = "2" thiserror = "2"
miette = { version = "7.4", features = ["fancy"] }
# Data Structure # Data Structure
hashbrown = { workspace = true } hashbrown = { workspace = true }
string-interner = { workspace = true } string-interner = { workspace = true }
# Memory Management
bumpalo = { workspace = true }
rnix = { workspace = true }
ere = { workspace = true } ere = { workspace = true }
ghost-cell = { workspace = true }
fix-common = { path = "../fix-common" } fix-bytecode = { path = "../fix-bytecode" }
fix-codegen = { path = "../fix-codegen" } fix-compiler = { path = "../fix-compiler" }
fix-error = { path = "../fix-error" } fix-error = { path = "../fix-error" }
fix-ir = { path = "../fix-ir" } fix-lang = { path = "../fix-lang" }
fix-runtime = { path = "../fix-runtime" }
fix-vm = { path = "../fix-vm" } fix-vm = { path = "../fix-vm" }
[dev-dependencies] [dev-dependencies]
criterion = { version = "0.8", features = ["html_reports"] } criterion = { version = "0.8", features = ["html_reports"] }
tempfile = "3.24" tempfile = "3.24"
test-log = { version = "0.2", features = ["trace"] } test-log = { version = "0.2", features = ["trace"] }
[[bench]]
name = "basic_ops"
harness = false
[[bench]]
name = "builtins"
harness = false
[[bench]]
name = "thunk_scope"
harness = false
+1 -1
View File
@@ -1,8 +1,8 @@
#![allow(dead_code)] #![allow(dead_code)]
use fix::Evaluator; use fix::Evaluator;
use fix_common::Value;
use fix_error::{Result, Source}; use fix_error::{Result, Source};
use fix_lang::Value;
pub fn eval(expr: &str) -> Value { pub fn eval(expr: &str) -> Value {
Evaluator::new() Evaluator::new()
+42 -473
View File
@@ -1,40 +1,30 @@
#![warn(clippy::unwrap_used)] #![warn(clippy::unwrap_used)]
#![allow(dead_code)] #![allow(dead_code)]
use bumpalo::Bump; use fix_bytecode::InstructionPtr;
use fix_codegen::disassembler::{Disassembler, DisassemblerContext}; use fix_bytecode::disassembler::{Disassembler, DisassemblerContext};
use fix_codegen::{BytecodeContext, InstructionPtr}; use fix_compiler::{CodeState, ExtraScope};
use fix_common::{StringId, Symbol}; use fix_error::{Result, Source};
use fix_error::{Error, Result, Source}; use fix_lang::StringId;
use fix_ir::downgrade::{Downgrade as _, DowngradeContext}; use fix_runtime::{ForceMode, StaticValue, VmCode, VmContext, VmRuntimeCtx};
use fix_ir::{Ir, IrRef, MaybeThunk, RawIrRef, ThunkId}; use fix_vm::Vm;
use fix_vm::{ForceMode, StaticValue, Vm, VmContext};
use ghost_cell::{GhostCell, GhostToken};
use hashbrown::{HashMap, HashSet}; use hashbrown::{HashMap, HashSet};
use string_interner::{DefaultStringInterner, Symbol as _}; use string_interner::{DefaultStringInterner, Symbol as _};
// mod fetcher;
// mod nar;
// mod nix_utils;
// mod store;
// mod string_context;
mod derivation; mod derivation;
pub mod logging; pub mod logging;
#[global_allocator] #[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
pub struct RuntimeState {
pub strings: DefaultStringInterner,
pub constants: Constants,
}
pub struct Evaluator { pub struct Evaluator {
bytecode: Vec<u8>, pub runtime: RuntimeState,
constants: Constants, pub code: CodeState,
strings: DefaultStringInterner,
sources: Vec<Source>,
spans: Vec<(usize, rnix::TextRange)>,
// FIXME: remove?
thunk_count: usize,
global_env: HashMap<StringId, Ir<'static, RawIrRef<'static>>>,
} }
impl Default for Evaluator { impl Default for Evaluator {
@@ -46,28 +36,25 @@ impl Default for Evaluator {
impl Evaluator { impl Evaluator {
pub fn new() -> Self { pub fn new() -> Self {
let mut strings = DefaultStringInterner::new(); let mut strings = DefaultStringInterner::new();
let global_env = fix_ir::new_global_env(&mut strings); let code = CodeState::new(&mut strings);
Self { Self {
sources: Vec::new(), runtime: RuntimeState {
spans: Vec::new(),
strings, strings,
thunk_count: 0,
bytecode: Vec::new(),
constants: Constants::default(), constants: Constants::default(),
},
global_env, code,
} }
} }
pub fn eval(&mut self, source: Source) -> Result<fix_common::Value> { pub fn eval(&mut self, source: Source) -> Result<fix_lang::Value> {
self.do_eval(source, None, ForceMode::AsIs) self.do_eval(source, None, ForceMode::AsIs)
} }
pub fn eval_shallow(&mut self, source: Source) -> Result<fix_common::Value> { pub fn eval_shallow(&mut self, source: Source) -> Result<fix_lang::Value> {
self.do_eval(source, None, ForceMode::Shallow) self.do_eval(source, None, ForceMode::Shallow)
} }
pub fn eval_deep(&mut self, source: Source) -> Result<fix_common::Value> { pub fn eval_deep(&mut self, source: Source) -> Result<fix_lang::Value> {
self.do_eval(source, None, ForceMode::Deep) self.do_eval(source, None, ForceMode::Deep)
} }
@@ -75,18 +62,19 @@ impl Evaluator {
&mut self, &mut self,
source: Source, source: Source,
scope: &HashSet<StringId>, scope: &HashSet<StringId>,
) -> Result<fix_common::Value> { ) -> Result<fix_lang::Value> {
self.do_eval(source, Some(Scope::Repl(scope)), ForceMode::Shallow) self.do_eval(source, Some(ExtraScope::Repl(scope)), ForceMode::Shallow)
} }
fn do_eval<'ctx>( fn do_eval<'ctx>(
&'ctx mut self, &'ctx mut self,
source: Source, source: Source,
extra_scope: Option<Scope<'ctx>>, extra_scope: Option<ExtraScope<'ctx>>,
force_mode: ForceMode, force_mode: ForceMode,
) -> Result<fix_common::Value> { ) -> Result<fix_lang::Value> {
let root = self.downgrade(source, extra_scope)?; let ip = self
let ip = fix_codegen::compile_bytecode(root.as_ref(), self); .code
.compile_bytecode(source, extra_scope, &mut self.runtime)?;
Vm::run(self, ip, force_mode) Vm::run(self, ip, force_mode)
} }
@@ -95,78 +83,20 @@ impl Evaluator {
_ident: &str, _ident: &str,
_expr: &str, _expr: &str,
_scope: &mut HashSet<StringId>, _scope: &mut HashSet<StringId>,
) -> Result<fix_common::Value> { ) -> Result<fix_lang::Value> {
todo!() todo!("add_binding")
} }
pub fn compile_bytecode(&mut self, source: Source) -> Result<InstructionPtr> { pub fn compile_bytecode(&mut self, source: Source) -> Result<InstructionPtr> {
let root = self.downgrade(source, None)?; self.code.compile_bytecode(source, None, &mut self.runtime)
let ip = fix_codegen::compile_bytecode(root.as_ref(), self);
Ok(ip)
} }
pub fn disassemble_colored(&self, ip: InstructionPtr) -> String { pub fn disassemble_colored(&self, ip: InstructionPtr) -> String {
Disassembler::new(ip, self).disassemble_colored() Disassembler::new(ip, self).disassemble_colored()
} }
fn downgrade_ctx<'a, 'bump, 'id>(
&'a mut self,
bump: &'bump Bump,
token: GhostToken<'id>,
extra_scope: Option<Scope<'a>>,
) -> DowngradeCtx<'a, 'id, 'bump> {
let Self {
global_env,
sources,
thunk_count,
strings,
..
} = self;
DowngradeCtx {
bump,
token,
strings,
source: sources.last().expect("no current source").clone(),
scopes: [Scope::Global(global_env)]
.into_iter()
.chain(extra_scope)
.collect(),
with_scope_count: 0,
arg_count: 0,
thunk_count,
thunk_scopes: vec![ThunkScope::new_in(bump)],
}
}
fn downgrade<'a>(
&'a mut self,
source: Source,
extra_scope: Option<Scope<'a>>,
) -> Result<OwnedIr> {
tracing::debug!("Parsing Nix expression");
self.sources.push(source.clone());
let root = rnix::Root::parse(&source.src);
handle_parse_error(root.errors(), source).map_or(Ok(()), Err)?;
tracing::debug!("Downgrading Nix expression");
let expr = root
.tree()
.expr()
.ok_or_else(|| Error::parse_error("unexpected EOF".into()))?;
let bump = Bump::new();
GhostToken::new(|token| {
let ir = self
.downgrade_ctx(&bump, token, extra_scope)
.downgrade_toplevel(expr)?;
let ir = unsafe { std::mem::transmute::<RawIrRef<'_>, RawIrRef<'static>>(ir) };
Ok(OwnedIr { _bump: bump, ir })
})
}
} }
impl VmContext for &mut Evaluator { impl VmRuntimeCtx for RuntimeState {
fn intern_string(&mut self, s: impl AsRef<str>) -> StringId { fn intern_string(&mut self, s: impl AsRef<str>) -> StringId {
StringId(self.strings.get_or_intern(s)) StringId(self.strings.get_or_intern(s))
} }
@@ -174,21 +104,23 @@ impl VmContext for &mut Evaluator {
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
self.strings.resolve(id.0).unwrap() self.strings.resolve(id.0).unwrap()
} }
fn bytecode(&self) -> &[u8] {
&self.bytecode
}
fn get_const(&self, id: u32) -> StaticValue { fn get_const(&self, id: u32) -> StaticValue {
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
self.constants.get(id).unwrap() self.constants.get(id).unwrap()
} }
fn add_const(&mut self, val: StaticValue) -> u32 {
self.constants.insert(val)
}
}
fn compile(&mut self, _source: Source) { impl VmContext for Evaluator {
todo!(); fn split(&mut self) -> (&mut impl VmCode, &mut impl VmRuntimeCtx) {
(&mut self.code, &mut self.runtime)
} }
} }
#[derive(Default)] #[derive(Default)]
struct Constants { pub struct Constants {
data: Vec<StaticValue>, data: Vec<StaticValue>,
dedup: HashMap<u64, u32>, dedup: HashMap<u64, u32>,
} }
@@ -208,377 +140,14 @@ impl Constants {
} }
} }
fn parse_error_span(error: &rnix::ParseError) -> Option<rnix::TextRange> {
use rnix::ParseError::*;
match error {
Unexpected(range)
| UnexpectedExtra(range)
| UnexpectedWanted(_, range, _)
| UnexpectedDoubleBind(range)
| DuplicatedArgs(range, _) => Some(*range),
_ => None,
}
}
fn handle_parse_error<'a>(
errors: impl IntoIterator<Item = &'a rnix::ParseError>,
source: Source,
) -> Option<Box<Error>> {
for err in errors {
if let Some(span) = parse_error_span(err) {
return Some(
Error::parse_error(err.to_string())
.with_source(source)
.with_span(span),
);
}
}
None
}
struct DowngradeCtx<'ctx, 'id, 'ir> {
bump: &'ir Bump,
token: GhostToken<'id>,
strings: &'ctx mut DefaultStringInterner,
source: Source,
scopes: Vec<Scope<'ctx>>,
with_scope_count: u32,
arg_count: u32,
thunk_count: &'ctx mut usize,
thunk_scopes: Vec<ThunkScope<'id, 'ir>>,
}
fn should_thunk<'id>(ir: IrRef<'id, '_>, token: &GhostToken<'id>) -> bool {
!matches!(
ir.borrow(token),
Ir::Builtin(_)
| Ir::Builtins
| Ir::Int(_)
| Ir::Float(_)
| Ir::Bool(_)
| Ir::Null
| Ir::Str(_)
| Ir::Thunk(_)
)
}
impl<'ctx, 'id, 'ir> DowngradeCtx<'ctx, 'id, 'ir> {
fn new(
bump: &'ir Bump,
token: GhostToken<'id>,
symbols: &'ctx mut DefaultStringInterner,
global: &'ctx HashMap<StringId, Ir<'static, RawIrRef<'static>>>,
extra_scope: Option<Scope<'ctx>>,
thunk_count: &'ctx mut usize,
source: Source,
) -> Self {
Self {
bump,
token,
strings: symbols,
source,
scopes: std::iter::once(Scope::Global(global))
.chain(extra_scope)
.collect(),
thunk_count,
arg_count: 0,
with_scope_count: 0,
thunk_scopes: vec![ThunkScope::new_in(bump)],
}
}
}
impl<'ctx: 'ir, 'id, 'ir> DowngradeContext<'id, 'ir> for DowngradeCtx<'ctx, 'id, 'ir> {
fn new_expr(&self, expr: Ir<'ir, IrRef<'id, 'ir>>) -> IrRef<'id, 'ir> {
IrRef::new(self.bump.alloc(GhostCell::new(expr)))
}
fn maybe_thunk(&mut self, ir: IrRef<'id, 'ir>) -> MaybeThunk {
use MaybeThunk::*;
match *ir.borrow(&self.token) {
Ir::Builtin(x) => return Builtin(x),
Ir::Int(x) => return Int(x),
Ir::Float(x) => return Float(x),
Ir::Bool(x) => return Bool(x),
Ir::Str(x) => return Str(x),
Ir::Thunk(x) => return Thunk(x),
Ir::Arg { layer } => return Arg { layer },
Ir::Builtins => return Builtins,
Ir::Null => return Null,
_ => (),
}
let id = ThunkId(*self.thunk_count);
*self.thunk_count = self.thunk_count.checked_add(1).expect("thunk id overflow");
self.thunk_scopes
.last_mut()
.expect("no active cache scope")
.add_binding(id, ir, &self.token);
Thunk(id)
}
fn intern_string(&mut self, sym: impl AsRef<str>) -> StringId {
StringId(self.strings.get_or_intern(sym))
}
fn resolve_sym(&self, id: StringId) -> Symbol<'_> {
self.strings.resolve(id.0).expect("no symbol found").into()
}
fn lookup(&self, sym: StringId, span: rnix::TextRange) -> Result<MaybeThunk> {
for scope in self.scopes.iter().rev() {
match scope {
&Scope::Global(global_scope) => {
use MaybeThunk::*;
if let Some(expr) = global_scope.get(&sym) {
let val = match expr {
Ir::Builtins => Builtins,
Ir::Builtin(s) => Builtin(*s),
Ir::Bool(b) => Bool(*b),
Ir::Null => Null,
_ => unreachable!("globals should only contain leaf IR nodes"),
};
return Ok(val);
}
}
&Scope::Repl(repl_bindings) => {
if repl_bindings.contains(&sym) {
return Ok(MaybeThunk::ReplBinding(sym));
}
}
Scope::ScopedImport(scoped_bindings) => {
if scoped_bindings.contains(&sym) {
return Ok(MaybeThunk::ScopedImportBinding(sym));
}
}
Scope::Let(let_scope) => {
if let Some(&expr) = let_scope.get(&sym) {
return Ok(MaybeThunk::Thunk(expr));
}
}
&Scope::Param {
sym: param_sym,
abs_layer,
} => {
if param_sym == sym {
return Ok(MaybeThunk::Arg {
layer: self.thunk_scopes.len() - abs_layer,
});
}
}
}
}
if self.with_scope_count > 0 {
Ok(MaybeThunk::WithLookup(sym))
} else {
Err(Error::downgrade_error(
format!("'{}' not found", self.resolve_sym(sym)),
self.get_current_source(),
span,
))
}
}
fn get_current_source(&self) -> Source {
self.source.clone()
}
fn with_let_scope<F, R>(&mut self, keys: &[StringId], f: F) -> Result<R>
where
F: FnOnce(&mut Self) -> Result<(bumpalo::collections::Vec<'ir, IrRef<'id, 'ir>>, R)>,
{
let base = *self.thunk_count;
*self.thunk_count = self
.thunk_count
.checked_add(keys.len())
.expect("thunk id overflow");
let iter = keys
.iter()
.enumerate()
.map(|(offset, &key)| (key, ThunkId(base + offset)));
self.scopes.push(Scope::Let(iter.collect()));
let (vals, ret) = {
let mut guard = ScopeGuard { ctx: self };
f(guard.as_ctx())?
};
assert_eq!(keys.len(), vals.len());
let scope = self.thunk_scopes.last_mut().expect("no active thunk scope");
scope.extend_bindings((base..base + keys.len()).map(ThunkId).zip(vals));
Ok(ret)
}
fn with_param_scope<F, R>(&mut self, sym: StringId, f: F) -> R
where
F: FnOnce(&mut Self) -> R,
{
self.scopes.push(Scope::Param {
sym,
abs_layer: self.thunk_scopes.len(),
});
let mut guard = ScopeGuard { ctx: self };
f(guard.as_ctx())
}
fn with_with_scope<F, R>(&mut self, f: F) -> R
where
F: FnOnce(&mut Self) -> R,
{
self.with_scope_count += 1;
let ret = f(self);
self.with_scope_count -= 1;
ret
}
fn with_thunk_scope<F, R>(
&mut self,
f: F,
) -> (
R,
bumpalo::collections::Vec<'ir, (ThunkId, IrRef<'id, 'ir>)>,
)
where
F: FnOnce(&mut Self) -> R,
{
self.thunk_scopes.push(ThunkScope::new_in(self.bump));
let ret = f(self);
(
ret,
self.thunk_scopes
.pop()
.expect("no thunk scope left???")
.bindings,
)
}
fn bump(&self) -> &'ir bumpalo::Bump {
self.bump
}
}
impl<'id, 'ir, 'ctx: 'ir> DowngradeCtx<'ctx, 'id, 'ir> {
fn downgrade_toplevel(mut self, root: rnix::ast::Expr) -> Result<RawIrRef<'ir>> {
let body = root.downgrade(&mut self)?;
let thunks = self
.thunk_scopes
.pop()
.expect("no thunk scope left???")
.bindings;
let ir = IrRef::alloc(self.bump, Ir::TopLevel { body, thunks });
Ok(ir.freeze(self.token))
}
}
struct ThunkScope<'id, 'ir> {
bindings: bumpalo::collections::Vec<'ir, (ThunkId, IrRef<'id, 'ir>)>,
}
impl<'id, 'ir> ThunkScope<'id, 'ir> {
fn new_in(bump: &'ir Bump) -> Self {
Self {
bindings: bumpalo::collections::Vec::new_in(bump),
}
}
fn add_binding(&mut self, id: ThunkId, ir: IrRef<'id, 'ir>, _token: &GhostToken<'id>) {
self.bindings.push((id, ir));
}
fn extend_bindings(&mut self, iter: impl IntoIterator<Item = (ThunkId, IrRef<'id, 'ir>)>) {
self.bindings.extend(iter);
}
}
enum Scope<'ctx> {
Global(&'ctx HashMap<StringId, Ir<'static, RawIrRef<'static>>>),
Repl(&'ctx HashSet<StringId>),
ScopedImport(HashSet<StringId>),
Let(HashMap<StringId, ThunkId>),
Param { sym: StringId, abs_layer: usize },
}
struct ScopeGuard<'a, 'ctx, 'id, 'ir> {
ctx: &'a mut DowngradeCtx<'ctx, 'id, 'ir>,
}
impl Drop for ScopeGuard<'_, '_, '_, '_> {
fn drop(&mut self) {
self.ctx.scopes.pop();
}
}
impl<'id, 'ir, 'ctx> ScopeGuard<'_, 'ctx, 'id, 'ir> {
fn as_ctx(&mut self) -> &mut DowngradeCtx<'ctx, 'id, 'ir> {
self.ctx
}
}
struct OwnedIr {
_bump: Bump,
ir: RawIrRef<'static>,
}
impl OwnedIr {
/// # Safety
///
/// `ir` must be allocated from `bump`.
unsafe fn new(ir: RawIrRef<'_>, bump: Bump) -> Self {
Self {
_bump: bump,
ir: unsafe { std::mem::transmute::<RawIrRef<'_>, RawIrRef<'static>>(ir) },
}
}
fn as_ref(&self) -> RawIrRef<'_> {
self.ir
}
}
impl BytecodeContext for Evaluator {
fn intern_string(&mut self, s: &str) -> StringId {
StringId(self.strings.get_or_intern(s))
}
fn register_span(&mut self, range: rnix::TextRange) -> u32 {
let id = self.spans.len();
let source_id = self
.sources
.len()
.checked_sub(1)
.expect("current_source not set");
self.spans.push((source_id, range));
id as u32
}
fn get_code(&self) -> &[u8] {
&self.bytecode
}
fn get_code_mut(&mut self) -> &mut Vec<u8> {
&mut self.bytecode
}
fn add_constant(&mut self, val: fix_codegen::Const) -> u32 {
use fix_codegen::Const::*;
let val = match val {
Smi(x) => StaticValue::new_inline(x),
Float(x) => StaticValue::new_float(x),
Bool(x) => StaticValue::new_inline(x),
String(x) => StaticValue::new_inline(x),
PrimOp { id, arity } => StaticValue::new_primop(id, arity),
Null => StaticValue::default(),
};
self.constants.insert(val)
}
}
impl DisassemblerContext for Evaluator { impl DisassemblerContext for Evaluator {
fn get_code(&self) -> &[u8] { fn get_code(&self) -> &[u8] {
&self.bytecode &self.code.bytecode
} }
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
fn resolve_string(&self, id: u32) -> &str { fn resolve_string(&self, id: u32) -> &str {
let id = string_interner::symbol::SymbolU32::try_from_usize(id as usize).unwrap(); let id = string_interner::symbol::SymbolU32::try_from_usize(id as usize).unwrap();
self.strings.resolve(id).unwrap() self.runtime.strings.resolve(id).unwrap()
} }
} }
-209
View File
@@ -1,209 +0,0 @@
use std::collections::{BTreeMap, BTreeSet, VecDeque};
pub enum StringContextElem {
Opaque { path: String },
DrvDeep { drv_path: String },
Built { drv_path: String, output: String },
}
impl StringContextElem {
pub fn decode(encoded: &str) -> Self {
if let Some(drv_path) = encoded.strip_prefix('=') {
StringContextElem::DrvDeep {
drv_path: drv_path.to_string(),
}
} else if let Some(rest) = encoded.strip_prefix('!') {
if let Some(second_bang) = rest.find('!') {
let output = rest[..second_bang].to_string();
let drv_path = rest[second_bang + 1..].to_string();
StringContextElem::Built { drv_path, output }
} else {
StringContextElem::Opaque {
path: encoded.to_string(),
}
}
} else {
StringContextElem::Opaque {
path: encoded.to_string(),
}
}
}
}
pub type InputDrvs = BTreeMap<String, BTreeSet<String>>;
pub type Srcs = BTreeSet<String>;
pub fn extract_input_drvs_and_srcs(context: &[String]) -> Result<(InputDrvs, Srcs), String> {
let mut input_drvs: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
let mut input_srcs: BTreeSet<String> = BTreeSet::new();
for encoded in context {
match StringContextElem::decode(encoded) {
StringContextElem::Opaque { path } => {
input_srcs.insert(path);
}
StringContextElem::DrvDeep { drv_path } => {
compute_fs_closure(&drv_path, &mut input_drvs, &mut input_srcs)?;
}
StringContextElem::Built { drv_path, output } => {
input_drvs.entry(drv_path).or_default().insert(output);
}
}
}
Ok((input_drvs, input_srcs))
}
fn compute_fs_closure(
drv_path: &str,
input_drvs: &mut BTreeMap<String, BTreeSet<String>>,
input_srcs: &mut BTreeSet<String>,
) -> Result<(), String> {
let mut queue: VecDeque<String> = VecDeque::new();
let mut visited: BTreeSet<String> = BTreeSet::new();
queue.push_back(drv_path.to_string());
while let Some(current_path) = queue.pop_front() {
if visited.contains(&current_path) {
continue;
}
visited.insert(current_path.clone());
input_srcs.insert(current_path.clone());
if !current_path.ends_with(".drv") {
continue;
}
let content = std::fs::read_to_string(&current_path)
.map_err(|e| format!("failed to read derivation {}: {}", current_path, e))?;
let inputs = parse_derivation_inputs(&content)
.ok_or_else(|| format!("failed to parse derivation {}", current_path))?;
for src in inputs.input_srcs {
input_srcs.insert(src.clone());
if !visited.contains(&src) {
queue.push_back(src);
}
}
for (dep_drv, outputs) in inputs.input_drvs {
input_srcs.insert(dep_drv.clone());
let entry = input_drvs.entry(dep_drv.clone()).or_default();
for output in outputs {
entry.insert(output);
}
if !visited.contains(&dep_drv) {
queue.push_back(dep_drv);
}
}
}
Ok(())
}
struct DerivationInputs {
input_drvs: Vec<(String, Vec<String>)>,
input_srcs: Vec<String>,
}
fn parse_derivation_inputs(aterm: &str) -> Option<DerivationInputs> {
let aterm = aterm.strip_prefix("Derive([")?;
let mut bracket_count: i32 = 1;
let mut pos = 0;
let bytes = aterm.as_bytes();
while pos < bytes.len() && bracket_count > 0 {
match bytes[pos] {
b'[' => bracket_count += 1,
b']' => bracket_count -= 1,
_ => {}
}
pos += 1;
}
if bracket_count != 0 {
return None;
}
let rest = &aterm[pos..];
let rest = rest.strip_prefix(",[")?;
let mut input_drvs = Vec::new();
let mut bracket_count: i32 = 1;
let mut start = 0;
pos = 0;
let bytes = rest.as_bytes();
while pos < bytes.len() && bracket_count > 0 {
match bytes[pos] {
b'[' => bracket_count += 1,
b']' => bracket_count -= 1,
b'(' if bracket_count == 1 => {
start = pos;
}
b')' if bracket_count == 1 => {
let entry = &rest[start + 1..pos];
if let Some((drv_path, outputs)) = parse_input_drv_entry(entry) {
input_drvs.push((drv_path, outputs));
}
}
_ => {}
}
pos += 1;
}
let rest = &rest[pos..];
let rest = rest.strip_prefix(",[")?;
let mut input_srcs = Vec::new();
bracket_count = 1;
pos = 0;
let bytes = rest.as_bytes();
while pos < bytes.len() && bracket_count > 0 {
match bytes[pos] {
b'[' => bracket_count += 1,
b']' => bracket_count -= 1,
b'"' if bracket_count == 1 => {
pos += 1;
let src_start = pos;
while pos < bytes.len() && bytes[pos] != b'"' {
if bytes[pos] == b'\\' && pos + 1 < bytes.len() {
pos += 2;
} else {
pos += 1;
}
}
let src = std::str::from_utf8(&bytes[src_start..pos]).ok()?;
input_srcs.push(src.to_string());
}
_ => {}
}
pos += 1;
}
Some(DerivationInputs {
input_drvs,
input_srcs,
})
}
fn parse_input_drv_entry(entry: &str) -> Option<(String, Vec<String>)> {
let entry = entry.strip_prefix('"')?;
let quote_end = entry.find('"')?;
let drv_path = entry[..quote_end].to_string();
let rest = &entry[quote_end + 1..];
let rest = rest.strip_prefix(",[")?;
let rest = rest.strip_suffix(']')?;
let mut outputs = Vec::new();
for part in rest.split(',') {
let part = part.trim();
if let Some(name) = part.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
outputs.push(name.to_string());
}
}
Some((drv_path, outputs))
}
+1 -1
View File
@@ -1,4 +1,4 @@
use fix_common::Value; use fix_lang::Value;
use crate::utils::{eval_deep, eval_deep_result}; use crate::utils::{eval_deep, eval_deep_result};
+1 -1
View File
@@ -1,6 +1,6 @@
use fix::Evaluator; use fix::Evaluator;
use fix_common::Value;
use fix_error::Source; use fix_error::Source;
use fix_lang::Value;
use crate::utils::{eval, eval_result}; use crate::utils::{eval, eval_result};
+1 -1
View File
@@ -3,8 +3,8 @@
use std::path::PathBuf; use std::path::PathBuf;
use fix::Evaluator; use fix::Evaluator;
use fix_common::Value;
use fix_error::{Source, SourceType}; use fix_error::{Source, SourceType};
use fix_lang::Value;
fn get_lang_dir() -> PathBuf { fn get_lang_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/tests/lang") PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/tests/lang")
+1 -1
View File
@@ -1,5 +1,5 @@
use fix::Evaluator; use fix::Evaluator;
use fix_common::Value; use fix_lang::Value;
use crate::utils::eval_result; use crate::utils::eval_result;
+1 -1
View File
@@ -1,8 +1,8 @@
#![allow(dead_code)] #![allow(dead_code)]
use fix::Evaluator; use fix::Evaluator;
use fix_common::Value;
use fix_error::{Result, Source}; use fix_error::{Result, Source};
use fix_lang::Value;
pub fn eval(expr: &str) -> Value { pub fn eval(expr: &str) -> Value {
Evaluator::new() Evaluator::new()
Generated
+27 -44
View File
@@ -31,7 +31,6 @@
"llm-agents", "llm-agents",
"flake-parts" "flake-parts"
], ],
"import-tree": "import-tree",
"nixpkgs": [ "nixpkgs": [
"llm-agents", "llm-agents",
"nixpkgs" "nixpkgs"
@@ -46,16 +45,15 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1776182890, "lastModified": 1778446047,
"narHash": "sha256-+/VOe8XGq5klpU+I19D+3TcaR7o+Cwbq67KNF7mcFak=", "narHash": "sha256-oQvcadh2BCkrog+SGrG6YffKJrveYpjj3TdQJWaKhaM=",
"owner": "Mic92", "owner": "nix-community",
"repo": "bun2nix", "repo": "bun2nix",
"rev": "648d293c51e981aec9cb07ba4268bc19e7a8c575", "rev": "f2bc12af1a6369648aac41041ceeaa0b866599c6",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "Mic92", "owner": "nix-community",
"ref": "catalog-support",
"repo": "bun2nix", "repo": "bun2nix",
"type": "github" "type": "github"
} }
@@ -68,11 +66,11 @@
"rust-analyzer-src": "rust-analyzer-src" "rust-analyzer-src": "rust-analyzer-src"
}, },
"locked": { "locked": {
"lastModified": 1776413252, "lastModified": 1781343250,
"narHash": "sha256-ZQhyB2vnFsE1KcWJlWle1UujEDVjTJVL3oMIHUvnzuo=", "narHash": "sha256-KBJktAwDG9+10j2wMfvOVkBEhZr3yS769xoqqdFI62s=",
"owner": "nix-community", "owner": "nix-community",
"repo": "fenix", "repo": "fenix",
"rev": "a318c3c6120e91375eea1d7c57a0cd101a81b14a", "rev": "aad7d8bb6936d473c2b9d1a5846a1fe1bc92767a",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -84,11 +82,11 @@
"flake-compat": { "flake-compat": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1751685974, "lastModified": 1777699697,
"narHash": "sha256-NKw96t+BgHIYzHUjkTK95FqYRVKB8DHpVhefWSz/kTw=", "narHash": "sha256-Eg9b/rq/ECYwNwEXs5i9wHyhxNI0JrYx2srdI2uZMaQ=",
"rev": "549f2762aebeff29a2e5ece7a7dc0f955281a1d1", "rev": "382052b74656a369c5408822af3f2501e9b1af81",
"type": "tarball", "type": "tarball",
"url": "https://git.lix.systems/api/v1/repos/lix-project/flake-compat/archive/549f2762aebeff29a2e5ece7a7dc0f955281a1d1.tar.gz" "url": "https://git.lix.systems/api/v1/repos/lix-project/flake-compat/archive/382052b74656a369c5408822af3f2501e9b1af81.tar.gz"
}, },
"original": { "original": {
"type": "tarball", "type": "tarball",
@@ -103,11 +101,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1775087534, "lastModified": 1778716662,
"narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=", "narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "flake-parts", "repo": "flake-parts",
"rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b", "rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -116,21 +114,6 @@
"type": "github" "type": "github"
} }
}, },
"import-tree": {
"locked": {
"lastModified": 1763762820,
"narHash": "sha256-ZvYKbFib3AEwiNMLsejb/CWs/OL/srFQ8AogkebEPF0=",
"owner": "vic",
"repo": "import-tree",
"rev": "3c23749d8013ec6daa1d7255057590e9ca726646",
"type": "github"
},
"original": {
"owner": "vic",
"repo": "import-tree",
"type": "github"
}
},
"llm-agents": { "llm-agents": {
"inputs": { "inputs": {
"blueprint": "blueprint", "blueprint": "blueprint",
@@ -143,11 +126,11 @@
"treefmt-nix": "treefmt-nix" "treefmt-nix": "treefmt-nix"
}, },
"locked": { "locked": {
"lastModified": 1776437995, "lastModified": 1781330261,
"narHash": "sha256-wcV5CIe5s2IsSCGJdPqy/Q+gcBSR76JMaIQDNpLXZAk=", "narHash": "sha256-2fFAGel2VVXr5mwrTXldqXva2ng3T3HHxyuBKRIxauI=",
"owner": "numtide", "owner": "numtide",
"repo": "llm-agents.nix", "repo": "llm-agents.nix",
"rev": "c4a2f76e29485eaafc90eebec5ef12b50f4dc8a1", "rev": "24ec6b7b1ddf8896ac8df3b65dc564575e0a1928",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -158,11 +141,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1776169885, "lastModified": 1781074563,
"narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=", "narHash": "sha256-md8WlXOlfnIeHeOScMTTHFyf2d6iaTwPl2apR5EQ3P4=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9", "rev": "9ae611a455b90cf061d8f332b977e387bda8e1ca",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -183,11 +166,11 @@
"rust-analyzer-src": { "rust-analyzer-src": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1776343166, "lastModified": 1781294997,
"narHash": "sha256-ZiHQPWwuUZk44epAZRbyFz23Kd4CYaq8WlBgAmCqAzQ=", "narHash": "sha256-XjCyIvJw4JtcwItTKRdQz5h1pLF9hr8ZSYeMP+/1d3A=",
"owner": "rust-lang", "owner": "rust-lang",
"repo": "rust-analyzer", "repo": "rust-analyzer",
"rev": "b8458013c217be4fccefc4e4f194026fa04ab4ca", "rev": "3f92cd1612268995d5667bd04fa03ba2916413d9",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -220,11 +203,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1775636079, "lastModified": 1780220602,
"narHash": "sha256-pc20NRoMdiar8oPQceQT47UUZMBTiMdUuWrYu2obUP0=", "narHash": "sha256-eynAfOmbmxJnkp7YewvCEbShNnnYJ9gLLqkzsYtBPeM=",
"owner": "numtide", "owner": "numtide",
"repo": "treefmt-nix", "repo": "treefmt-nix",
"rev": "790751ff7fd3801feeaf96d7dc416a8d581265ba", "rev": "db947814a175b7ca6ded66e21383d938df01c227",
"type": "github" "type": "github"
}, },
"original": { "original": {
+3
View File
@@ -47,9 +47,12 @@
just just
samply samply
tokei tokei
tombi
# llm-agents.codex
llm-agents.claude-code llm-agents.claude-code
llm-agents.opencode llm-agents.opencode
# llm-agents.forge
]; ];
}; };
} }