Skip to content

Commit d7f6516

Browse files
authored
libnixf: suggest adding builtins. for known primops (#720)
1 parent 7ba686e commit d7f6516

File tree

14 files changed

+198
-129
lines changed

14 files changed

+198
-129
lines changed

flake.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
;
3636
nixComponents = nixVersions.nixComponents_2_30;
3737
llvmPackages = llvmPackages_19;
38-
nixf = callPackage ./libnixf { };
38+
nixf = callPackage ./libnixf { inherit (nixComponents) nix-expr; };
3939
nixt = callPackage ./libnixt { inherit nixComponents; };
4040
nixd = callPackage ./nixd {
4141
inherit nixComponents nixf nixt;

libnixf/default.nix

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
boost,
1010
nlohmann_json,
1111
python312,
12+
nix-expr,
1213
}:
1314

1415
stdenv.mkDerivation {
@@ -42,6 +43,7 @@ stdenv.mkDerivation {
4243
gtest
4344
boost
4445
nlohmann_json
46+
nix-expr
4547
];
4648

4749
meta = {
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#include <cstdint>
2+
#include <map>
3+
#include <string>
4+
#include <vector>
5+
6+
namespace nixf {
7+
8+
/**
9+
* Info about a primitive operation, but omit the implementation.
10+
*/
11+
struct PrimOpInfo {
12+
/**
13+
* Names of the parameters of a primop, for primops that take a
14+
* fixed number of arguments to be substituted for these parameters.
15+
*/
16+
std::vector<std::string> Args;
17+
18+
/**
19+
* Aritiy of the primop.
20+
*
21+
* If `args` is not empty, this field will be computed from that
22+
* field instead, so it doesn't need to be manually set.
23+
*/
24+
size_t Arity = 0;
25+
26+
/**
27+
* Optional free-form documentation about the primop.
28+
*/
29+
std::string Doc;
30+
31+
/**
32+
* If true, this primop is not exposed to the user.
33+
*/
34+
bool Internal;
35+
};
36+
37+
extern std::map<std::string, nixf::PrimOpInfo> PrimOpsInfo;
38+
39+
/// \brief Result of looking up a primop by name.
40+
enum class PrimopLookupResult : std::uint8_t {
41+
/// The primop was found with an exact match.
42+
Found,
43+
/// The primop was found, but needs "builtin." prefix.
44+
PrefixedFound,
45+
/// The primop was not found.
46+
NotFound,
47+
};
48+
49+
/// \brief Look up information about a global primop by name.
50+
PrimopLookupResult lookupGlobalPrimOpInfo(const std::string &Name);
51+
52+
} // namespace nixf

libnixf/include/nixf/Sema/VariableLookup.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ class VariableLookupAnalysis {
115115
Undefined,
116116
FromWith,
117117
Defined,
118+
PrimOp,
118119
NoSuchVar,
119120
};
120121

libnixf/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ project( 'nixf'
44
, version: 'nightly'
55
)
66

7+
nix_expr = dependency('nix-expr')
78
nlohmann_json = dependency('nlohmann_json')
89
boost = dependency('boost')
910
gtest = dependency('gtest')
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#include <nix/expr/eval.hh>
2+
#include <nix/expr/primops.hh>
3+
4+
#include <iostream>
5+
6+
int main(int argc, char **argv) {
7+
std::freopen(argv[1], "w", stdout);
8+
nix::initGC();
9+
// Generate .cpp file that inject all primops into a custom vector.
10+
// This is used by the language server to provide documentation,
11+
// but omit the implementation of primops.
12+
std::cout << "#include <nixf/Sema/PrimOpInfo.h>" << "\n\n";
13+
std::cout << "std::map<std::string, nixf::PrimOpInfo> nixf::PrimOpsInfo = {"
14+
<< '\n';
15+
for (const auto &PrimOp : nix::RegisterPrimOp::primOps()) {
16+
std::cout << " {" << "\"" << PrimOp.name << "\", {\n";
17+
18+
// .args
19+
std::cout << " .Args = {";
20+
for (size_t I = 0; I < PrimOp.args.size(); ++I) {
21+
std::cout << "\"" << PrimOp.args[I] << "\"";
22+
if (I + 1 < PrimOp.args.size()) {
23+
std::cout << ", ";
24+
}
25+
}
26+
std::cout << "},\n";
27+
28+
// .arity
29+
std::cout << " .Arity = " << PrimOp.arity << ",\n";
30+
31+
// .doc
32+
std::cout << " .Doc = R\"xabc(" << (PrimOp.doc ? PrimOp.doc : "")
33+
<< ")xabc\",\n";
34+
35+
// .internal
36+
std::cout << " .Internal = " << (PrimOp.internal ? "true" : "false")
37+
<< ",\n";
38+
39+
std::cout << " }},\n";
40+
}
41+
std::cout << "};\n";
42+
return 0;
43+
}

libnixf/src/Basic/diagnostic.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,4 +231,13 @@ class Diagnostic(TypedDict):
231231
"severity": "Warning",
232232
"message": "unused `with` expression",
233233
},
234+
# Primary Operation (PrimOp) related diagnostics
235+
# We call this "builtin" in messages to match Nix's terminology
236+
# "PrimOp" is specific to Nix/nixf implementation
237+
{
238+
"sname": "sema-primop-needs-prefix",
239+
"cname": "PrimOpNeedsPrefix",
240+
"severity": "Error",
241+
"message": "this is not a prelude builtin, the `builtins.` prefix is required",
242+
},
234243
]

libnixf/src/Sema/PrimOpLookup.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#include "nixf/Sema/PrimOpInfo.h"
2+
3+
using namespace nixf;
4+
5+
PrimopLookupResult nixf::lookupGlobalPrimOpInfo(const std::string &Name) {
6+
if (PrimOpsInfo.contains(Name)) {
7+
return PrimopLookupResult::Found;
8+
}
9+
10+
// Prefix the name with "__", and check again.
11+
std::string PrefixedName = "__" + Name;
12+
if (PrimOpsInfo.contains(PrefixedName)) {
13+
return PrimopLookupResult::PrefixedFound;
14+
}
15+
16+
return PrimopLookupResult::NotFound;
17+
}

libnixf/src/Sema/VariableLookup.cpp

Lines changed: 32 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include "nixf/Basic/Diagnostic.h"
33
#include "nixf/Basic/Nodes/Attrs.h"
44
#include "nixf/Basic/Nodes/Lambda.h"
5+
#include "nixf/Sema/PrimOpInfo.h"
56

67
using namespace nixf;
78

@@ -108,11 +109,28 @@ void VariableLookupAnalysis::lookupVar(const ExprVar &Var,
108109
}
109110
Results.insert({&Var, LookupResult{LookupResultKind::FromWith, Def}});
110111
} else {
111-
// Otherwise, this variable is undefined.
112-
Results.insert({&Var, LookupResult{LookupResultKind::Undefined, nullptr}});
113-
Diagnostic &Diag =
114-
Diags.emplace_back(Diagnostic::DK_UndefinedVariable, Var.range());
115-
Diag << Var.id().name();
112+
// Check if this is a primop.
113+
switch (lookupGlobalPrimOpInfo(Name)) {
114+
case PrimopLookupResult::Found:
115+
Results.insert({&Var, LookupResult{LookupResultKind::PrimOp, nullptr}});
116+
break;
117+
case PrimopLookupResult::PrefixedFound: {
118+
Diagnostic &D =
119+
Diags.emplace_back(Diagnostic::DK_PrimOpNeedsPrefix, Var.range());
120+
D.fix("use `builtins.` prefix")
121+
.edit(TextEdit::mkInsertion(Var.range().lCur(), "builtins."));
122+
Results.insert({&Var, LookupResult{LookupResultKind::PrimOp, nullptr}});
123+
break;
124+
}
125+
case PrimopLookupResult::NotFound:
126+
// Otherwise, this variable is undefined.
127+
Results.insert(
128+
{&Var, LookupResult{LookupResultKind::Undefined, nullptr}});
129+
Diagnostic &Diag =
130+
Diags.emplace_back(Diagnostic::DK_UndefinedVariable, Var.range());
131+
Diag << Var.id().name();
132+
break;
133+
}
116134
}
117135
}
118136

