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 crate::instructions::misc::canon_path_str;
use crate::value::*;
use crate::{BytecodeReader, NixNum, Step, VmError, VmRuntimeCtx, VmRuntimeCtxExt as _};
@@ -14,7 +15,25 @@ impl<'gc> crate::Vm<'gc> {
mc: &Mutation<'gc>,
) -> Step {
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}")));
self.push(Value::new_gc(ns));
return Step::Continue(());
@@ -232,6 +251,10 @@ impl<'gc> crate::Vm<'gc> {
if lhs.is::<crate::Null>() && rhs.is::<crate::Null>() {
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)) {
return Ok(a == b);
}
@@ -294,6 +317,12 @@ impl<'gc> crate::Vm<'gc> {
self.push(Value::new_inline(pred(a.cmp(b))));
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
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 num_enum::TryFromPrimitive;
use crate::value::{AttrSet, NixString, StrictValue};
use crate::value::{AttrSet, NixString, Path, StrictValue};
use crate::{BytecodeReader, PrimOp, Step, Value, VmRuntimeCtx, VmRuntimeCtxExt};
impl<'gc> crate::Vm<'gc> {
@@ -74,6 +74,10 @@ impl<'gc> crate::Vm<'gc> {
let val = self.force_and_retry::<StrictValue>(reader, mc)?;
if val.is::<StringId>() || val.is::<NixString>() {
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 {
todo!("coerce other types to string: {:?}", val.ty());
}
@@ -120,6 +124,11 @@ impl<'gc> crate::Vm<'gc> {
) -> Step {
let path_val = self.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>() {
self.push(Value::new_inline(p));
return Step::Continue(());
}
let path = match ctx.get_string(path_val) {
Some(s) => s.to_owned(),
None => {
@@ -134,22 +143,15 @@ impl<'gc> crate::Vm<'gc> {
Err(e) => return self.finish_err(e),
};
let sid = ctx.intern_string(resolved);
self.push(Value::new_inline(sid));
self.push(Value::new_inline(Path(sid)));
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>> {
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("~/") {
#[allow(deprecated)]
let mut dir =
std::env::home_dir().ok_or_else(|| Error::eval_error("home dir not defined"))?;
dir.push(rest);
@@ -159,8 +161,13 @@ fn resolve_path_str(current_dir: &str, path: &str) -> Result<String, Box<Error>>
dir.push(path);
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();
for component in raw.components() {
for component in p.components() {
match component {
Component::Prefix(p) => normalized.push(p.as_os_str()),
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),
}
}
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 {
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>,
@@ -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>(
&'a mut self,
val: StrictValue<'gc>,
@@ -154,6 +167,8 @@ impl<T: VmRuntimeCtx> ConvertValueWithSeen for T {
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() {
+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)
}
}
+22 -10
View File
@@ -7,6 +7,7 @@ use gc_arena::{Gc, Mutation};
use hashbrown::HashSet;
use crate::bytecode_reader::BytecodeReader;
use crate::instructions::misc::canon_path_str;
use crate::value::*;
use crate::{Break, CallFrame, PendingLoad, PendingScope, Step, Vm, VmRuntimeCtx, VmRuntimeCtxExt};
@@ -19,11 +20,11 @@ impl<'gc> Vm<'gc> {
) -> Step {
// stack: [path]
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(),
None => {
return self.finish_err(Error::eval_error(format!(
"expected a path string, got {}",
"expected a path or string, got {}",
path_val.ty()
)));
}
@@ -93,11 +94,11 @@ impl<'gc> Vm<'gc> {
// stack: [scope, path]
let (scope_attrs, path_val) =
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(),
None => {
return self.finish_err(Error::eval_error(format!(
"expected a path string, got {}",
"expected a path or string, got {}",
path_val.ty()
)));
}
@@ -145,17 +146,28 @@ impl<'gc> Vm<'gc> {
mc: &Mutation<'gc>,
) -> Step {
let path_val = self.force_and_retry::<StrictValue>(reader, mc)?;
let path = match ctx.get_string(path_val) {
Some(s) => s.to_owned(),
None => {
// CppNix: 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 self.finish_err(Error::eval_error(format!(
"expected a path string, got {}",
"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 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 {
std::fs::metadata(p).map(|m| m.is_dir()).unwrap_or(false)
} else {
+4
View File
@@ -62,6 +62,10 @@ impl<'gc> Vm<'gc> {
ScopedImportFinalize => self.primop_scoped_import_finalize(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:?}"),
}
+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";
StringId => RawTag::P4, "SmallString";
PrimOp => RawTag::P5, "PrimOp";
Path => RawTag::N6, "Path";
}
gc {
i64 => RawTag::P6, "BigInt";
@@ -289,6 +290,8 @@ impl<'gc> Value<'gc> {
NixType::PrimOp
} else if self.is::<NixString>() {
NixType::String
} else if self.is::<Path>() {
NixType::Path
} else if self.is::<AttrSet>() {
NixType::AttrSet
} 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.
///
/// Stored on the GC heap via `Gc<'gc, NixString>`. The string data itself
@@ -659,6 +676,7 @@ pub(crate) enum NixType {
Bool,
Null,
String,
Path,
AttrSet,
List,
Thunk,
@@ -676,6 +694,7 @@ impl NixType {
Bool => "a boolean",
Null => "null",
String => "a string",
Path => "a path",
AttrSet => "a set",
List => "a list",
Thunk => "a thunk",