Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add %eval_nix% primop #1465

Merged
merged 32 commits into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
dbdafce
add importing nix files behind a nix-experimental feature flag
Radvendii Jul 20, 2023
6499670
add %eval_nix% builtin to evaluate a nix string
Radvendii Jul 20, 2023
3922928
basic error reporting from nix
Radvendii Jul 20, 2023
c187723
error messages
Radvendii Jul 20, 2023
5bf858c
use pkg-config to find nix dependency
Radvendii Jul 24, 2023
7609d8c
nice error message when feature disabled
Radvendii Jul 24, 2023
9a9d7b8
better errors
Radvendii Jul 24, 2023
0cbada8
resolve XXX
Radvendii Jul 24, 2023
c918bca
put appropriate dependencies in nativeBuildInputs
Radvendii Jul 31, 2023
e185c3c
explain -sys crate option
Radvendii Jul 31, 2023
0c4b4cf
no %% around builtins in error messages
Radvendii Aug 1, 2023
df50300
clippy needs nativeBuildInputs
Radvendii Aug 4, 2023
b67634e
clippy
Radvendii Aug 8, 2023
9030b9b
fix cargo doc
Radvendii Aug 10, 2023
db5c915
use nix master
Radvendii Aug 14, 2023
17cd868
c++17 -> c++20
Radvendii Aug 22, 2023
bd9046b
cargo fmt
Radvendii Aug 22, 2023
32da396
review comments
Radvendii Sep 8, 2023
0dced44
fixup after merge
Radvendii Sep 8, 2023
d2dfae1
fix after rebase
Radvendii Sep 8, 2023
356c0ea
revert to (hopefully) known good nix commit
Radvendii Sep 11, 2023
138cc4b
try using nix's own nixpkgs
Radvendii Sep 11, 2023
69dcf73
this worked on build02
Radvendii Sep 13, 2023
35ef70f
debug for mac CI
Radvendii Oct 5, 2023
f28f237
debug doesn't work
Radvendii Oct 6, 2023
834f0f3
cargo fmt
Radvendii Oct 6, 2023
9c99347
debug
Radvendii Oct 6, 2023
2f5b650
don't fail everything if we can't debug
Radvendii Oct 6, 2023
dda015a
debug take N
Radvendii Oct 6, 2023
e9fb9b4
nix-version
Radvendii Oct 6, 2023
2d1c3cb
remove debug
Radvendii Oct 9, 2023
5aba32b
disable checks for nix on x86_64-darwin
Radvendii Oct 9, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ codespan-reporting = "0.11"
comrak = "0.17.0"
criterion = "0.4"
csv = "1"
cxx = "1.0"
cxx-build = "1.0"
derive_more = "0.99"
directories = "4.0.1"
env_logger = "0.10"
Expand All @@ -65,6 +67,7 @@ malachite-q = "0.3.2"
md-5 = "0.10.5"
once_cell = "1.17.1"
pprof = "0.11.1"
pkg-config = "0.3.27"
pretty = "0.11.3"
pretty_assertions = "1.3.0"
pyo3 = "0.17.3"
Expand Down
9 changes: 9 additions & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ keywords.workspace = true
license.workspace = true
repository.workspace = true
readme.workspace = true
# Maybe we should have the nix bindings in a separate -sys crate. maybe we
# should even have separate crates for "nix-store", "nix-cmd", "nix-expr", "nix-
# main", since they're separate libraries, but that seems a bit over the top.
# SEE: https://kornel.ski/rust-sys-crate
links = "nix"

[lib]
bench = false
Expand All @@ -21,9 +26,12 @@ repl-wasm = ["dep:wasm-bindgen", "dep:js-sys", "dep:serde_repr"]
doc = ["dep:comrak"]
format = ["dep:topiary", "dep:topiary-queries", "dep:tree-sitter-nickel"]
metrics = ["dep:metrics"]
nix-experimental = [ "dep:cxx", "dep:cxx-build", "dep:pkg-config" ]

[build-dependencies]
lalrpop.workspace = true
cxx-build = { workspace = true, optional = true }
pkg-config = { workspace = true, optional = true }

