Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
40 changes: 40 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,43 @@ jobs:
run: ./gradlew build --no-daemon -PjavaToolchain=${{ matrix.java }}
env:
GRADLE_OPTS: -Dorg.gradle.java.installations.auto-download=true

e2e:
name: Assemble and compile the e2e corpus
# macOS rather than Ubuntu for one reason: Homebrew is preinstalled and
# carries current NSIS versions, where Debian's packages lag. Bootstrapping
# Homebrew on Ubuntu would get the same makensis but spends a minute or two
# on setup first.
runs-on: macos-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5

# makensis is the oracle the corpus asserts against; without it run.sh skips.
# Homebrew tracks the current release rather than pinning one, so record the
# version: a red run caused by an NSIS upgrade should be distinguishable
# from one caused by a change here.
- name: Install NSIS
run: |
brew install makensis
makensis -VERSION

# One JDK only: the assembled .nsi does not vary by toolchain, so running
# the corpus across the matrix would just spend CI minutes on makensis.
# E2E_REQUIRE_MAKENSIS turns the skip path into a failure - in CI a missing
# compiler is a broken workflow, not a reason to pass.
- name: Run the e2e corpus
run: ./e2e/run.sh
env:
E2E_REQUIRE_MAKENSIS: 1
GRADLE_OPTS: -Dorg.gradle.java.installations.auto-download=true
2 changes: 1 addition & 1 deletion docs/Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ nsL is a high-level language for [NSIS](http://nsis.sourceforge.net). The nsL as

## Source Files

Just like with NSIS, one writes their installation wizard code in a plain text file with a text editor such as Notepad. For nsL, the source code files must have an “nsl” file extension. Right clicking on an nsL source code file in Windows Explorer will show the “Compile nsL Script” option. This option will run the nsL Assembler on the chosen file, which assembles the corresponding NSIS (.nsi) script. The makensisw compiler executable is then automatically run on the assembled NSIS script to build the installation wizard executable.
Just like with NSIS, one writes their installation wizard code in a plain text file with a text editor such as Notepad. For nsL, the source code files must have an “nsl” file extension. Right clicking on an nsL source code file in Windows Explorer will show the “Compile nsL Script” option. This option will run the nsL Assembler on the chosen file, which assembles the corresponding NSIS (.nsi) script. The NSIS compiler is then automatically run on the assembled NSIS script to build the installation wizard executable — `makensisw.exe` on Windows, or `makensis` from the `PATH` on other platforms.

## Syntax

Expand Down
4 changes: 4 additions & 0 deletions e2e/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Assembled and compiled output, when a script is run in place rather than in a
# scratch copy.
*.nsi
*.exe
67 changes: 67 additions & 0 deletions e2e/01-expressions.nsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Literals, types and the register pool.
*
* Everything here is about what an expression can *be* - the operators that
* combine them are 02.
*/

#include "guard.nsl"

Name("01 expressions");
OutFile("01-expressions.exe");

section Test("expressions")
{
// Every literal type.
$R0 = 42;
$R1 = "a string";
$R2 = true;
$R3 = false;
$R4 = 0x1F;
$R5 = -7;

// Registers and NSIS constants as values.
$R6 = $R0;
$R7 = $INSTDIR;
$R8 = $WINDIR;
$R9 = $PROGRAMFILES;

// A named variable, which becomes a Var rather than one of the pool.
$namedVariable = "declared by first assignment";
DetailPrint($namedVariable);

// Assemble-time conversions.
$0 = toint("255");
$1 = toint("0xFF");
$2 = toint(true);
$3 = toint($R0);
$4 = length("twelve chars");
$5 = type(42);
$6 = type("s");
$7 = type(true);
$8 = type($R0);
$9 = type($WINDIR);

DetailPrint($0." ".$1." ".$2." ".$3." ".$4);
DetailPrint($5." ".$6." ".$7." ".$8." ".$9);

/*
* Register pool pressure: enough nested sub-expressions in one statement that
* the allocator has to hand out and release several temporaries. A leak shows
* up as "out of registers" at assemble time.
*/
$R0 = 1;
$R1 = 2;
$R2 = 3;
$R3 = (($R0 + $R1) * ($R1 + $R2)) - (($R0 * $R2) + ($R1 * $R1));
DetailPrint("nested = ".$R3);

// Deeply nested concatenation of mixed operand types.
DetailPrint("int ".42." bool ".true." reg ".$R0." const ".$WINDIR." end");

// A block introduces a new scope; $blockLocal does not escape it.
{
$blockLocal = "scoped";
DetailPrint($blockLocal);
}
}
133 changes: 133 additions & 0 deletions e2e/02-operators.nsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* Every operator nsL defines, in both the assemble-time-constant and the
* run-time-register form.
*
* The two forms matter: the assembler folds an expression whose operands are
* all literals, so "9 + 9" never reaches NSIS and proves nothing about the
* emitted IntOp. Each group below therefore does the same work twice.
*/

#include "guard.nsl"

Name("02 operators");
OutFile("02-operators.exe");

section Test("operators")
{
// --- Arithmetic, folded at assemble time ---
DetailPrint("fold + = ".(9 + 4));
DetailPrint("fold - = ".(9 - 4));
DetailPrint("fold * = ".(9 * 4));
DetailPrint("fold / = ".(9 / 4));
DetailPrint("fold % = ".(9 % 4));
DetailPrint("fold | = ".(9 | 4));
DetailPrint("fold & = ".(9 & 4));
DetailPrint("fold ^ = ".(9 ^ 4));
DetailPrint("fold ~ = ".(~9));

// --- Arithmetic, emitted as IntOp ---
$R0 = 9;
$R1 = 4;
$0 = $R0 + $R1;
$1 = $R0 - $R1;
$2 = $R0 * $R1;
$3 = $R0 / $R1;
$4 = $R0 % $R1;
$5 = $R0 | $R1;
$6 = $R0 & $R1;
$7 = $R0 ^ $R1;
$8 = ~$R0;
$9 = -$R0;
DetailPrint($0." ".$1." ".$2." ".$3." ".$4);
DetailPrint($5." ".$6." ".$7." ".$8." ".$9);

// Shifts.
$0 = $R0 < 2;
$1 = $R0 > 2;
DetailPrint("shifts ".$0." ".$1);

// Precedence and explicit grouping must not agree by accident.
DetailPrint("prec = ".(2 + 3 * 4 - 6 / 2));
DetailPrint("group = ".((2 + 3) * (4 - 6) / 2));
$R2 = 2;
$R3 = 3;
$0 = $R2 + $R3 * 4 - 6 / $R2;
$1 = ($R2 + $R3) * (4 - 6) / $R2;
DetailPrint("prec = ".$0);
DetailPrint("group = ".$1);

// --- Comparison: signed, unsigned (u), case sensitive (S), insensitive (s) ---
$R0 = 5;
$R1 = 9;
$0 = $R0 == $R1;
$1 = $R0 != $R1;
$2 = $R0 >= $R1;
$3 = $R0 <= $R1;
$4 = $R1 >=u $R0;
$5 = $R1 <=u $R0;
DetailPrint($0." ".$1." ".$2." ".$3." ".$4." ".$5);

$R2 = "Text";
$0 = $R2 ==s "text";
$1 = $R2 ==S "text";
$2 = $R2 !=s "text";
$3 = $R2 !=S "text";
DetailPrint($0." ".$1." ".$2." ".$3);

// StrCmp is documented as the same thing; assert both forms assemble.
$0 = StrCmp($R2, "text");
$1 = StrCmpS($R2, "text");
DetailPrint($0." ".$1);

// --- Boolean ---
DetailPrint("fold && = ".(true && false));
DetailPrint("fold || = ".(true || false));
DetailPrint("fold ! = ".(!true));

$R0 = 1;
$R1 = 0;
$0 = $R0 > 0 && $R1 > 0;
$1 = $R0 > 0 || $R1 > 0;
$2 = !($R0 > 0);
$3 = ($R0 == 1 || $R1 > 0) && $R0 == 1 && !($R1 == 4 || $R0 == 2);
DetailPrint($0." ".$1." ".$2." ".$3);

// --- Concatenation ---
$R0 = "one";
$R1 = "two";
DetailPrint($R0.$R1);
DetailPrint("a".$R0."b".$R1."c".1.2.3);

// --- Ternary, both folded and emitted ---
$R0 = 7;
$0 = $R0 >= 0 ? $R0 : 0;
$1 = true ? "taken" : "not taken";
$2 = $R0 > 0 ? ($R0 > 5 ? "big" : "small") : "negative";
DetailPrint($0." ".$1." ".$2);
DetailPrint($R0 > 0 ? "ternary in an argument" : "no");

// --- Assignment, every compound form ---
$R0 = 64;
$R0 += 2;
$R0 -= 1;
$R0 *= 3;
$R0 /= 2;
$R0 %= 50;
$R0 |= 8;
$R0 &= 60;
$R0 ^= 5;
$R0 <<= 2;
$R0 >>= 1;
DetailPrint("compound = ".$R0);

// Increment and decrement.
$R1 = 0;
$R1++;
$R1--;
DetailPrint("incdec = ".$R1);

// Assignment inside an expression, evaluated left to right.
$R2 = 3;
$0 = ($R2 = 9) > 5 || $R2 == 3;
DetailPrint("inline assign = ".$0." ".$R2);
}
67 changes: 67 additions & 0 deletions e2e/03-strings.nsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* String literals: the three quote characters, escape sequences, the verbatim
* "@" prefix, and format().
*
* nsL diverges from NSIS here - "$\" is gone and the escapes are C-style - so
* this is one of the easier places to emit something NSIS reads differently
* from what the author wrote.
*/

#include "guard.nsl"

Name("03 strings");
OutFile("03-strings.exe");

section Test("strings")
{
// All three quote characters are interchangeable.
DetailPrint("double quoted");
DetailPrint('single quoted');
DetailPrint(`back quoted`);

// Each quote character lets the other two through untouched.
DetailPrint('contains "double" quotes');
DetailPrint("contains 'single' quotes");
DetailPrint(`contains "both" of 'them'`);

// Escape sequences.
DetailPrint("tab:\there");
DetailPrint("newline:\r\nsecond line");
DetailPrint("escaped quote: \" and backslash: \\");

// A verbatim string keeps its backslashes, which is what Windows paths want.
DetailPrint(@"C:\no\escapes\here");
DetailPrint(@'verbatim \r\n stays literal');

// The same path written both ways must mean the same thing.
$R0 = @"\a\b\c";
$R1 = "\\a\\b\\c";
DetailPrint("paths equal: ".($R0 ==S $R1));

// Variables cannot appear inside a quoted string; concatenate instead.
$R2 = "world";
DetailPrint("hello ".$R2);

// ... or use format(), which is resolved at assemble time.
DetailPrint(format("hello {0}", $R2));
DetailPrint(format("{0} then {1} then {0} again", "first", "second"));
// Doubling a brace escapes it, so the first {0} here is literal text.
DetailPrint(format("a literal brace: {{0} is not a placeholder, {0} is", "this"));
DetailPrint(format("{0}-{1}-{2}", 1, true, $WINDIR));
// A result shorter than the format string, and two adjacent placeholders:
// both used to walk the assembler's index off the string.
DetailPrint(format("{0}", 1));
DetailPrint(format("{0}{1}", "AAAAA", "BBBBB"));

// Assemble-time length of a literal, versus the run-time instruction.
DetailPrint("assemble-time length = ".length("abcdef"));
// StrLen() rejects any argument the assembler considers literal, and a plain
// register counts as one, so its operand has to be a nested instruction call.
// See KNOWN-GAPS.md.
$R3 = StrLen(ReadEnvStr("PATH"));
DetailPrint("run-time length = ".$R3);

// An empty string is a legal operand.
$R4 = "";
DetailPrint("empty is empty: ".($R4 ==S ""));
}
Loading
Loading