implement Path type
This commit is contained in:
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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:?}"),
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user