[dependencies]
lalrpop-util.workspace = true
Expand All @@ -32,6 +40,7 @@ simple-counter.workspace = true
clap = { workspace = true, features = ["derive"] }
codespan.workspace = true
codespan-reporting.workspace = true
cxx = { workspace = true, optional = true }
logos.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
Expand Down
25 changes: 25 additions & 0 deletions core/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,29 @@ fn main() {
.use_cargo_dir_conventions()
.process_file("src/parser/grammar.lalrpop")
.unwrap();

#[cfg(feature = "nix-experimental")]
{
use cxx_build::CFG;
use std::path::PathBuf;

for lib in &["nix-store", "nix-cmd", "nix-expr", "nix-main"] {
let lib = pkg_config::Config::new()
.atleast_version("2.16.0")
.probe(lib)
.unwrap();
let lib_include_paths = lib.include_paths.iter().map(PathBuf::as_path);
CFG.exported_header_dirs.extend(lib_include_paths);
}

cxx_build::bridge("src/nix_ffi/mod.rs")
.file("src/nix_ffi/cpp/nix.cc")
.flag_if_supported("-std=c++20")
.flag_if_supported("-U_FORTIFY_SOURCE") // Otherwise builds with `-O0` raise a lot of warnings
.compile("nickel-lang");

println!("cargo:rerun-if-changed=src/nix_ffi/mod.rs");
println!("cargo:rerun-if-changed=src/nix_ffi/cpp/nix.cc");
println!("cargo:rerun-if-changed=src/nix_ffi/cpp/nix.hh");
}
}
21 changes: 18 additions & 3 deletions core/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
use crate::error::{Error, ImportError, ParseError, ParseErrors, TypecheckError};
use crate::eval::cache::Cache as EvalCache;
use crate::eval::Closure;
#[cfg(feature = "nix-experimental")]
use crate::nix_ffi;
use crate::parser::{lexer::Lexer, ErrorTolerantParser};
use crate::position::TermPos;
use crate::stdlib::{self as nickel_stdlib, StdlibModule};
Expand All @@ -28,12 +30,15 @@ use std::time::SystemTime;
use void::Void;

/// Supported input formats.
#[derive(Clone, Copy, Eq, Debug, PartialEq)]
#[derive(Default, Clone, Copy, Eq, Debug, PartialEq)]
pub enum InputFormat {
#[default]
Nickel,
Json,
Yaml,
Toml,
#[cfg(feature = "nix-experimental")]
Nix,
}

impl InputFormat {
Expand All @@ -44,6 +49,8 @@ impl InputFormat {
Some("json") => Some(InputFormat::Json),
Some("yaml") | Some("yml") => Some(InputFormat::Yaml),
Some("toml") => Some(InputFormat::Toml),
#[cfg(feature = "nix-experimental")]
Some("nix") => Some(InputFormat::Nix),
_ => None,
}
}
Expand Down Expand Up @@ -520,7 +527,7 @@ impl Cache {

/// Parse a source without querying nor populating the cache.
pub fn parse_nocache(&self, file_id: FileId) -> Result<(RichTerm, ParseErrors), ParseError> {
self.parse_nocache_multi(file_id, InputFormat::Nickel)
self.parse_nocache_multi(file_id, InputFormat::default())
}

/// Parse a source without querying nor populating the cache. Support multiple formats.
Expand Down Expand Up @@ -588,6 +595,14 @@ impl Cache {
InputFormat::Toml => toml::from_str(self.files.source(file_id))
.map(|t| (attach_pos(t), ParseErrors::default()))
.map_err(|err| (ParseError::from_toml(err, file_id))),
#[cfg(feature = "nix-experimental")]
InputFormat::Nix => {
let json = nix_ffi::eval_to_json(self.files.source(file_id))
.map_err(|e| ParseError::from_nix(e.what(), file_id))?;
serde_json::from_str(&json)
.map(|t| (attach_pos(t), ParseErrors::default()))
.map_err(|err| ParseError::from_serde_json(err, file_id, &self.files))
}
}
}

Expand Down Expand Up @@ -1297,7 +1312,7 @@ impl ImportResolver for Cache {
) -> Result<(ResolvedTerm, FileId), ImportError> {
let parent_path = parent.and_then(|p| self.get_path(p)).map(PathBuf::from);
let path_buf = with_parent(path, parent_path);
let format = InputFormat::from_path(&path_buf).unwrap_or(InputFormat::Nickel);
let format = InputFormat::from_path(&path_buf).unwrap_or_default();
let id_op = self.get_or_add_file(&path_buf).map_err(|err| {
ImportError::IOError(
path_buf.to_string_lossy().into_owned(),
Expand Down
21 changes: 21 additions & 0 deletions core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,8 @@ pub enum ParseError {
/// The previous instance of the duplicated identifier.
prev_ident: LocIdent,
},
/// There was an attempt to use a feature that hasn't been enabled
DisabledFeature { feature: String, span: RawSpan },
}

/// An error occurring during the resolution of an import.
Expand Down Expand Up @@ -699,6 +701,9 @@ impl ParseError {
InternalParseError::DuplicateIdentInRecordPattern { ident, prev_ident } => {
ParseError::DuplicateIdentInRecordPattern { ident, prev_ident }
}
InternalParseError::DisabledFeature { feature, span } => {
ParseError::DisabledFeature { feature, span }
}
},
}
}
Expand Down Expand Up @@ -764,6 +769,12 @@ impl ParseError {
}),
)
}

