import assert from "node:assert"; import { describe, test } from "node:test"; import { parse } from "../../src/compiler/parse.js"; import { undetermined } from "../../src/compiler/parserHelpers.js"; import * as ops from "../../src/runtime/ops.js"; import { stripCodeLocations } from "./stripCodeLocations.js"; describe("Origami parser", () => { test("additiveExpression", () => { assertParse("additiveExpression", "1 + 2", [ ops.addition, [ops.literal, 1], [ops.literal, 2], ]); assertParse("additiveExpression", "5 - 4", [ ops.subtraction, [ops.literal, 5], [ops.literal, 4], ]); }); test("arrayLiteral", () => { assertParse("arrayLiteral", "[]", [ops.array]); assertParse("arrayLiteral", "[1, 2, 3]", [ ops.array, [ops.literal, 1], [ops.literal, 2], [ops.literal, 3], ]); assertParse("arrayLiteral", "[ 1 , 2 , 3 ]", [ ops.array, [ops.literal, 1], [ops.literal, 2], [ops.literal, 3], ]); assertParse("arrayLiteral", "[1,,,4]", [ ops.array, [ops.literal, 1], [ops.literal, undefined], [ops.literal, undefined], [ops.literal, 4], ]); assertParse("arrayLiteral", "[ 1, ...[2, 3]]", [ ops.merge, [ops.array, [ops.literal, 1]], [ops.array, [ops.literal, 2], [ops.literal, 3]], ]); assertParse( "arrayLiteral", `[ 1 2 3 ]`, [ops.array, [ops.literal, 1], [ops.literal, 2], [ops.literal, 3]] ); }); test("arrowFunction", () => { assertParse("arrowFunction", "() => foo", [ ops.lambda, [], [ops.scope, "foo"], ]); assertParse("arrowFunction", "x => y", [ ops.lambda, ["x"], [ops.scope, "y"], ]); assertParse("arrowFunction", "(a, b, c) ⇒ fn(a, b, c)", [ ops.lambda, ["a", "b", "c"], [ [ops.builtin, "fn"], [ops.scope, "a"], [ops.scope, "b"], [ops.scope, "c"], ], ]); assertParse("arrowFunction", "a => b => fn(a, b)", [ ops.lambda, ["a"], [ ops.lambda, ["b"], [ [ops.builtin, "fn"], [ops.scope, "a"], [ops.scope, "b"], ], ], ]); }); test("bitwiseAndExpression", () => { assertParse("bitwiseAndExpression", "5 & 3", [ ops.bitwiseAnd, [ops.literal, 5], [ops.literal, 3], ]); }); test("bitwiseOrExpression", () => { assertParse("bitwiseOrExpression", "5 | 3", [ ops.bitwiseOr, [ops.literal, 5], [ops.literal, 3], ]); }); test("bitwiseXorExpression", () => { assertParse("bitwiseXorExpression", "5 ^ 3", [ ops.bitwiseXor, [ops.literal, 5], [ops.literal, 3], ]); }); test("callExpression", () => { assertParse("callExpression", "fn()", [[ops.builtin, "fn"], undefined]); assertParse("callExpression", "foo.js(arg)", [ [ops.scope, "foo.js"], [ops.scope, "arg"], ]); assertParse("callExpression", "fn(a, b)", [ [ops.builtin, "fn"], [ops.scope, "a"], [ops.scope, "b"], ]); assertParse("callExpression", "foo.js( a , b )", [ [ops.scope, "foo.js"], [ops.scope, "a"], [ops.scope, "b"], ]); assertParse("callExpression", "fn()(arg)", [ [[ops.builtin, "fn"], undefined], [ops.scope, "arg"], ]); assertParse("callExpression", "tree/", [ops.unpack, [ops.scope, "tree/"]]); assertParse("callExpression", "tree/foo/bar", [ ops.traverse, [ops.scope, "tree/"], [ops.literal, "foo/"], [ops.literal, "bar"], ]); assertParse("callExpression", "tree/foo/bar/", [ ops.traverse, [ops.scope, "tree/"], [ops.literal, "foo/"], [ops.literal, "bar/"], ]); assertParse("callExpression", "/foo/bar", [ ops.traverse, [ops.rootDirectory, [ops.literal, "foo/"]], [ops.literal, "bar"], ]); assertParse("callExpression", "foo.js()/key", [ ops.traverse, [[ops.scope, "foo.js"], undefined], [ops.literal, "key"], ]); assertParse("callExpression", "tree/key()", [ [ops.traverse, [ops.scope, "tree/"], [ops.literal, "key"]], undefined, ]); assertParse("callExpression", "(tree)/", [ops.unpack, [ops.scope, "tree"]]); assertParse("callExpression", "fn()/key()", [ [ops.traverse, [[ops.builtin, "fn"], undefined], [ops.literal, "key"]], undefined, ]); assertParse("callExpression", "(foo.js())('arg')", [ [[ops.scope, "foo.js"], undefined], [ops.literal, "arg"], ]); assertParse("callExpression", "fn('a')('b')", [ [ [ops.builtin, "fn"], [ops.literal, "a"], ], [ops.literal, "b"], ]); assertParse("callExpression", "(foo.js())(a, b)", [ [[ops.scope, "foo.js"], undefined], [ops.scope, "a"], [ops.scope, "b"], ]); assertParse("callExpression", "{ a: 1, b: 2}/b", [ ops.traverse, [ops.object, ["a", [ops.literal, 1]], ["b", [ops.literal, 2]]], [ops.literal, "b"], ]); assertParse("callExpression", "indent`hello`", [ [ops.builtin, "indent"], [ops.literal, ["hello"]], ]); assertParse("callExpression", "fn.js`Hello, world.`", [ [ops.scope, "fn.js"], [ops.literal, ["Hello, world."]], ]); assertParse("callExpression", "files:src/assets", [ ops.traverse, [ [ops.builtin, "files:"], [ops.literal, "src/"], ], [ops.literal, "assets"], ]); assertParse("callExpression", "new:(js:Date, '2025-01-01')", [ [ops.builtin, "new:"], [ [ops.builtin, "js:"], [ops.literal, "Date"], ], [ops.literal, "2025-01-01"], ]); assertParse("callExpression", "map(markdown, mdHtml)", [ [ops.builtin, "map"], [ops.scope, "markdown"], [ops.scope, "mdHtml"], ]); assertParse("callExpression", "package:@weborigami/dropbox/auth(creds)", [ [ ops.traverse, [ [ops.builtin, "package:"], [ops.literal, "@weborigami/"], ], [ops.literal, "dropbox/"], [ops.literal, "auth"], ], [ops.scope, "creds"], ]); }); test("commaExpression", () => { assertParse("commaExpression", "1", [ops.literal, 1]); assertParse("commaExpression", "a, b, c", [ ops.comma, [ops.scope, "a"], [ops.scope, "b"], [ops.scope, "c"], ]); }); test("conditionalExpression", () => { assertParse("conditionalExpression", "1", [ops.literal, 1]); assertParse("conditionalExpression", "true ? 1 : 0", [ ops.conditional, [ops.scope, "true"], [ops.lambda, [], [ops.literal, "1"]], [ops.lambda, [], [ops.literal, "0"]], ]); assertParse("conditionalExpression", "false ? () => 1 : 0", [ ops.conditional, [ops.scope, "false"], [ops.lambda, [], [ops.lambda, [], [ops.literal, "1"]]], [ops.lambda, [], [ops.literal, "0"]], ]); assertParse("conditionalExpression", "false ? =1 : 0", [ ops.conditional, [ops.scope, "false"], [ops.lambda, [], [ops.lambda, ["_"], [ops.literal, "1"]]], [ops.lambda, [], [ops.literal, "0"]], ]); }); test("equalityExpression", () => { assertParse("equalityExpression", "1 === 1", [ ops.strictEqual, [ops.literal, 1], [ops.literal, 1], ]); assertParse("equalityExpression", "a === b === c", [ ops.strictEqual, [ops.strictEqual, [undetermined, "a"], [undetermined, "b"]], [undetermined, "c"], ]); assertParse("equalityExpression", "1 !== 1", [ ops.notStrictEqual, [ops.literal, 1], [ops.literal, 1], ]); }); test("exponentiationExpression", () => { assertParse("exponentiationExpression", "2 ** 2 ** 3", [ ops.exponentiation, [ops.literal, 2], [ops.exponentiation, [ops.literal, 2], [ops.literal, 3]], ]); }); test("expression", () => { assertParse( "expression", ` { index.html = index.ori(teamData.yaml) thumbnails = map(images, { value: thumbnail.js }) } `, [ ops.object, [ "index.html", [ ops.getter, [ [ops.scope, "index.ori"], [ops.scope, "teamData.yaml"], ], ], ], [ "thumbnails", [ ops.getter, [ [ops.builtin, "map"], [ops.scope, "images"], [ops.object, ["value", [ops.scope, "thumbnail.js"]]], ], ], ], ] ); // Builtin on its own is the function itself, not a function call assertParse("expression", "mdHtml:", [ops.builtin, "mdHtml:"]); // Consecutive slahes in a path are removed assertParse("expression", "path//key", [ ops.traverse, [ops.scope, "path/"], [ops.literal, "key"], ]); // Single slash at start of something = absolute file path assertParse("expression", "/path", [ ops.rootDirectory, [ops.literal, "path"], ]); // Consecutive slashes at start of something = comment assertParse("expression", "path //comment", [ops.scope, "path"], false); assertParse("expression", "page.ori(mdHtml:(about.md))", [ [ops.scope, "page.ori"], [ [ops.builtin, "mdHtml:"], [ops.scope, "about.md"], ], ]); // Slash on its own is the root folder assertParse("expression", "keys /", [ [ops.builtin, "keys"], [ops.rootDirectory], ]); assertParse("expression", "'Hello' -> test.orit", [ [ops.scope, "test.orit"], [ops.literal, "Hello"], ]); assertParse("expression", "obj.json", [ops.scope, "obj.json"]); assertParse("expression", "(fn a, b, c)", [ [ops.builtin, "fn"], [undetermined, "a"], [undetermined, "b"], [undetermined, "c"], ]); assertParse("expression", "foo.bar('hello', 'world')", [ [ops.scope, "foo.bar"], [ops.literal, "hello"], [ops.literal, "world"], ]); assertParse("expression", "(key)('a')", [ [ops.scope, "key"], [ops.literal, "a"], ]); assertParse("expression", "1", [ops.literal, 1]); assertParse("expression", "{ a: 1, b: 2 }", [ ops.object, ["a", [ops.literal, 1]], ["b", [ops.literal, 2]], ]); assertParse("expression", "serve { index.html: 'hello' }", [ [ops.builtin, "serve"], [ops.object, ["index.html", [ops.literal, "hello"]]], ]); assertParse("expression", "fn =`x`", [ [ops.builtin, "fn"], [ops.lambda, ["_"], [ops.template, [ops.literal, ["x"]]]], ]); assertParse("expression", "copy app.js(formulas), files:snapshot", [ [ops.builtin, "copy"], [ [ops.scope, "app.js"], [ops.scope, "formulas"], ], [ [ops.builtin, "files:"], [ops.literal, "snapshot"], ], ]); assertParse("expression", "map =`
  • ${_}
  • `", [ [ops.builtin, "map"], [ ops.lambda, ["_"], [ ops.template, [ops.literal, ["
  • ", "
  • "]], [ops.concat, [ops.scope, "_"]], ], ], ]); assertParse("expression", `https://example.com/about/`, [ [ops.builtin, "https:"], [ops.literal, "example.com/"], [ops.literal, "about/"], ]); assertParse("expression", "tag`Hello, ${name}!`", [ [ops.builtin, "tag"], [ops.literal, ["Hello, ", "!"]], [ops.concat, [ops.scope, "name"]], ]); assertParse("expression", "(post, slug) => fn.js(post, slug)", [ ops.lambda, ["post", "slug"], [ [ops.scope, "fn.js"], [ops.scope, "post"], [ops.scope, "slug"], ], ]); // Verify parser treatment of identifiers containing operators assertParse("expression", "a + b", [ ops.addition, [undetermined, "a"], [undetermined, "b"], ]); assertParse("expression", "a+b", [ops.scope, "a+b"]); assertParse("expression", "a - b", [ ops.subtraction, [undetermined, "a"], [undetermined, "b"], ]); assertParse("expression", "a-b", [ops.scope, "a-b"]); assertParse("expression", "a&b", [ops.scope, "a&b"]); assertParse("expression", "a & b", [ ops.bitwiseAnd, [undetermined, "a"], [undetermined, "b"], ]); }); test("group", () => { assertParse("group", "(hello)", [ops.scope, "hello"]); assertParse("group", "(((nested)))", [ops.scope, "nested"]); assertParse("group", "(fn())", [[ops.builtin, "fn"], undefined]); assertParse("group", "(a -> b)", [ [ops.builtin, "b"], [ops.scope, "a"], ]); }); test("homeDirectory", () => { assertParse("homeDirectory", "~", [ops.homeDirectory]); }); test("host", () => { assertParse("host", "abc", [ops.literal, "abc"]); assertParse("host", "abc:123", [ops.literal, "abc:123"]); assertParse("host", "foo\\ bar", [ops.literal, "foo bar"]); }); test("identifier", () => { assertParse("identifier", "abc", "abc", false); assertParse("identifier", "index.html", "index.html", false); assertParse("identifier", "foo\\ bar", "foo bar", false); assertParse("identifier", "x-y-z", "x-y-z", false); }); test("implicitParenthesesCallExpression", () => { assertParse("implicitParenthesesCallExpression", "fn arg", [ [ops.builtin, "fn"], [undetermined, "arg"], ]); assertParse("implicitParenthesesCallExpression", "page.ori 'a', 'b'", [ [ops.scope, "page.ori"], [ops.literal, "a"], [ops.literal, "b"], ]); assertParse("implicitParenthesesCallExpression", "fn a(b), c", [ [ops.builtin, "fn"], [ [ops.builtin, "a"], [ops.scope, "b"], ], [undetermined, "c"], ]); assertParse("implicitParenthesesCallExpression", "(fn()) 'arg'", [ [[ops.builtin, "fn"], undefined], [ops.literal, "arg"], ]); assertParse("implicitParenthesesCallExpression", "tree/key arg", [ [ops.traverse, [ops.scope, "tree/"], [ops.literal, "key"]], [undetermined, "arg"], ]); assertParse("implicitParenthesesCallExpression", "foo.js bar.ori 'arg'", [ [ops.scope, "foo.js"], [ [ops.scope, "bar.ori"], [ops.literal, "arg"], ], ]); }); test("list", () => { assertParse("list", "1", [[ops.literal, 1]]); assertParse("list", "1,2,3", [ [ops.literal, 1], [ops.literal, 2], [ops.literal, 3], ]); assertParse("list", "1, 2, 3,", [ [ops.literal, 1], [ops.literal, 2], [ops.literal, 3], ]); assertParse("list", "1 , 2 , 3", [ [ops.literal, 1], [ops.literal, 2], [ops.literal, 3], ]); assertParse("list", "1\n2\n3", [ [ops.literal, 1], [ops.literal, 2], [ops.literal, 3], ]); assertParse("list", "'a' , 'b' , 'c'", [ [ops.literal, "a"], [ops.literal, "b"], [ops.literal, "c"], ]); }); test("logicalAndExpression", () => { assertParse("logicalAndExpression", "true && false", [ ops.logicalAnd, [ops.scope, "true"], [ops.lambda, [], [undetermined, "false"]], ]); }); test("logicalOrExpression", () => { assertParse("logicalOrExpression", "1 || 0", [ ops.logicalOr, [ops.literal, 1], [ops.literal, 0], ]); assertParse("logicalOrExpression", "false || false || true", [ ops.logicalOr, [ops.scope, "false"], [ops.lambda, [], [undetermined, "false"]], [ops.lambda, [], [undetermined, "true"]], ]); assertParse("logicalOrExpression", "1 || 2 && 0", [ ops.logicalOr, [ops.literal, 1], [ops.lambda, [], [ops.logicalAnd, [ops.literal, 2], [ops.literal, 0]]], ]); }); test("multiLineComment", () => { assertParse("multiLineComment", "/*\nHello, world!\n*/", null, false); }); test("multiplicativeExpression", () => { assertParse("multiplicativeExpression", "3 * 4", [ ops.multiplication, [ops.literal, 3], [ops.literal, 4], ]); assertParse("multiplicativeExpression", "5 / 2", [ ops.division, [ops.literal, 5], [ops.literal, 2], ]); assertParse("multiplicativeExpression", "6 % 5", [ ops.remainder, [ops.literal, 6], [ops.literal, 5], ]); }); test("namespace", () => { assertParse("namespace", "js:", [ops.builtin, "js:"]); }); test("nullishCoalescingExpression", () => { assertParse("nullishCoalescingExpression", "a ?? b", [ ops.nullishCoalescing, [ops.scope, "a"], [ops.lambda, [], [undetermined, "b"]], ]); assertParse("nullishCoalescingExpression", "a ?? b ?? c", [ ops.nullishCoalescing, [ops.scope, "a"], [ops.lambda, [], [undetermined, "b"]], [ops.lambda, [], [undetermined, "c"]], ]); }); test("numericLiteral", () => { assertParse("numericLiteral", "123", [ops.literal, 123]); assertParse("numericLiteral", ".5", [ops.literal, 0.5]); assertParse("numericLiteral", "123.45", [ops.literal, 123.45]); }); test("objectLiteral", () => { assertParse("objectLiteral", "{}", [ops.object]); assertParse("objectLiteral", "{ a: 1, b }", [ ops.object, ["a", [ops.literal, 1]], ["b", [ops.inherited, "b"]], ]); assertParse("objectLiteral", "{ sub: { a: 1 } }", [ ops.object, ["sub", [ops.object, ["a", [ops.literal, 1]]]], ]); assertParse("objectLiteral", "{ sub: { a/: 1 } }", [ ops.object, ["sub", [ops.object, ["a/", [ops.literal, 1]]]], ]); assertParse("objectLiteral", `{ "a": 1, "b": 2 }`, [ ops.object, ["a", [ops.literal, 1]], ["b", [ops.literal, 2]], ]); assertParse("objectLiteral", "{ a = b, b = 2 }", [ ops.object, ["a", [ops.getter, [ops.scope, "b"]]], ["b", [ops.literal, 2]], ]); assertParse( "objectLiteral", `{ a = b b = 2 }`, [ ops.object, ["a", [ops.getter, [ops.scope, "b"]]], ["b", [ops.literal, 2]], ] ); assertParse("objectLiteral", "{ a: { b: 1 } }", [ ops.object, ["a", [ops.object, ["b", [ops.literal, 1]]]], ]); assertParse("objectLiteral", "{ a: { b = 1 } }", [ ops.object, ["a", [ops.object, ["b", [ops.literal, 1]]]], ]); assertParse("objectLiteral", "{ a: { b = fn() } }", [ ops.object, [ "a/", [ops.object, ["b", [ops.getter, [[ops.builtin, "fn"], undefined]]]], ], ]); assertParse("objectLiteral", "{ x = fn.js('a') }", [ ops.object, [ "x", [ ops.getter, [ [ops.scope, "fn.js"], [ops.literal, "a"], ], ], ], ]); assertParse("objectLiteral", "{ a: 1, ...b }", [ ops.merge, [ops.object, ["a", [ops.literal, 1]]], [undetermined, "b"], ]); assertParse("objectLiteral", "{ (a): 1 }", [ ops.object, ["(a)", [ops.literal, 1]], ]); }); test("objectEntry", () => { assertParse("objectEntry", "foo", ["foo", [ops.inherited, "foo"]]); assertParse("objectEntry", "x: y", ["x", [ops.scope, "y"]]); assertParse("objectEntry", "a: a", ["a", [ops.inherited, "a"]]); assertParse("objectEntry", "a: (a) => a", [ "a", [ops.lambda, ["a"], [ops.scope, "a"]], ]); assertParse("objectEntry", "posts/: map(posts, post.ori)", [ "posts/", [ [ops.builtin, "map"], [ops.inherited, "posts"], [ops.scope, "post.ori"], ], ]); }); test("objectGetter", () => { assertParse("objectGetter", "data = obj.json", [ "data", [ops.getter, [ops.scope, "obj.json"]], ]); assertParse("objectGetter", "foo = page.ori 'bar'", [ "foo", [ ops.getter, [ [ops.scope, "page.ori"], [ops.literal, "bar"], ], ], ]); }); test("objectProperty", () => { assertParse("objectProperty", "a: 1", ["a", [ops.literal, 1]]); assertParse("objectProperty", "name: 'Alice'", [ "name", [ops.literal, "Alice"], ]); assertParse("objectProperty", "x: fn('a')", [ "x", [ [ops.builtin, "fn"], [ops.literal, "a"], ], ]); }); test("objectPublicKey", () => { assertParse("objectPublicKey", "a", "a", false); assertParse("objectPublicKey", "markdown/", "markdown/", false); assertParse("objectPublicKey", "foo\\ bar", "foo bar", false); }); test("parenthesesArguments", () => { assertParse("parenthesesArguments", "()", [undefined]); assertParse("parenthesesArguments", "(a, b, c)", [ [ops.scope, "a"], [ops.scope, "b"], [ops.scope, "c"], ]); }); test("path", () => { assertParse("path", "/tree/", [[ops.literal, "tree/"]]); assertParse("path", "/month/12", [ [ops.literal, "month/"], [ops.literal, "12"], ]); assertParse("path", "/tree/foo/bar", [ [ops.literal, "tree/"], [ops.literal, "foo/"], [ops.literal, "bar"], ]); assertParse("path", "/a///b", [ [ops.literal, "a/"], [ops.literal, "b"], ]); }); test("pathArguments", () => { assertParse("pathArguments", "/", [ops.traverse]); assertParse("pathArguments", "/tree", [ ops.traverse, [ops.literal, "tree"], ]); assertParse("pathArguments", "/tree/", [ ops.traverse, [ops.literal, "tree/"], ]); }); test("pipelineExpression", () => { assertParse("pipelineExpression", "foo", [ops.scope, "foo"]); assertParse("pipelineExpression", "a -> b", [ [ops.builtin, "b"], [ops.scope, "a"], ]); assertParse("pipelineExpression", "input → one.js → two.js", [ [ops.scope, "two.js"], [ [ops.scope, "one.js"], [ops.scope, "input"], ], ]); assertParse("pipelineExpression", "fn a -> b", [ [ops.builtin, "b"], [ [ops.builtin, "fn"], [undetermined, "a"], ], ]); }); test("primary", () => { assertParse("primary", "foo.js", [ops.scope, "foo.js"]); assertParse("primary", "[1, 2]", [ ops.array, [ops.literal, 1], [ops.literal, 2], ]); }); test("program", () => { assertParse( "program", `#!/usr/bin/env ori invoke 'Hello' `, [ops.literal, "Hello"], false ); }); test("protocolExpression", () => { assertParse("protocolExpression", "foo://bar", [ [ops.builtin, "foo:"], [ops.literal, "bar"], ]); assertParse("protocolExpression", "http://example.com", [ [ops.builtin, "http:"], [ops.literal, "example.com"], ]); assertParse("protocolExpression", "https://example.com/about/", [ [ops.builtin, "https:"], [ops.literal, "example.com/"], [ops.literal, "about/"], ]); assertParse("protocolExpression", "https://example.com/about/index.html", [ [ops.builtin, "https:"], [ops.literal, "example.com/"], [ops.literal, "about/"], [ops.literal, "index.html"], ]); assertParse("protocolExpression", "http://localhost:5000/foo", [ [ops.builtin, "http:"], [ops.literal, "localhost:5000/"], [ops.literal, "foo"], ]); }); test("qualifiedReference", () => { assertParse("qualifiedReference", "js:Date", [ [ops.builtin, "js:"], [ops.literal, "Date"], ]); }); test("relationalExpression", () => { assertParse("relationalExpression", "1 < 2", [ ops.lessThan, [ops.literal, 1], [ops.literal, 2], ]); assertParse("relationalExpression", "3 > 4", [ ops.greaterThan, [ops.literal, 3], [ops.literal, 4], ]); assertParse("relationalExpression", "5 <= 6", [ ops.lessThanOrEqual, [ops.literal, 5], [ops.literal, 6], ]); assertParse("relationalExpression", "7 >= 8", [ ops.greaterThanOrEqual, [ops.literal, 7], [ops.literal, 8], ]); }); test("rootDirectory", () => { assertParse("rootDirectory", "/", [ops.rootDirectory]); }); test("scopeReference", () => { assertParse("scopeReference", "keys", [undetermined, "keys"]); assertParse("scopeReference", "greet.js", [ops.scope, "greet.js"]); // scopeReference checks whether a slash follows; hard to test in isolation // assertParse("scopeReference", "markdown/", [ops.scope, "markdown"]); }); test("shiftExpression", () => { assertParse("shiftExpression", "1 << 2", [ ops.shiftLeft, [ops.literal, 1], [ops.literal, 2], ]); assertParse("shiftExpression", "3 >> 4", [ ops.shiftRightSigned, [ops.literal, 3], [ops.literal, 4], ]); assertParse("shiftExpression", "5 >>> 6", [ ops.shiftRightUnsigned, [ops.literal, 5], [ops.literal, 6], ]); }); test("shorthandFunction", () => { assertParse("shorthandFunction", "=message", [ ops.lambda, ["_"], [undetermined, "message"], ]); assertParse("shorthandFunction", "=`Hello, ${name}.`", [ ops.lambda, ["_"], [ ops.template, [ops.literal, ["Hello, ", "."]], [ops.concat, [ops.scope, "name"]], ], ]); assertParse("shorthandFunction", "=indent`hello`", [ ops.lambda, ["_"], [ [ops.builtin, "indent"], [ops.literal, ["hello"]], ], ]); }); test("singleLineComment", () => { assertParse("singleLineComment", "// Hello, world!", null, false); }); test("spread", () => { assertParse("spread", "...a", [ops.spread, [undetermined, "a"]]); assertParse("spread", "…a", [ops.spread, [undetermined, "a"]]); }); test("stringLiteral", () => { assertParse("stringLiteral", '"foo"', [ops.literal, "foo"]); assertParse("stringLiteral", "'bar'", [ops.literal, "bar"]); assertParse("stringLiteral", '"foo bar"', [ops.literal, "foo bar"]); assertParse("stringLiteral", "'bar baz'", [ops.literal, "bar baz"]); assertParse("stringLiteral", `"foo\\"s bar"`, [ops.literal, `foo"s bar`]); assertParse("stringLiteral", `'bar\\'s baz'`, [ops.literal, `bar's baz`]); assertParse("stringLiteral", `«string»`, [ops.literal, "string"]); assertParse("stringLiteral", `"\\0\\b\\f\\n\\r\\t\\v"`, [ ops.literal, "\0\b\f\n\r\t\v", ]); }); test("templateDocument", () => { assertParse("templateDocument", "hello${foo}world", [ ops.lambda, ["_"], [ ops.template, [ops.literal, ["hello", "world"]], [ops.concat, [ops.scope, "foo"]], ], ]); assertParse("templateDocument", "Documents can contain ` backticks", [ ops.lambda, ["_"], [ops.template, [ops.literal, ["Documents can contain ` backticks"]]], ]); }); test("templateLiteral", () => { assertParse("templateLiteral", "`Hello, world.`", [ ops.template, [ops.literal, ["Hello, world."]], ]); assertParse("templateLiteral", "`foo ${x} bar`", [ ops.template, [ops.literal, ["foo ", " bar"]], [ops.concat, [ops.scope, "x"]], ]); assertParse("templateLiteral", "`${`nested`}`", [ ops.template, [ops.literal, ["", ""]], [ops.concat, [ops.template, [ops.literal, ["nested"]]]], ]); assertParse("templateLiteral", "`${ map:(people, =`${name}`) }`", [ ops.template, [ops.literal, ["", ""]], [ ops.concat, [ [ops.builtin, "map:"], [ops.scope, "people"], [ ops.lambda, ["_"], [ ops.template, [ops.literal, ["", ""]], [ops.concat, [ops.scope, "name"]], ], ], ], ], ]); }); test("templateSubtitution", () => { assertParse("templateSubstitution", "${foo}", [ops.scope, "foo"], false); }); test("whitespace block", () => { assertParse( "__", ` // First comment // Second comment `, null, false ); }); test("unaryExpression", () => { assertParse("unaryExpression", "!true", [ ops.logicalNot, [undetermined, "true"], ]); assertParse("unaryExpression", "+1", [ops.unaryPlus, [ops.literal, 1]]); assertParse("unaryExpression", "-2", [ops.unaryMinus, [ops.literal, 2]]); assertParse("unaryExpression", "~3", [ops.bitwiseNot, [ops.literal, 3]]); }); }); function assertParse(startRule, source, expected, checkLocation = true) { const code = parse(source, { grammarSource: { text: source }, startRule, }); // Verify that the parser returned a `location` property and that it spans the // entire source. We skip this check in cases where the source starts or ends // with a comment; the parser will strip those. if (checkLocation) { assert(code.location, "no location"); const resultSource = code.location.source.text.slice( code.location.start.offset, code.location.end.offset ); assert.equal(resultSource, source.trim()); } const actual = stripCodeLocations(code); assert.deepEqual(actual, expected); }