feat: implement scopedImport & REPL scoping
This commit is contained in:
@@ -46,8 +46,24 @@ Dependency tracking for imported derivations may be incomplete.`,
|
||||
|
||||
export const scopedImport =
|
||||
(scope: NixValue) =>
|
||||
(path: NixValue): never => {
|
||||
throw new Error("Not implemented: scopedImport");
|
||||
(path: NixValue): NixValue => {
|
||||
const scopeAttrs = forceAttrs(scope);
|
||||
const scopeKeys = Object.keys(scopeAttrs);
|
||||
|
||||
const context: NixStringContext = new Set();
|
||||
const pathStr = coerceToPath(path, context);
|
||||
|
||||
if (context.size > 0) {
|
||||
console.warn(
|
||||
`[WARN] scopedImport: Path has string context which is not yet fully tracked.
|
||||
Dependency tracking for imported derivations may be incomplete.`,
|
||||
);
|
||||
}
|
||||
|
||||
const code = Deno.core.ops.op_scoped_import(pathStr, scopeKeys);
|
||||
|
||||
const scopedFunc = Function(`return (${code})`)();
|
||||
return scopedFunc(scopeAttrs);
|
||||
};
|
||||
|
||||
export const storePath = (pathArg: NixValue): StringWithContext => {
|
||||
|
||||
@@ -23,11 +23,13 @@ import { op } from "./operators";
|
||||
import { builtins, PRIMOP_METADATA } from "./builtins";
|
||||
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
|
||||
import { HAS_CONTEXT } from "./string-context";
|
||||
import { IS_PATH, mkAttrs, mkFunction, mkAttrsWithPos, ATTR_POSITIONS } from "./types";
|
||||
import { IS_PATH, mkAttrs, mkFunction, mkAttrsWithPos, ATTR_POSITIONS, NixValue } from "./types";
|
||||
import { forceBool } from "./type-assert";
|
||||
|
||||
export type NixRuntime = typeof Nix;
|
||||
|
||||
const replBindings: Record<string, NixValue> = {};
|
||||
|
||||
/**
|
||||
* The global Nix runtime object
|
||||
*/
|
||||
@@ -67,6 +69,12 @@ export const Nix = {
|
||||
op,
|
||||
builtins,
|
||||
PRIMOP_METADATA,
|
||||
|
||||
replBindings,
|
||||
setReplBinding: (name: string, value: NixValue) => {
|
||||
replBindings[name] = value;
|
||||
},
|
||||
getReplBinding: (name: string) => replBindings[name],
|
||||
};
|
||||
|
||||
globalThis.Nix = Nix;
|
||||
|
||||
1
nix-js/runtime-ts/src/types/global.d.ts
vendored
1
nix-js/runtime-ts/src/types/global.d.ts
vendored
@@ -37,6 +37,7 @@ declare global {
|
||||
namespace ops {
|
||||
function op_resolve_path(currentDir: string, path: string): string;
|
||||
function op_import(path: string): string;
|
||||
function op_scoped_import(path: string, scopeKeys: string[]): string;
|
||||
function op_read_file(path: string): string;
|
||||
function op_read_file_type(path: string): string;
|
||||
function op_read_dir(path: string): Record<string, string>;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use hashbrown::HashSet;
|
||||
use nix_js::context::Context;
|
||||
use nix_js::error::Source;
|
||||
use rustyline::DefaultEditor;
|
||||
@@ -9,6 +10,7 @@ fn main() -> Result<()> {
|
||||
|
||||
let mut rl = DefaultEditor::new()?;
|
||||
let mut context = Context::new()?;
|
||||
let mut scope = HashSet::new();
|
||||
const RE: ere::Regex<3> = ere::compile_regex!("^[ \t]*([a-zA-Z_][a-zA-Z0-9_'-]*)[ \t]*(.*)$");
|
||||
loop {
|
||||
let readline = rl.readline("nix-js-repl> ");
|
||||
@@ -18,16 +20,24 @@ fn main() -> Result<()> {
|
||||
continue;
|
||||
}
|
||||
let _ = rl.add_history_entry(line.as_str());
|
||||
if let Some([Some(_), Some(_ident), Some(_expr)]) = RE.exec(&line) {
|
||||
eprintln!("Error: binding not implemented yet");
|
||||
continue;
|
||||
// if expr.is_empty() {
|
||||
// eprintln!("Error: missing expression after '='");
|
||||
// continue;
|
||||
// }
|
||||
// if let Err(err) = context.add_binding(ident, expr) {
|
||||
// eprintln!("Error: {}", err);
|
||||
// }
|
||||
if let Some([Some(_), Some(ident), Some(rest)]) = RE.exec(&line) {
|
||||
if let Some(expr) = rest.strip_prefix('=') {
|
||||
let expr = expr.trim_start();
|
||||
if expr.is_empty() {
|
||||
eprintln!("Error: missing expression after '='");
|
||||
continue;
|
||||
}
|
||||
match context.add_binding(ident, expr, &mut scope) {
|
||||
Ok(value) => println!("{} = {}", ident, value),
|
||||
Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
|
||||
}
|
||||
} else {
|
||||
let src = Source::new_repl(line)?;
|
||||
match context.eval_repl(src, &scope) {
|
||||
Ok(value) => println!("{value}"),
|
||||
Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let src = Source::new_repl(line)?;
|
||||
match context.eval_shallow(src) {
|
||||
|
||||
@@ -116,7 +116,7 @@ pub(crate) fn compile(expr: &Ir, ctx: &impl CodegenContext) -> String {
|
||||
code!(&mut buf, ctx;
|
||||
"Nix.builtins.storeDir="
|
||||
quoted(ctx.get_store_dir())
|
||||
";const currentDir="
|
||||
";const __currentDir="
|
||||
quoted(&ctx.get_current_dir().display().to_string())
|
||||
";return "
|
||||
expr
|
||||
@@ -125,6 +125,28 @@ pub(crate) fn compile(expr: &Ir, ctx: &impl CodegenContext) -> String {
|
||||
buf.into_string()
|
||||
}
|
||||
|
||||
pub(crate) fn compile_scoped(expr: &Ir, ctx: &impl CodegenContext) -> String {
|
||||
let mut buf = CodeBuffer::with_capacity(8192);
|
||||
|
||||
code!(&mut buf, ctx; "((__scope)=>{");
|
||||
|
||||
if std::env::var("NIX_JS_DEBUG_THUNKS").is_ok() {
|
||||
code!(&mut buf, ctx; "Nix.DEBUG_THUNKS.enabled=true;");
|
||||
}
|
||||
|
||||
code!(&mut buf, ctx;
|
||||
"Nix.builtins.storeDir="
|
||||
quoted(ctx.get_store_dir())
|
||||
";const __currentDir="
|
||||
quoted(&ctx.get_current_dir().display().to_string())
|
||||
";return "
|
||||
expr
|
||||
"})"
|
||||
);
|
||||
|
||||
buf.into_string()
|
||||
}
|
||||
|
||||
trait Compile<Ctx: CodegenContext> {
|
||||
fn compile(&self, ctx: &Ctx, buf: &mut CodeBuffer);
|
||||
}
|
||||
@@ -216,7 +238,7 @@ impl<Ctx: CodegenContext> Compile<Ctx> for Ir {
|
||||
code!(buf, ctx; quoted(&s.val));
|
||||
}
|
||||
Ir::Path(p) => {
|
||||
code!(buf, ctx; "Nix.resolvePath(currentDir," ctx.get_ir(p.expr) ")");
|
||||
code!(buf, ctx; "Nix.resolvePath(__currentDir," ctx.get_ir(p.expr) ")");
|
||||
}
|
||||
Ir::If(x) => x.compile(ctx, buf),
|
||||
Ir::BinOp(x) => x.compile(ctx, buf),
|
||||
@@ -275,6 +297,20 @@ impl<Ctx: CodegenContext> Compile<Ctx> for Ir {
|
||||
")"
|
||||
);
|
||||
}
|
||||
&Ir::ReplBinding(ReplBinding { inner: name, .. }) => {
|
||||
code!(buf, ctx;
|
||||
"Nix.getReplBinding("
|
||||
quoted(ctx.get_sym(name))
|
||||
")"
|
||||
);
|
||||
}
|
||||
&Ir::ScopedImportBinding(ScopedImportBinding { inner: name, .. }) => {
|
||||
code!(buf, ctx;
|
||||
"__scope["
|
||||
quoted(ctx.get_sym(name))
|
||||
"]"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::path::Path;
|
||||
use std::ptr::NonNull;
|
||||
|
||||
use hashbrown::HashMap;
|
||||
use hashbrown::{HashMap, HashSet};
|
||||
use itertools::Itertools as _;
|
||||
use rnix::TextRange;
|
||||
use string_interner::DefaultStringInterner;
|
||||
@@ -9,8 +9,8 @@ use string_interner::DefaultStringInterner;
|
||||
use crate::codegen::{CodegenContext, compile};
|
||||
use crate::error::{Error, Result, Source};
|
||||
use crate::ir::{
|
||||
Arg, ArgId, Bool, Builtin, Downgrade as _, DowngradeContext, ExprId, Ir, Null, SymId, Thunk,
|
||||
ToIr as _,
|
||||
Arg, ArgId, Bool, Builtin, Downgrade as _, DowngradeContext, ExprId, Ir, Null, ReplBinding,
|
||||
ScopedImportBinding, SymId, Thunk, ToIr as _,
|
||||
};
|
||||
use crate::runtime::{Runtime, RuntimeContext};
|
||||
use crate::store::{Store, StoreBackend, StoreConfig};
|
||||
@@ -47,18 +47,38 @@ impl Context {
|
||||
eval!(eval_shallow, "Nix.forceShallow({})");
|
||||
eval!(eval_deep, "Nix.forceDeep({})");
|
||||
|
||||
pub fn compile(&mut self, source: Source) -> Result<String> {
|
||||
self.ctx.compile(source)
|
||||
pub fn eval_repl<'a>(&'a mut self, source: Source, scope: &'a HashSet<SymId>) -> Result<Value> {
|
||||
tracing::info!("Starting evaluation");
|
||||
|
||||
tracing::debug!("Compiling code");
|
||||
let code = self.ctx.compile(source, Some(Scope::Repl(scope)))?;
|
||||
|
||||
tracing::debug!("Executing JavaScript");
|
||||
self.runtime.eval(format!("Nix.forceShallow({})", code), &mut self.ctx)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn eval_js(&mut self, code: String) -> Result<Value> {
|
||||
self.runtime.eval(code, &mut self.ctx)
|
||||
pub fn compile(&mut self, source: Source) -> Result<String> {
|
||||
self.ctx.compile(source, None)
|
||||
}
|
||||
|
||||
pub fn get_store_dir(&self) -> &str {
|
||||
self.ctx.get_store_dir()
|
||||
}
|
||||
|
||||
pub fn add_binding<'a>(&'a mut self, name: &str, expr: &str, scope: &'a mut HashSet<SymId>) -> Result<Value> {
|
||||
let source = Source::new_repl(expr.to_string())?;
|
||||
let code = self.ctx.compile(source, Some(Scope::Repl(scope)))?;
|
||||
|
||||
let sym = self.ctx.symbols.get_or_intern(name);
|
||||
|
||||
let eval_and_store = format!(
|
||||
"(()=>{{const __v=Nix.forceShallow({});Nix.setReplBinding(\"{}\",__v);return __v}})()",
|
||||
code, name
|
||||
);
|
||||
|
||||
scope.insert(sym);
|
||||
self.runtime.eval(eval_and_store, &mut self.ctx)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Ctx {
|
||||
@@ -166,9 +186,12 @@ impl Ctx {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn downgrade_ctx<'a>(&'a mut self) -> DowngradeCtx<'a> {
|
||||
fn downgrade_ctx<'a>(
|
||||
&'a mut self,
|
||||
extra_scope: Option<Scope<'a>>,
|
||||
) -> DowngradeCtx<'a> {
|
||||
let global_ref = unsafe { self.global.as_ref() };
|
||||
DowngradeCtx::new(self, global_ref)
|
||||
DowngradeCtx::new(self, global_ref, extra_scope)
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_dir(&self) -> &Path {
|
||||
@@ -190,7 +213,7 @@ impl Ctx {
|
||||
self.sources.get(id).expect("source not found").clone()
|
||||
}
|
||||
|
||||
fn compile(&mut self, source: Source) -> Result<String> {
|
||||
fn compile<'a>(&'a mut self, source: Source, extra_scope: Option<Scope<'a>>) -> Result<String> {
|
||||
tracing::debug!("Parsing Nix expression");
|
||||
|
||||
self.sources.push(source.clone());
|
||||
@@ -204,7 +227,7 @@ impl Ctx {
|
||||
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let root = self
|
||||
.downgrade_ctx()
|
||||
.downgrade_ctx(extra_scope)
|
||||
.downgrade(root.tree().expr().unwrap())?;
|
||||
|
||||
tracing::debug!("Generating JavaScript code");
|
||||
@@ -212,6 +235,40 @@ impl Ctx {
|
||||
tracing::debug!("Generated code: {}", &code);
|
||||
Ok(code)
|
||||
}
|
||||
|
||||
pub(crate) fn compile_scoped(
|
||||
&mut self,
|
||||
source: Source,
|
||||
scope: Vec<String>,
|
||||
) -> Result<String> {
|
||||
use crate::codegen::compile_scoped;
|
||||
|
||||
tracing::debug!("Parsing Nix expression for scoped import");
|
||||
|
||||
self.sources.push(source.clone());
|
||||
|
||||
let root = rnix::Root::parse(&source.src);
|
||||
if !root.errors().is_empty() {
|
||||
let error_msg = root.errors().iter().join("; ");
|
||||
let err = Error::parse_error(error_msg).with_source(source);
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
let scope = Scope::ScopedImport(
|
||||
scope
|
||||
.into_iter()
|
||||
.map(|k| self.symbols.get_or_intern(k))
|
||||
.collect(),
|
||||
);
|
||||
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let root = self.downgrade_ctx(Some(scope)).downgrade(root.tree().expr().unwrap())?;
|
||||
|
||||
tracing::debug!("Generating JavaScript code for scoped import");
|
||||
let code = compile_scoped(self.get_ir(root), self);
|
||||
tracing::debug!("Generated scoped code: {}", &code);
|
||||
Ok(code)
|
||||
}
|
||||
}
|
||||
|
||||
impl CodegenContext for Ctx {
|
||||
@@ -245,8 +302,11 @@ impl RuntimeContext for Ctx {
|
||||
fn add_source(&mut self, source: Source) {
|
||||
self.sources.push(source);
|
||||
}
|
||||
fn compile_code(&mut self, source: Source) -> Result<String> {
|
||||
self.compile(source)
|
||||
fn compile(&mut self, source: Source) -> Result<String> {
|
||||
self.compile(source, None)
|
||||
}
|
||||
fn compile_scoped(&mut self, source: Source, scope: Vec<String>) -> Result<String> {
|
||||
self.compile_scoped(source, scope)
|
||||
}
|
||||
fn get_source(&self, id: usize) -> Source {
|
||||
self.get_source(id)
|
||||
@@ -258,6 +318,8 @@ impl RuntimeContext for Ctx {
|
||||
|
||||
enum Scope<'ctx> {
|
||||
Global(&'ctx HashMap<SymId, ExprId>),
|
||||
Repl(&'ctx HashSet<SymId>),
|
||||
ScopedImport(HashSet<SymId>),
|
||||
Let(HashMap<SymId, ExprId>),
|
||||
Param(SymId, ExprId),
|
||||
With(ExprId),
|
||||
@@ -288,9 +350,15 @@ pub struct DowngradeCtx<'ctx> {
|
||||
}
|
||||
|
||||
impl<'ctx> DowngradeCtx<'ctx> {
|
||||
fn new(ctx: &'ctx mut Ctx, global: &'ctx HashMap<SymId, ExprId>) -> Self {
|
||||
fn new(
|
||||
ctx: &'ctx mut Ctx,
|
||||
global: &'ctx HashMap<SymId, ExprId>,
|
||||
extra_scope: Option<Scope<'ctx>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
scopes: vec![Scope::Global(global)],
|
||||
scopes: std::iter::once(Scope::Global(global))
|
||||
.chain(extra_scope)
|
||||
.collect(),
|
||||
irs: vec![],
|
||||
arg_id: 0,
|
||||
thunk_scopes: vec![Vec::new()],
|
||||
@@ -364,6 +432,16 @@ impl DowngradeContext for DowngradeCtx<'_> {
|
||||
return Ok(expr);
|
||||
}
|
||||
}
|
||||
&Scope::Repl(repl_bindings) => {
|
||||
if repl_bindings.contains(&sym) {
|
||||
return Ok(self.new_expr(ReplBinding { inner: sym, span }.to_ir()));
|
||||
}
|
||||
}
|
||||
Scope::ScopedImport(scoped_bindings) => {
|
||||
if scoped_bindings.contains(&sym) {
|
||||
return Ok(self.new_expr(ScopedImportBinding { inner: sym, span }.to_ir()));
|
||||
}
|
||||
}
|
||||
Scope::Let(let_scope) => {
|
||||
if let Some(&expr) = let_scope.get(&sym) {
|
||||
return Ok(self.new_expr(Thunk { inner: expr, span }.to_ir()));
|
||||
|
||||
@@ -71,6 +71,8 @@ ir! {
|
||||
Builtins,
|
||||
Builtin(SymId),
|
||||
CurPos,
|
||||
ReplBinding(SymId),
|
||||
ScopedImportBinding(SymId),
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
|
||||
@@ -18,7 +18,8 @@ type LocalSymbol<'a> = v8::Local<'a, v8::Symbol>;
|
||||
pub(crate) trait RuntimeContext: 'static {
|
||||
fn get_current_dir(&self) -> &Path;
|
||||
fn add_source(&mut self, path: Source);
|
||||
fn compile_code(&mut self, source: Source) -> Result<String>;
|
||||
fn compile(&mut self, source: Source) -> Result<String>;
|
||||
fn compile_scoped(&mut self, source: Source, scope: Vec<String>) -> Result<String>;
|
||||
fn get_source(&self, id: usize) -> Source;
|
||||
fn get_store(&self) -> &dyn Store;
|
||||
}
|
||||
@@ -44,6 +45,7 @@ fn runtime_extension<Ctx: RuntimeContext>() -> Extension {
|
||||
&deno_core::include_js_files!(nix_runtime dir "runtime-ts/dist", "runtime.js");
|
||||
let mut ops = vec![
|
||||
op_import::<Ctx>(),
|
||||
op_scoped_import::<Ctx>(),
|
||||
op_read_file(),
|
||||
op_read_file_type(),
|
||||
op_read_dir(),
|
||||
@@ -446,29 +448,3 @@ fn to_primop<'a>(
|
||||
Some(Value::PrimOpApp(name))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::context::Context;
|
||||
|
||||
#[test]
|
||||
fn to_value_working() {
|
||||
let mut ctx = Context::new().unwrap();
|
||||
const EXPR: &str = "({ test: [1., 9223372036854775807n, true, false, 'hello world!'] })";
|
||||
assert_eq!(
|
||||
ctx.eval_js(EXPR.into()).unwrap(),
|
||||
Value::AttrSet(AttrSet::new(std::collections::BTreeMap::from([(
|
||||
Symbol::from("test"),
|
||||
Value::List(List::new(vec![
|
||||
Value::Float(1.),
|
||||
Value::Int(9223372036854775807),
|
||||
Value::Bool(true),
|
||||
Value::Bool(false),
|
||||
Value::String("hello world!".to_string())
|
||||
]))
|
||||
)])))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use hashbrown::hash_map::{HashMap, Entry};
|
||||
use hashbrown::hash_map::{Entry, HashMap};
|
||||
|
||||
use deno_core::OpState;
|
||||
use regex::Regex;
|
||||
@@ -59,7 +59,7 @@ pub(super) fn op_import<Ctx: RuntimeContext>(
|
||||
.into(),
|
||||
};
|
||||
ctx.add_source(source.clone());
|
||||
return Ok(ctx.compile_code(source).map_err(|err| err.to_string())?);
|
||||
return Ok(ctx.compile(source).map_err(|err| err.to_string())?);
|
||||
} else {
|
||||
return Err(format!("Corepkg not found: {}", corepkg_name).into());
|
||||
}
|
||||
@@ -83,7 +83,37 @@ pub(super) fn op_import<Ctx: RuntimeContext>(
|
||||
tracing::debug!("Compiling file");
|
||||
ctx.add_source(source.clone());
|
||||
|
||||
Ok(ctx.compile_code(source).map_err(|err| err.to_string())?)
|
||||
Ok(ctx.compile(source).map_err(|err| err.to_string())?)
|
||||
}
|
||||
|
||||
#[deno_core::op2]
|
||||
#[string]
|
||||
pub(super) fn op_scoped_import<Ctx: RuntimeContext>(
|
||||
state: &mut OpState,
|
||||
#[string] path: String,
|
||||
#[serde] scope: Vec<String>,
|
||||
) -> std::result::Result<String, NixRuntimeError> {
|
||||
let _span = tracing::info_span!("op_scoped_import", path = %path).entered();
|
||||
let ctx: &mut Ctx = state.get_ctx_mut();
|
||||
|
||||
let current_dir = ctx.get_current_dir();
|
||||
let mut absolute_path = current_dir.join(&path);
|
||||
|
||||
if absolute_path.is_dir() {
|
||||
absolute_path.push("default.nix")
|
||||
}
|
||||
|
||||
tracing::info!("Scoped importing file: {}", absolute_path.display());
|
||||
|
||||
let source = Source::new_file(absolute_path.clone())
|
||||
.map_err(|e| format!("Failed to read {}: {}", absolute_path.display(), e))?;
|
||||
|
||||
tracing::debug!("Compiling file for scoped import");
|
||||
ctx.add_source(source.clone());
|
||||
|
||||
Ok(ctx
|
||||
.compile_scoped(source, scope)
|
||||
.map_err(|err| err.to_string())?)
|
||||
}
|
||||
|
||||
#[deno_core::op2]
|
||||
|
||||
@@ -7,8 +7,9 @@ use utils::{eval_deep, eval_deep_result};
|
||||
|
||||
#[test]
|
||||
fn derivation_minimal() {
|
||||
let result =
|
||||
eval_deep(r#"derivation { name = "hello"; builder = "/bin/sh"; system = "x86_64-linux"; }"#);
|
||||
let result = eval_deep(
|
||||
r#"derivation { name = "hello"; builder = "/bin/sh"; system = "x86_64-linux"; }"#,
|
||||
);
|
||||
|
||||
match result {
|
||||
Value::AttrSet(attrs) => {
|
||||
@@ -78,7 +79,8 @@ fn derivation_to_string() {
|
||||
|
||||
#[test]
|
||||
fn derivation_missing_name() {
|
||||
let result = eval_deep_result(r#"derivation { builder = "/bin/sh"; system = "x86_64-linux"; }"#);
|
||||
let result =
|
||||
eval_deep_result(r#"derivation { builder = "/bin/sh"; system = "x86_64-linux"; }"#);
|
||||
|
||||
assert!(result.is_err());
|
||||
let err_msg = result.unwrap_err().to_string();
|
||||
|
||||
@@ -181,10 +181,7 @@ eval_okay_test!(
|
||||
);
|
||||
eval_okay_test!(r#if);
|
||||
eval_okay_test!(ind_string);
|
||||
eval_okay_test!(
|
||||
#[ignore = "not implemented: scopedImport"]
|
||||
import
|
||||
);
|
||||
eval_okay_test!(import);
|
||||
eval_okay_test!(inherit_attr_pos);
|
||||
eval_okay_test!(
|
||||
#[ignore = "__overrides is not supported"]
|
||||
|
||||
Reference in New Issue
Block a user