implement Path type

This commit is contained in:
2026-05-08 19:01:13 +08:00
parent 3d07f89afe
commit 21899f7380
8 changed files with 209 additions and 26 deletions
+30 -1
View File
@@ -2,6 +2,7 @@ use std::cmp::Ordering;
use gc_arena::{Gc, Mutation, RefLock}; use gc_arena::{Gc, Mutation, RefLock};
use crate::instructions::misc::canon_path_str;
use crate::value::*; use crate::value::*;
use crate::{BytecodeReader, NixNum, Step, VmError, VmRuntimeCtx, VmRuntimeCtxExt as _}; use crate::{BytecodeReader, NixNum, Step, VmError, VmRuntimeCtx, VmRuntimeCtxExt as _};
@@ -14,7 +15,25 @@ impl<'gc> crate::Vm<'gc> {
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?; let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
if let (Some(ls), Some(rs)) = (ctx.get_string(lhs), ctx.get_string(rhs)) { // CppNix: if the LHS is a path, the result is a path obtained by
// canonicalizing the concatenated string. RHS may be a path or a
// string. (A `string + path` keeps the string-typed result, handled
// by the next branch.)
if lhs.is::<Path>() {
let (Some(ls), Some(rs)) = (ctx.get_string_or_path(lhs), ctx.get_string_or_path(rhs))
else {
return self.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);
self.push(Value::new_inline(crate::value::Path(sid)));
return Step::Continue(());
}
if let (Some(ls), Some(rs)) = (ctx.get_string(lhs), ctx.get_string_or_path(rhs)) {
let ns = Gc::new(mc, crate::NixString::new(format!("{ls}{rs}"))); let ns = Gc::new(mc, crate::NixString::new(format!("{ls}{rs}")));
self.push(Value::new_gc(ns)); self.push(Value::new_gc(ns));
return Step::Continue(()); return Step::Continue(());
@@ -232,6 +251,10 @@ impl<'gc> crate::Vm<'gc> {
if lhs.is::<crate::Null>() && rhs.is::<crate::Null>() { if lhs.is::<crate::Null>() && rhs.is::<crate::Null>() {
return Ok(true); return Ok(true);
} }
// Paths only equal paths (not strings, even if their text matches).
if let (Some(a), Some(b)) = (lhs.as_inline::<Path>(), rhs.as_inline::<Path>()) {
return Ok(a.0 == b.0);
}
if let (Some(a), Some(b)) = (ctx.get_string(lhs), ctx.get_string(rhs)) { if let (Some(a), Some(b)) = (ctx.get_string(lhs), ctx.get_string(rhs)) {
return Ok(a == b); return Ok(a == b);
} }
@@ -294,6 +317,12 @@ impl<'gc> crate::Vm<'gc> {
self.push(Value::new_inline(pred(a.cmp(b)))); self.push(Value::new_inline(pred(a.cmp(b))));
return Ok(()); return Ok(());
} }
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);
self.push(Value::new_inline(pred(a.cmp(b))));
return Ok(());
}
// TODO: compare other types // TODO: compare other types
Err(crate::vm_err("cannot compare these types")) Err(crate::vm_err("cannot compare these types"))
} }
+19 -12
View File
@@ -5,7 +5,7 @@ use fix_common::StringId;
use fix_error::Error; use fix_error::Error;
use num_enum::TryFromPrimitive; use num_enum::TryFromPrimitive;
use crate::value::{AttrSet, NixString, StrictValue}; use crate::value::{AttrSet, NixString, Path, StrictValue};
use crate::{BytecodeReader, PrimOp, Step, Value, VmRuntimeCtx, VmRuntimeCtxExt}; use crate::{BytecodeReader, PrimOp, Step, Value, VmRuntimeCtx, VmRuntimeCtxExt};
impl<'gc> crate::Vm<'gc> { impl<'gc> crate::Vm<'gc> {
@@ -74,6 +74,10 @@ impl<'gc> crate::Vm<'gc> {
let val = self.force_and_retry::<StrictValue>(reader, mc)?; let val = self.force_and_retry::<StrictValue>(reader, mc)?;
if val.is::<StringId>() || val.is::<NixString>() { if val.is::<StringId>() || val.is::<NixString>() {
self.push(val.relax()); self.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
self.push(Value::new_inline(p.0));
} else { } else {
todo!("coerce other types to string: {:?}", val.ty()); todo!("coerce other types to string: {:?}", val.ty());
} }
@@ -120,6 +124,11 @@ impl<'gc> crate::Vm<'gc> {
) -> Step { ) -> Step {
let path_val = self.force_and_retry::<StrictValue>(reader, mc)?; let path_val = self.force_and_retry::<StrictValue>(reader, mc)?;
let dir_id = reader.read_string_id(); 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>() {
self.push(Value::new_inline(p));
return Step::Continue(());
}
let path = match ctx.get_string(path_val) { let path = match ctx.get_string(path_val) {
Some(s) => s.to_owned(), Some(s) => s.to_owned(),
None => { None => {
@@ -134,22 +143,15 @@ impl<'gc> crate::Vm<'gc> {
Err(e) => return self.finish_err(e), Err(e) => return self.finish_err(e),
}; };
let sid = ctx.intern_string(resolved); let sid = ctx.intern_string(resolved);
self.push(Value::new_inline(sid)); self.push(Value::new_inline(Path(sid)));
Step::Continue(()) Step::Continue(())
} }
} }
/// Resolve a Nix path literal against `current_dir`.
///
/// Mirrors nix-js's `op_resolve_path`: absolute paths returned as-is, `~/X`
/// expanded against `$HOME`, otherwise joined onto `current_dir`. The result
/// is normalized by removing `.` components and resolving `..` lexically
/// (no symlink resolution).
fn resolve_path_str(current_dir: &str, path: &str) -> Result<String, Box<Error>> { fn resolve_path_str(current_dir: &str, path: &str) -> Result<String, Box<Error>> {
let raw = if path.starts_with('/') { let raw = if path.starts_with('/') {
return Ok(path.to_owned()); return Ok(canon_path_str(path));
} else if let Some(rest) = path.strip_prefix("~/") { } else if let Some(rest) = path.strip_prefix("~/") {
#[allow(deprecated)]
let mut dir = let mut dir =
std::env::home_dir().ok_or_else(|| Error::eval_error("home dir not defined"))?; std::env::home_dir().ok_or_else(|| Error::eval_error("home dir not defined"))?;
dir.push(rest); dir.push(rest);
@@ -159,8 +161,13 @@ fn resolve_path_str(current_dir: &str, path: &str) -> Result<String, Box<Error>>
dir.push(path); dir.push(path);
dir dir
}; };
Ok(canon_path_str(&raw))
}
pub(crate) fn canon_path_str(path: impl AsRef<std::path::Path>) -> String {
let p = path.as_ref();
let mut normalized = PathBuf::new(); let mut normalized = PathBuf::new();
for component in raw.components() { for component in p.components() {
match component { match component {
Component::Prefix(p) => normalized.push(p.as_os_str()), Component::Prefix(p) => normalized.push(p.as_os_str()),
Component::RootDir => normalized.push("/"), Component::RootDir => normalized.push("/"),
@@ -171,5 +178,5 @@ fn resolve_path_str(current_dir: &str, path: &str) -> Result<String, Box<Error>>
Component::Normal(c) => normalized.push(c), Component::Normal(c) => normalized.push(c),
} }
} }
Ok(normalized.to_string_lossy().into_owned()) normalized.to_string_lossy().into_owned()
} }
+15
View File
@@ -98,6 +98,7 @@ pub enum ExtraScope {
trait VmRuntimeCtxExt: VmRuntimeCtx { trait VmRuntimeCtxExt: VmRuntimeCtx {
fn get_string<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str>; 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>( fn get_string_id<'a, 'gc: 'a>(
&'a mut self, &'a mut self,
val: StrictValue<'gc>, val: StrictValue<'gc>,
@@ -114,6 +115,18 @@ impl<T: VmRuntimeCtx> VmRuntimeCtxExt for T {
} }
} }
/// 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>( fn get_string_id<'a, 'gc: 'a>(
&'a mut self, &'a mut self,
val: StrictValue<'gc>, val: StrictValue<'gc>,
@@ -154,6 +167,8 @@ impl<T: VmRuntimeCtx> ConvertValueWithSeen for T {
Value::String(s) Value::String(s)
} else if let Some(ns) = val.as_gc::<NixString>() { } else if let Some(ns) = val.as_gc::<NixString>() {
Value::String(ns.as_str().to_owned()) 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>() { } else if let Some(attrs) = val.as_gc::<AttrSet>() {
let bits = val.to_bits(); let bits = val.to_bits();
if attrs.entries.is_empty() { if attrs.entries.is_empty() {
+52
View File
@@ -1 +1,53 @@
use fix_common::StringId;
use fix_error::Error;
use gc_arena::Mutation;
use crate::bytecode_reader::BytecodeReader;
use crate::value::*;
use crate::{Step, Vm, VmRuntimeCtx};
impl<'gc> Vm<'gc> {
pub(crate) fn primop_to_string(
&mut self,
_ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = self.force_and_retry::<StrictValue>(reader, mc)?;
if val.is::<StringId>() || val.is::<NixString>() {
return self.return_from_primop(val.relax(), reader);
}
if let Some(p) = val.as_inline::<Path>() {
return self.return_from_primop(Value::new_inline(p.0), reader);
}
// TODO: derivations / `__toString` / `outPath`,
// numbers, lists.
self.finish_err(Error::eval_error(format!(
"cannot coerce {} to a string",
val.ty()
)))
}
pub(crate) fn primop_type_of(
&mut self,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = self.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);
self.return_from_primop(Value::new_inline(sid), reader)
}
}
+25 -13
View File
@@ -7,6 +7,7 @@ use gc_arena::{Gc, Mutation};
use hashbrown::HashSet; use hashbrown::HashSet;
use crate::bytecode_reader::BytecodeReader; use crate::bytecode_reader::BytecodeReader;
use crate::instructions::misc::canon_path_str;
use crate::value::*; use crate::value::*;
use crate::{Break, CallFrame, PendingLoad, PendingScope, Step, Vm, VmRuntimeCtx, VmRuntimeCtxExt}; use crate::{Break, CallFrame, PendingLoad, PendingScope, Step, Vm, VmRuntimeCtx, VmRuntimeCtxExt};
@@ -19,11 +20,11 @@ impl<'gc> Vm<'gc> {
) -> Step { ) -> Step {
// stack: [path] // stack: [path]
let path_val = self.force_and_retry::<StrictValue>(reader, mc)?; let path_val = self.force_and_retry::<StrictValue>(reader, mc)?;
let path_str = match ctx.get_string(path_val) { let path_str = match ctx.get_string_or_path(path_val) {
Some(s) => s.to_owned(), Some(s) => s.to_owned(),
None => { None => {
return self.finish_err(Error::eval_error(format!( return self.finish_err(Error::eval_error(format!(
"expected a path string, got {}", "expected a path or string, got {}",
path_val.ty() path_val.ty()
))); )));
} }
@@ -93,11 +94,11 @@ impl<'gc> Vm<'gc> {
// stack: [scope, path] // stack: [scope, path]
let (scope_attrs, path_val) = let (scope_attrs, path_val) =
self.force_and_retry::<(Gc<AttrSet>, StrictValue)>(reader, mc)?; self.force_and_retry::<(Gc<AttrSet>, StrictValue)>(reader, mc)?;
let path_str = match ctx.get_string(path_val) { let path_str = match ctx.get_string_or_path(path_val) {
Some(s) => s.to_owned(), Some(s) => s.to_owned(),
None => { None => {
return self.finish_err(Error::eval_error(format!( return self.finish_err(Error::eval_error(format!(
"expected a path string, got {}", "expected a path or string, got {}",
path_val.ty() path_val.ty()
))); )));
} }
@@ -145,17 +146,28 @@ impl<'gc> Vm<'gc> {
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let path_val = self.force_and_retry::<StrictValue>(reader, mc)?; let path_val = self.force_and_retry::<StrictValue>(reader, mc)?;
let path = match ctx.get_string(path_val) { // CppNix: pathExists requires an absolute path. A `Path` value is
Some(s) => s.to_owned(), // always absolute; a string is accepted only if it starts with `/`.
None => { let (path, is_path_value) = if let Some(p) = path_val.as_inline::<Path>() {
return self.finish_err(Error::eval_error(format!( (ctx.resolve_string(p.0).to_owned(), true)
"expected a path string, got {}", } else if let Some(s) = ctx.get_string(path_val) {
path_val.ty() (s.to_owned(), false)
))); } else {
} return self.finish_err(Error::eval_error(format!(
"expected a path or string, got {}",
path_val.ty()
)));
}; };
if !is_path_value && !path.starts_with('/') {
return self.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 must_be_dir = path.ends_with('/') || path.ends_with("/.");
let p = std::path::Path::new(&path); let canon = canon_path_str(&path);
let p = std::path::Path::new(&canon);
let exists = if must_be_dir { let exists = if must_be_dir {
std::fs::metadata(p).map(|m| m.is_dir()).unwrap_or(false) std::fs::metadata(p).map(|m| m.is_dir()).unwrap_or(false)
} else { } else {
+4
View File
@@ -62,6 +62,10 @@ impl<'gc> Vm<'gc> {
ScopedImportFinalize => self.primop_scoped_import_finalize(ctx, reader, mc), ScopedImportFinalize => self.primop_scoped_import_finalize(ctx, reader, mc),
PathExists => self.primop_path_exists(ctx, reader, mc), PathExists => self.primop_path_exists(ctx, reader, mc),
ToPath => self.primop_to_path(ctx, reader, mc),
IsPath => self.primop_is_path(reader, mc),
ToString => self.primop_to_string(ctx, reader, mc),
TypeOf => self.primop_type_of(ctx, reader, mc),
phase => todo!("primop phase {phase:?}"), phase => todo!("primop phase {phase:?}"),
} }
+45
View File
@@ -1 +1,46 @@
use fix_error::Error;
use gc_arena::Mutation;
use crate::bytecode_reader::BytecodeReader;
use crate::instructions::misc::canon_path_str;
use crate::value::*;
use crate::{Step, Vm, VmRuntimeCtx, VmRuntimeCtxExt};
impl<'gc> Vm<'gc> {
pub(crate) fn primop_to_path(
&mut self,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// coerce to path THEN TO STRING
let val = self.force_and_retry::<StrictValue>(reader, mc)?;
if let Some(Path(s)) = val.as_inline::<Path>() {
return self.return_from_primop(Value::new_inline(s), reader);
}
let Some(s) = ctx.get_string(val) else {
return self.finish_err(Error::eval_error(format!(
"cannot coerce {} to a path",
val.ty()
)));
};
if !s.starts_with('/') {
return self.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);
self.return_from_primop(Value::new_inline(sid), reader)
}
pub(crate) fn primop_is_path(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = self.force_and_retry::<StrictValue>(reader, mc)?;
let is_path = val.is::<Path>();
self.return_from_primop(Value::new_inline(is_path), reader)
}
}
+19
View File
@@ -117,6 +117,7 @@ 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::N6, "Path";
} }
gc { gc {
i64 => RawTag::P6, "BigInt"; i64 => RawTag::P6, "BigInt";
@@ -289,6 +290,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>() {
@@ -404,6 +407,20 @@ impl RawStore for StringId {
} }
} }
/// A canonicalized absolute path. Inline value carrying an interned
/// `StringId` whose contents are the path's absolute, dot-resolved form.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct Path(pub StringId);
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))
}
}
/// Heap-allocated Nix string. /// Heap-allocated Nix string.
/// ///
/// Stored on the GC heap via `Gc<'gc, NixString>`. The string data itself /// Stored on the GC heap via `Gc<'gc, NixString>`. The string data itself
@@ -659,6 +676,7 @@ pub(crate) enum NixType {
Bool, Bool,
Null, Null,
String, String,
Path,
AttrSet, AttrSet,
List, List,
Thunk, Thunk,
@@ -676,6 +694,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",