You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

184 lines
4.7 KiB
Plaintext

Global notes:
- When a JS construct has a statement form and expression form, the transpiler should *always* produce the expression form, to match Nix semantics. This can be forced in @babel/tmpl by wrapping the template in parentheses; these parentheses will not be present in the final output.
- "Expression wrapping" in this document refers to the practice of taking a syntax construct in JS that only exists in statement form (ie. no return value), and wrapping it in an IIFE using an internal `return` to translate that to expression form. This is necessary to match the semantics of Nix, where everything is an expression.
- "Readability" refers not only to how easy it is to literally read the transpiler output, but also to the clarity of eg. stacktraces when an error occurs. *Ideally*, it should be possible for a user to debug their Nix code from a JS stacktrace without needing any sourcemaps and without needing to look at the transpiler output.
- Identifiers which have special semantic meaning in JS but not in Nix, need to be prefixed such that Nix code cannot accidentally try to access them; for example, `this` and `function`.
- Runtime guards may be needed to prevent runtime type mismatches; Nix appears to be stricter here than JS.
- Call a non-function: error in both
- Specify undeclared named parameters to a function: error in Nix (without a rest parameter), allowed in JS
- Add a number to a string:
================
# `with` statement
Nix
with pkgs; <expression>
JS
(() => {
with (pkgs) {
return <expression>;
}
})()
Notes:
- Unlike in Nix, `with` is a statement in JS, not an expression, therefore expression wrapping is used.
- Strict mode in JS does not allow the use of `with`, so strict mode cannot be used. If it were ever necessary to do so, an alternative approach is to emulate the behaviour of `with` by translating all contained variable lookups to a `pkgs.foo ?? foo` format, but this would significantly reduce output readability and increase translation complexity.
================
# Attribute set literals
Nix
{
inherit foo;
inherit (other) bar;
a = 1;
b = a;
}
JS
{
foo: () => foo,
bar: () => other.bar,
a: () => 1,
b: () => this.?a ?? a
}
Notes:
- Lazy evaluation semantics require wrapping the values in a function, even though they are static values.
- `this.a` is not used to this object, but rather to any potential higher-level recursive attribute sets (see below)
================
# *Recursive* attribute set literals
Nix
rec { a = 1; b = a; }
JS
{
a: function() { return 1; },
b: function() { return this.a ?? a; }
}
Notes:
- Using regular functions instead of arrow functions; since a regular attribute set can refer to its own properties, a new `this` context needs to be created that refers to the object on which the lazy-wrapper is called.
-
================
# Attribute set merging
Nix
a // b // c
JS
{ ... a, ... b, ... c }
================
# List merging
Nix
a ++ b ++ c
JS
[ ... a, ... b, ... c ]
================
# Function definition
Nix
foo: <body>
JS
function(foo) { return <body>; }
Notes:
- FIXME: Is using a regular function (with its own `this` context) actually correct here? Or should it be an arrow function instead?
================
# Function definition with set pattern (named parameters)
Nix
{ foo, bar }: <body>
JS
function({ foo, bar }) { return <body>; }
Notes:
- The rest parameter (ellipsis) does not have or need an equivalent JS representation.
================
# Function definition with set pattern *and* a single identifier (ie. all named parameters as an attribute set)
Nix
{ foo, bar } @ all: <body>
JS
function(all) {
const { foo, bar } = all;
return <body>;
}
================
# Conditionals
Nix
if a then b else c
JS
(a) ? b : c
Notes:
- A ternary is used here despite the worse readability (compared to an if statement), because a ternary is natively an expression but an if statement is not. Therefore, this avoids an extra level of expression wrapping.
================
# `let ... in`
Nix
let a = 1; b = 2; in <expression>
JS
(() => {
let a = () => 1;
let b = () => 2;
return <expression>;
})()
Notes:
- Expression wrapping is used here to create a new scope for the evaluation of this expression; in Nix, a `let` binding only applies for the expression to which it is prefixed, not to the function scope like in JS, nor does JS have an equivalent native block scope for variable bindings.
- Bindings are *also* lazily evaluated
================
# Path literals
Nix
/some/path
JS
$core.evaluatePath("/some/path")
Notes:
- Returns a computed store path immediately as a string, and internally queues up an evaluation/build
================
# List literals
Nix
[ 1 2 3 (4 + 5) ]
JS
[ 1, 2, 3, 4 + 5 ]