diff --git a/nix-js/runtime-ts/src/builtins/attrs.ts b/nix-js/runtime-ts/src/builtins/attrs.ts index 01d7c09..93755da 100644 --- a/nix-js/runtime-ts/src/builtins/attrs.ts +++ b/nix-js/runtime-ts/src/builtins/attrs.ts @@ -140,3 +140,22 @@ export const zipAttrsWith = return result; }; + +export const unsafeGetAttrPos = + (attrName: NixValue) => + (attrSet: NixValue): NixValue => { + const name = forceStringValue(attrName); + const attrs = forceAttrs(attrSet); + + if (!(name in attrs)) { + return null; + } + + const positions = (attrs as any)[Nix.ATTR_POSITIONS]; + if (!positions || !(name in positions)) { + return null; + } + + const span = positions[name]; + return Nix.mkPos(span); + }; diff --git a/nix-js/runtime-ts/src/builtins/index.ts b/nix-js/runtime-ts/src/builtins/index.ts index abe334c..2dd3c35 100644 --- a/nix-js/runtime-ts/src/builtins/index.ts +++ b/nix-js/runtime-ts/src/builtins/index.ts @@ -173,6 +173,7 @@ export const builtins: any = { catAttrs: mkPrimop(attrs.catAttrs, "catAttrs", 2), groupBy: mkPrimop(attrs.groupBy, "groupBy", 2), zipAttrsWith: mkPrimop(attrs.zipAttrsWith, "zipAttrsWith", 2), + unsafeGetAttrPos: mkPrimop(attrs.unsafeGetAttrPos, "unsafeGetAttrPos", 2), stringLength: mkPrimop(string.stringLength, "stringLength", 1), substring: mkPrimop(string.substring, "substring", 3), @@ -232,7 +233,6 @@ export const builtins: any = { 1, ), unsafeDiscardStringContext: mkPrimop(misc.unsafeDiscardStringContext, "unsafeDiscardStringContext", 1), - unsafeGetAttrPos: mkPrimop(misc.unsafeGetAttrPos, "unsafeGetAttrPos", 2), addDrvOutputDependencies: mkPrimop(misc.addDrvOutputDependencies, "addDrvOutputDependencies", 2), compareVersions: mkPrimop(misc.compareVersions, "compareVersions", 2), flakeRefToString: mkPrimop(misc.flakeRefToString, "flakeRefToString", 1), diff --git a/nix-js/runtime-ts/src/builtins/misc.ts b/nix-js/runtime-ts/src/builtins/misc.ts index d8ea458..d4f7f09 100644 --- a/nix-js/runtime-ts/src/builtins/misc.ts +++ b/nix-js/runtime-ts/src/builtins/misc.ts @@ -5,7 +5,7 @@ import { createThunk, force } from "../thunk"; import { CatchableError } from "../types"; import type { NixAttrs, NixBool, NixStrictValue, NixValue } from "../types"; -import { forceList, forceAttrs, forceFunction, forceStringValue, forceString } from "../type-assert"; +import { forceList, forceAttrs, forceFunction, forceStringValue, forceString, forceStringNoCtx } from "../type-assert"; import * as context from "./context"; import { compareValues } from "../operators"; import { isBool, isFloat, isInt, isList, isString, typeOf } from "./type-check"; @@ -50,10 +50,6 @@ export const unsafeDiscardOutputDependency = context.unsafeDiscardOutputDependen export const unsafeDiscardStringContext = context.unsafeDiscardStringContext; -export const unsafeGetAttrPos = (s: NixValue): never => { - throw new Error("Not implemented: unsafeGetAttrPos"); -}; - export const addDrvOutputDependencies = context.addDrvOutputDependencies; export const compareVersions = diff --git a/nix-js/runtime-ts/src/helpers.ts b/nix-js/runtime-ts/src/helpers.ts index ab4549b..647095a 100644 --- a/nix-js/runtime-ts/src/helpers.ts +++ b/nix-js/runtime-ts/src/helpers.ts @@ -388,3 +388,7 @@ export const ifFunc = (cond: NixValue, consq: NixValue, alter: NixValue) => { } return alter; }; + +export const mkPos = (span: string): NixAttrs => { + return Deno.core.ops.op_decode_span(span); +}; diff --git a/nix-js/runtime-ts/src/index.ts b/nix-js/runtime-ts/src/index.ts index e515e54..88cd884 100644 --- a/nix-js/runtime-ts/src/index.ts +++ b/nix-js/runtime-ts/src/index.ts @@ -18,12 +18,13 @@ import { pushContext, popContext, withContext, + mkPos, } from "./helpers"; 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 } from "./types"; +import { IS_PATH, mkAttrs, mkFunction, mkAttrsWithPos, ATTR_POSITIONS } from "./types"; import { forceBool } from "./type-assert"; export type NixRuntime = typeof Nix; @@ -53,7 +54,10 @@ export const Nix = { concatStringsWithContext, StringCoercionMode, mkAttrs, + mkAttrsWithPos, mkFunction, + mkPos, + ATTR_POSITIONS, pushContext, popContext, diff --git a/nix-js/runtime-ts/src/types.ts b/nix-js/runtime-ts/src/types.ts index 86b6439..0d42a79 100644 --- a/nix-js/runtime-ts/src/types.ts +++ b/nix-js/runtime-ts/src/types.ts @@ -87,6 +87,39 @@ export const mkAttrs = (attrs: NixAttrs, keys: NixValue[], values: NixValue[]): return attrs; }; +const ATTR_POSITIONS = Symbol("attrPositions"); + +export const mkAttrsWithPos = ( + attrs: NixAttrs, + positions: Record, + dyns?: { dynKeys: NixValue[]; dynVals: NixValue[]; dynSpans: string[] } +): NixAttrs => { + if (dyns) { + const len = dyns.dynKeys.length; + for (let i = 0; i < len; i++) { + const key = force(dyns.dynKeys[i]); + if (key === null) { + continue; + } + const str = forceStringNoCtx(key); + attrs[str] = dyns.dynVals[i]; + positions[str] = dyns.dynSpans[i]; + } + } + + if (Object.keys(positions).length > 0) { + Object.defineProperty(attrs, ATTR_POSITIONS, { + value: positions, + enumerable: false, + writable: false, + }); + } + + return attrs; +}; + +export { ATTR_POSITIONS }; + /** * Interface for lazy thunk values * Thunks delay evaluation until forced diff --git a/nix-js/runtime-ts/src/types/global.d.ts b/nix-js/runtime-ts/src/types/global.d.ts index 8fe747f..9d887bc 100644 --- a/nix-js/runtime-ts/src/types/global.d.ts +++ b/nix-js/runtime-ts/src/types/global.d.ts @@ -42,6 +42,7 @@ declare global { function op_read_dir(path: string): Record; function op_path_exists(path: string): boolean; function op_sha256_hex(data: string): string; + function op_decode_span(span: string): { file: string | null; line: number | null; column: number | null }; function op_make_store_path(ty: string, hash_hex: string, name: string): string; function op_output_path_name(drv_name: string, output_name: string): string; function op_make_fixed_output_path( diff --git a/nix-js/src/codegen.rs b/nix-js/src/codegen.rs index fe79f27..bdbfe38 100644 --- a/nix-js/src/codegen.rs +++ b/nix-js/src/codegen.rs @@ -171,6 +171,10 @@ impl Compile for Ir { ) } } + Ir::CurPos(cur_pos) => { + let span_str = encode_span(cur_pos.span, ctx); + format!("Nix.mkPos({})", span_str) + } } } } @@ -346,9 +350,10 @@ impl Compile for Select { impl Compile for AttrSet { fn compile(&self, ctx: &Ctx) -> String { let mut attrs = Vec::new(); + let mut attr_positions = Vec::new(); let stack_trace_enabled = std::env::var("NIX_JS_STACK_TRACE").is_ok(); - for (&sym, &expr) in &self.stcs { + for (&sym, &(expr, attr_span)) in &self.stcs { let key = ctx.get_sym(sym); let value_code = ctx.get_ir(expr).compile(ctx); @@ -362,12 +367,15 @@ impl Compile for AttrSet { value_code }; attrs.push(format!("{}:{}", key.escape_quote(), value)); + + let attr_pos_str = encode_span(attr_span, ctx); + attr_positions.push(format!("{}:{}", key.escape_quote(), attr_pos_str)); } if !self.dyns.is_empty() { - let (keys, vals) = self.dyns.iter().map(|&(key, val)| { - let key = ctx.get_ir(key).compile(ctx); - let val_expr = ctx.get_ir(val); + let (keys, vals, dyn_spans) = self.dyns.iter().map(|(key, val, attr_span)| { + let key = ctx.get_ir(*key).compile(ctx); + let val_expr = ctx.get_ir(*val); let val = val_expr.compile(ctx); let span = val_expr.span(); let val = if stack_trace_enabled { @@ -379,9 +387,19 @@ impl Compile for AttrSet { } else { val }; - (key, val) - }).collect::<(Vec<_>, Vec<_>)>(); - format!("Nix.mkAttrs({{{}}},[{}],[{}])", attrs.join(","), keys.join(","), vals.join(",")) + let dyn_span_str = encode_span(*attr_span, ctx); + (key, val, dyn_span_str) + }).multiunzip::<(Vec<_>, Vec<_>, Vec<_>)>(); + format!( + "Nix.mkAttrsWithPos({{{}}},{{{}}},{{dynKeys:[{}],dynVals:[{}],dynSpans:[{}]}})", + attrs.join(","), + attr_positions.join(","), + keys.join(","), + vals.join(","), + dyn_spans.join(",") + ) + } else if !attr_positions.is_empty() { + format!("Nix.mkAttrsWithPos({{{}}},{{{}}})", attrs.join(","), attr_positions.join(",")) } else { format!("{{{}}}", attrs.join(",")) } diff --git a/nix-js/src/ir.rs b/nix-js/src/ir.rs index 73d23d6..064a3a1 100644 --- a/nix-js/src/ir.rs +++ b/nix-js/src/ir.rs @@ -59,7 +59,7 @@ ir! { Bool(bool), Null, Str { pub val: String }, - AttrSet { pub stcs: HashMap, pub dyns: Vec<(ExprId, ExprId)> }, + AttrSet { pub stcs: HashMap, pub dyns: Vec<(ExprId, ExprId, rnix::TextRange)> }, List { pub items: Vec }, HasAttr { pub lhs: ExprId, pub rhs: Vec }, @@ -78,6 +78,7 @@ ir! { Thunk(ExprId), Builtins, Builtin(SymId), + CurPos, } impl Ir { @@ -106,6 +107,7 @@ impl Ir { Ir::Thunk(t) => t.span, Ir::Builtins(b) => b.span, Ir::Builtin(b) => b.span, + Ir::CurPos(c) => c.span, } } } @@ -123,7 +125,7 @@ impl AttrSet { match attr { Attr::Str(ident, span) => { // If the next attribute is a static string. - if let Some(&id) = self.stcs.get(&ident) { + if let Some(&(id, _)) = self.stcs.get(&ident) { // If a sub-attrset already exists, recurse into it. let mut ir = ctx.extract_ir(id); let result = ir @@ -151,8 +153,8 @@ impl AttrSet { span, }; attrs._insert(path, name, value, ctx)?; - let attrs = ctx.new_expr(attrs.to_ir()); - self.stcs.insert(ident, attrs); + let attrs_expr = ctx.new_expr(attrs.to_ir()); + self.stcs.insert(ident, (attrs_expr, span)); } Ok(()) } @@ -166,7 +168,7 @@ impl AttrSet { span, }; attrs._insert(path, name, value, ctx)?; - self.dyns.push((dynamic, ctx.new_expr(attrs.to_ir()))); + self.dyns.push((dynamic, ctx.new_expr(attrs.to_ir()), span)); Ok(()) } } @@ -174,7 +176,7 @@ impl AttrSet { // This is the final attribute in the path, so insert the value here. match name { Attr::Str(ident, span) => { - if self.stcs.insert(ident, value).is_some() { + if self.stcs.insert(ident, (value, span)).is_some() { return Err(Error::downgrade_error( format!( "attribute '{}' already defined", @@ -185,8 +187,8 @@ impl AttrSet { )); } } - Attr::Dynamic(dynamic, _) => { - self.dyns.push((dynamic, value)); + Attr::Dynamic(dynamic, span) => { + self.dyns.push((dynamic, value, span)); } } Ok(()) diff --git a/nix-js/src/ir/downgrade.rs b/nix-js/src/ir/downgrade.rs index 35eb135..686f5de 100644 --- a/nix-js/src/ir/downgrade.rs +++ b/nix-js/src/ir/downgrade.rs @@ -174,9 +174,15 @@ impl Downgrade for ast::Literal { impl Downgrade for ast::Ident { fn downgrade(self, ctx: &mut Ctx) -> Result { - let sym = self.ident_token().unwrap().to_string(); - let sym = ctx.new_sym(sym); - ctx.lookup(sym, self.syntax().text_range()) + let text = self.ident_token().unwrap().to_string(); + let span = self.syntax().text_range(); + + if text == "__curPos" { + return Ok(ctx.new_expr(CurPos { span }.to_ir())); + } + + let sym = ctx.new_sym(text); + ctx.lookup(sym, span) } } @@ -203,7 +209,7 @@ impl Downgrade for ast::AttrSet { for sym in binding_keys { // FIXME: span let expr = ctx.lookup(*sym, synthetic_span())?; - attrs.stcs.insert(*sym, expr); + attrs.stcs.insert(*sym, (expr, synthetic_span())); } Ok(ctx.new_expr(attrs.to_ir())) @@ -325,7 +331,7 @@ impl Downgrade for ast::LegacyLet { for sym in binding_keys { // FIXME: span let expr = ctx.lookup(sym, synthetic_span())?; - attrs.stcs.insert(sym, expr); + attrs.stcs.insert(sym, (expr, synthetic_span())); } Result::Ok(ctx.new_expr(attrs.to_ir())) @@ -334,7 +340,6 @@ impl Downgrade for ast::LegacyLet { let body_sym = ctx.new_sym("body".to_string()); let select = Select { expr: attrset_expr, - // FIXME: span attrpath: vec![Attr::Str(body_sym, synthetic_span())], default: None, span, diff --git a/nix-js/src/ir/utils.rs b/nix-js/src/ir/utils.rs index 6ed4749..b6ebaf3 100644 --- a/nix-js/src/ir/utils.rs +++ b/nix-js/src/ir/utils.rs @@ -59,7 +59,7 @@ pub fn downgrade_static_attrs( } } - Ok(attrs.stcs) + Ok(attrs.stcs.into_iter().map(|(k, (v, _))| (k, v)).collect()) } /// Downgrades an `inherit` statement. @@ -67,7 +67,7 @@ pub fn downgrade_static_attrs( /// `inherit a b;` is translated into `a = a; b = b;` (i.e., bringing variables into scope). pub fn downgrade_inherit( inherit: ast::Inherit, - stcs: &mut HashMap, + stcs: &mut HashMap, ctx: &mut impl DowngradeContext, ) -> Result<()> { // Downgrade the `from` expression if it exists. @@ -122,7 +122,7 @@ pub fn downgrade_inherit( .with_span(span) .with_source(ctx.get_current_source())); } - Entry::Vacant(vacant) => vacant.insert(expr), + Entry::Vacant(vacant) => vacant.insert((expr, span)), }; } Ok(()) @@ -534,7 +534,7 @@ where } } - Ok(temp_attrs.stcs) + Ok(temp_attrs.stcs.into_iter().map(|(k, (v, _))| (k, v)).collect()) }, body_fn, ) diff --git a/nix-js/src/runtime.rs b/nix-js/src/runtime.rs index 5711e33..d39d95d 100644 --- a/nix-js/src/runtime.rs +++ b/nix-js/src/runtime.rs @@ -51,6 +51,7 @@ fn runtime_extension() -> Extension { op_path_exists(), op_resolve_path(), op_sha256_hex(), + op_decode_span::(), op_make_store_path::(), op_output_path_name(), op_make_fixed_output_path::(), @@ -268,6 +269,56 @@ fn op_sha256_hex(#[string] data: String) -> String { crate::nix_hash::sha256_hex(&data) } +#[deno_core::op2] +#[serde] +fn op_decode_span( + state: &mut OpState, + #[string] span_str: String, +) -> std::result::Result { + let parts: Vec<&str> = span_str.split(':').collect(); + if parts.len() != 3 { + return Ok(serde_json::json!({ + "file": serde_json::Value::Null, + "line": serde_json::Value::Null, + "column": serde_json::Value::Null + })); + } + + let source_id: usize = parts[0].parse().map_err(|_| "Invalid source ID")?; + let start: u32 = parts[1].parse().map_err(|_| "Invalid start offset")?; + + let ctx: &Ctx = state.get_ctx(); + let source = ctx.get_source(source_id); + let content = &source.src; + + let (line, column) = byte_offset_to_line_col(content, start as usize); + + Ok(serde_json::json!({ + "file": source.get_name(), + "line": line, + "column": column + })) +} + +fn byte_offset_to_line_col(content: &str, offset: usize) -> (u32, u32) { + let mut line = 1u32; + let mut col = 1u32; + + for (idx, ch) in content.char_indices() { + if idx >= offset { + break; + } + if ch == '\n' { + line += 1; + col = 1; + } else { + col += 1; + } + } + + (line, col) +} + #[deno_core::op2] #[string] fn op_make_store_path(