#[cfg(feature = "nix-experimental")]
pub fn from_nix(error: &str, _file_id: FileId) -> Self {
// Span is shown in the nix error message
ParseError::ExternalFormatError(String::from("nix"), error.to_string(), None)
}
}

pub const INTERNAL_ERROR_MSG: &str =
Expand Down Expand Up @@ -1785,6 +1796,16 @@ impl IntoDiagnostics<FileId> for ParseError {
secondary(&prev_ident.pos.unwrap()).with_message("previous binding here"),
primary(&ident.pos.unwrap()).with_message("duplicated binding here"),
]),
ParseError::DisabledFeature { feature, span } => Diagnostic::error()
.with_message("interpreter compiled without required features")
.with_labels(vec![primary(&span).with_message(format!(
"this syntax is only supported with the `{}` feature enabled",
feature
))])
.with_notes(vec![format!(
"Recompile nickel with `--features {}`",
feature
)]),
};

vec![diagnostic]
Expand Down
27 changes: 27 additions & 0 deletions core/src/eval/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ use super::{
subst, Cache, Closure, Environment, ImportResolver, VirtualMachine,
};

#[cfg(feature = "nix-experimental")]
use crate::nix_ffi;

use crate::{
error::{EvalError, IllegalPolymorphicTailAction},
identifier::LocIdent,
Expand Down Expand Up @@ -1155,6 +1158,30 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
Err(mk_type_error!("label_push_diag", "Label"))
}}
}
#[cfg(feature = "nix-experimental")]
UnaryOp::EvalNix() => {
if let Term::Str(s) = &*t {
let json = nix_ffi::eval_to_json(&String::from(s)).map_err(|e| {
EvalError::Other(
format!("nix code failed to evaluate:\n {}", e.what()),
pos,
)
})?;
Ok(Closure::atomic_closure(
serde_json::from_str(&json).map_err(|e| {
EvalError::Other(format!("Nix produced invalid json: {}", e), pos)
})?,
))
} else {
// Not using mk_type_error! because of a non-uniform message
Err(EvalError::TypeError(
String::from("String"),
String::from("eval_nix takes a string of nix code as an argument"),
arg_pos,
RichTerm { term: t, pos },
))
}
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ pub mod error;
pub mod eval;
pub mod identifier;
pub mod label;
#[cfg(feature = "nix-experimental")]
pub mod nix_ffi;
pub mod parser;
pub mod position;
pub mod pretty;
Expand Down
47 changes: 47 additions & 0 deletions core/src/nix_ffi/cpp/nix.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#include "nix.hh"

using namespace nix;

#include <nix/config.h>
#include <nix/store-api.hh>
#include <nix/eval.hh>
#include <nix/shared.hh>
#include <nix/canon-path.hh>
#include <nix/nixexpr.hh>
#include <nix/value-to-json.hh>
#include <nix/command.hh>
#include <nix/value.hh>

#include "nickel-lang-core/src/nix_ffi/mod.rs.h"

// nix is designed with command-line use in mind, and there's some setup stuff
// that's tied to the EvalCommand class.
struct DummyEvalCommand : virtual EvalCommand {
Radvendii marked this conversation as resolved.
Show resolved Hide resolved

void run(ref<Store> store) override
{
// so it doesn't complain about unused variables
(void)store;
}

};

// FIXME: error messages have an extra `error:` on them
rust::String eval_to_json(rust::Str nixCode)
{
auto dummy = DummyEvalCommand({});
initNix();
initGC();
auto & state = *dummy.getEvalState();

auto vRes = state.allocValue();
state.eval(state.parseExprFromString(std::string(nixCode), state.rootPath(CanonPath::root)), *vRes);

std::stringstream out;
NixStringContext context;
// TODO: don't force the value. figure out a way to return thunks.
// (second argument)
printValueAsJSON(state, true, *vRes, noPos, out, context, false);

return out.str();
}
Loading