@@ -344,129 +362,20 @@ void VariableLookupAnalysis::dfs(const Node &Root,
344362
void VariableLookupAnalysis::runOnAST(const Node &Root) {
345363
// Create a basic env
346364
DefBuilder DB;
347-
std::vector<std::string> Builtins{
348-
"__add",
349-
"__fetchurl",
350-
"__isFloat",
351-
"__seq",
352-
"break",
353-
"__addDrvOutputDependencies",
354-
"__filter",
355-
"__isFunction",
356-
"__sort",
365+
std::vector<std::string> BaseEnv{
357366
"builtins",
358-
"__addErrorContext",
359-
"__filterSource",
360-
"__isInt",
361-
"__split",
362-
"derivation",
363-
"__all",
364-
"__findFile",
365-
"__isList",
366-
"__splitVersion",
367-
"derivationStrict",
368-
"__any",
369-
"__flakeRefToString",
370-
"__isPath",
371-
"__storeDir",
372-
"dirOf",
373-
"__appendContext",
374-
"__floor",
375-
"__isString",
376-
"__storePath",
377-
"false",
378-
"__attrNames",
379-
"__foldl'",
380-
"__langVersion",
381-
"__stringLength",
382-
"fetchGit",
383-
"__attrValues",
384-
"__fromJSON",
385-
"__length",
386-
"__sub",
387-
"fetchMercurial",
388-
"__bitAnd",
389-
"__functionArgs",
390-
"__lessThan",
391-
"__substring",
392-
"fetchTarball",
393-
"__bitOr",
394-
"__genList",
395-
"__listToAttrs",
396-
"__tail",
397-
"fetchTree",
398-
"__bitXor",
399-
"__genericClosure",
400-
"__mapAttrs",
401-
"__toFile",
402-
"fromTOML",
403-
"__catAttrs",
404-
"__getAttr",
405-
"__match",
406-
"__toJSON",
407-
"import",
408-
"__ceil",
409-
"__getContext",
410-
"__mul",
411-
"__toPath",
412-
"isNull",
413-
"__compareVersions",
414-
"__getEnv",
415-
"__nixPath",
416-
"__toXML",
417-
"map",
418-
"__concatLists",
419-
"__getFlake",
420-
"__nixVersion",
421-
"__trace",
422-
"null",
423-
"__concatMap",
424-
"__groupBy",
425-
"__parseDrvName",
426-
"__traceVerbose",
427-
"placeholder",
428-
"__concatStringsSep",
429-
"__hasAttr",
430-
"__parseFlakeRef",
431-
"__tryEval",
432-
"removeAttrs",
433-
"__convertHash",
434-
"__hasContext",
435-
"__partition",
436-
"__typeOf",
437-
"scopedImport",
438-
"__currentSystem",
439-
"__hashFile",
440-
"__path",
441-
"__unsafeDiscardOutputDependency",
442-
"throw",
443-
"__currentTime",
444-
"__hashString",
445-
"__pathExists",
446-
"__unsafeDiscardStringContext",
447-
"toString",
448-
"__deepSeq",
449-
"__head",
450-
"__readDir",
451-
"__unsafeGetAttrPos",
452-
"true",
453-
"__div",
454-
"__intersectAttrs",
455-
"__readFile",
456-
"__zipAttrsWith",
457-
"__elem",
458-
"__isAttrs",
459-
"__readFileType",
460-
"abort",
461-
"__elemAt",
462-
"__isBool",
463-
"__replaceStrings",
464-
"baseNameOf",
465367
// This is an undocumented keyword actually.
466368
"__curPos",
467369
};
468370

469-
for (const auto &Builtin : Builtins)
371+
for (const auto &[Name, Info] : PrimOpsInfo) {
372+
if (!Info.Internal && !Name.starts_with("__")) {
373+
// Only add non-internal primops without "__" prefix.
374+
BaseEnv.push_back(Name);
375+
}
376+
}
377+
378+
for (const auto &Builtin : BaseEnv)
470379
DB.addBuiltin(Builtin);
471380

472381
auto Env = std::make_shared<EnvNode>(nullptr, DB.finish(), nullptr);

libnixf/src/meson.build

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,21 @@ tokens_kinds_inc = custom_target(
3535
install_dir: 'include/nixf/Parse/'
3636
)
3737

38+
39+
prim_ops_info_gen = executable(
40+
'PrimOpsInfo',
41+
'Basic/PrimOpsInfoGen.cpp',
42+
dependencies: [ nix_expr ],
43+
install: false,
44+
)
45+
46+
47+
builtins_cpp = custom_target(
48+
input: [ prim_ops_info_gen ],
49+
output: 'PrimOpsInfo.cpp',
50+
command: [ prim_ops_info_gen, '@OUTPUT@' ],
51+
)
52+
3853
libnixf = library(
3954
'nixf',
4055
'Basic/Nodes.cpp',
@@ -49,12 +64,14 @@ libnixf = library(
4964
'Parse/ParseStrings.cpp',
5065
'Parse/ParseSupport.cpp',
5166
'Sema/ParentMap.cpp',
67+
'Sema/PrimOpLookup.cpp',
5268
'Sema/SemaActions.cpp',
5369
'Sema/VariableLookup.cpp',
5470
diagnostic_enum_h,
5571
diagnostic_cpp,
5672
tokens_h,
5773
tokens_kinds_inc,
74+
builtins_cpp,
5875
include_directories: libnixf_inc,
5976
dependencies: libnixf_deps,
6077
install: true,

0 commit comments

Comments
 (0)