Repository: peggyjs/peggy
Pull request #502 (2024-03-05 08:51:13)

Soft mode
ref #501 

hildjj: Once #593 lands, can you rebase this, please?  I'll play around with it to see if it makes sense to go into the next major release.  I think there's a semver-major breaking change with existing grammars that use the custom option `soft`.

We will also need docs, but don't bother with those until this gets rebased and I try it out.
hildjj: ok, I'm ready for this to be rebased, please.
frostburn: Rebased. CI tests seem to pass even though locally I have three tests failing when running `nve latest npm test` on `main` and on this branch.
hildjj: Great.  I will take a look at this and make a crisp decision before landing #602.
hildjj: Can you remove node_modules, do `pnpm i -r` and see if your local errors go away, please?
hildjj: Overall, I'm +1 on this, but it needs a few tweaks.
hildjj: Apologies, but I just realized that this code should build on the peg$library option.  It makes sense for the results from that to include the the `fail()` function.  That would give you everything you need, I think, except for the nicer ergonomics that your approach has.
frostburn: > Can you remove node_modules, do `pnpm i -r` and see if your local errors go away, please?

They do not. I'll chalk it off to running an old Ubuntu locally.
hildjj: Can you copy over the error you're seeing into this discussion, please?
frostburn: Changed `fail()` to `peg$throw()` and used library mode for everything.
hildjj: Oh.  That's because you had a test fail, and you need to clean up the files that got left over.  Delete the files that the error says, and it it should work cleanly.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cc30187..db31868 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -28,7 +28,8 @@ Released: TBD (Not before 2025-05-01)
   moving away from ES5. [#593](https://github.com/peggyjs/peggy/pull/593)
 
 ### New features
-
+- Extend library mode to include a success flag and a function for throwing syntax errors when needed.
+  [#501](https://github.com/peggyjs/peggy/issues/501)
 - Slightly better verbose output from the CLI, showing where files are written.
   [#601](https://github.com/peggyjs/peggy/pull/601)
 - Merged class rules (rules which consist of a character class like `foo =
diff --git a/docs/js/examples.js b/docs/js/examples.js
index 72a8285..6c5e13f 100644
--- a/docs/js/examples.js
+++ b/docs/js/examples.js
@@ -1599,18 +1599,8 @@ function peg$parse(input, options) {
 
   peg$result = peg$startRuleFunction();
 
-  if (options.peg$library) {
-    return /** @type {any} */ ({
-      peg$result,
-      peg$currPos,
-      peg$FAILED,
-      peg$maxFailExpected,
-      peg$maxFailPos,
-    });
-  }
-  if (peg$result !== peg$FAILED && peg$currPos === input.length) {
-    return peg$result;
-  } else {
+  const peg$success = (peg$result !== peg$FAILED && peg$currPos === input.length);
+  function peg$throw() {
     if (peg$result !== peg$FAILED && peg$currPos < input.length) {
       peg$fail(peg$endExpectation());
     }
@@ -1623,6 +1613,22 @@ function peg$parse(input, options) {
         : peg$computeLocation(peg$maxFailPos, peg$maxFailPos)
     );
   }
+  if (options.peg$library) {
+    return /** @type {any} */ ({
+      peg$result,
+      peg$currPos,
+      peg$FAILED,
+      peg$maxFailExpected,
+      peg$maxFailPos,
+      peg$success,
+      peg$throw: peg$success ? undefined : peg$throw,
+    });
+  }
+  if (peg$success) {
+    return peg$result;
+  } else {
+    peg$throw();
+  }
 }
 
   root.peggyExamples = {
diff --git a/lib/compiler/passes/generate-js.js b/lib/compiler/passes/generate-js.js
index bf73364..18ecfb1 100644
--- a/lib/compiler/passes/generate-js.js
+++ b/lib/compiler/passes/generate-js.js
@@ -1348,19 +1348,8 @@ function generateJS(ast, options) {
     parts.push(
       "  peg$result = peg$startRuleFunction();",
       "",
-      "  if (options.peg$library) {",
-      // Hide this from TypeScript.  It's internal-only.
-      "    return /** @type {any} */ ({",
-      "      peg$result,",
-      "      peg$currPos,",
-      "      peg$FAILED,",
-      "      peg$maxFailExpected,",
-      "      peg$maxFailPos,",
-      "    });",
-      "  }",
-      "  if (peg$result !== peg$FAILED && peg$currPos === input.length) {",
-      "    return peg$result;",
-      "  } else {",
+      "  const peg$success = (peg$result !== peg$FAILED && peg$currPos === input.length);",
+      "  function peg$throw() {",
       "    if (peg$result !== peg$FAILED && peg$currPos < input.length) {",
       "      peg$fail(peg$endExpectation());",
       "    }",
@@ -1373,6 +1362,23 @@ function generateJS(ast, options) {
       "        : peg$computeLocation(peg$maxFailPos, peg$maxFailPos)",
       "    );",
       "  }",
+      "  if (options.peg$library) {",
+      // Hide this from TypeScript.  It's internal-only until library mode is stabilized.
+      "    return /** @type {any} */ ({",
+      "      peg$result,",
+      "      peg$currPos,",
+      "      peg$FAILED,",
+      "      peg$maxFailExpected,",
+      "      peg$maxFailPos,",
+      "      peg$success,",
+      "      peg$throw: peg$success ? undefined : peg$throw,",
+      "    });",
+      "  }",
+      "  if (peg$success) {",
+      "    return peg$result;",
+      "  } else {",
+      "    peg$throw();",
+      "  }",
       "}"
     );
 
diff --git a/lib/parser.js b/lib/parser.js
index 185325b..1e031a0 100644
--- a/lib/parser.js
+++ b/lib/parser.js
@@ -3856,18 +3856,8 @@ function peg$parse(input, options) {
 
   peg$result = peg$startRuleFunction();
 
-  if (options.peg$library) {
-    return /** @type {any} */ ({
-      peg$result,
-      peg$currPos,
-      peg$FAILED,
-      peg$maxFailExpected,
-      peg$maxFailPos,
-    });
-  }
-  if (peg$result !== peg$FAILED && peg$currPos === input.length) {
-    return peg$result;
-  } else {
+  const peg$success = (peg$result !== peg$FAILED && peg$currPos === input.length);
+  function peg$throw() {
     if (peg$result !== peg$FAILED && peg$currPos < input.length) {
       peg$fail(peg$endExpectation());
     }
@@ -3880,6 +3870,22 @@ function peg$parse(input, options) {
         : peg$computeLocation(peg$maxFailPos, peg$maxFailPos)
     );
   }
+  if (options.peg$library) {
+    return /** @type {any} */ ({
+      peg$result,
+      peg$currPos,
+      peg$FAILED,
+      peg$maxFailExpected,
+      peg$maxFailPos,
+      peg$success,
+      peg$throw: peg$success ? undefined : peg$throw,
+    });
+  }
+  if (peg$success) {
+    return peg$result;
+  } else {
+    peg$throw();
+  }
 }
 
 module.exports = {


Test patch:
diff --git a/test/api/generated-parser-api.spec.js b/test/api/generated-parser-api.spec.js
index ba10679..0cbf25c 100644
--- a/test/api/generated-parser-api.spec.js
+++ b/test/api/generated-parser-api.spec.js
@@ -14,12 +14,30 @@ describe("generated parser API", () => {
       expect(parser.parse("a")).to.equal("a");
     });
 
+    it("parses input in library mode", () => {
+      const parser = peg.generate("start = 'a'");
+
+      const result = parser.parse("a", { peg$library: true });
+      expect(result.peg$result).to.equal("a");
+      expect(result.peg$success).to.equal(true);
+      expect(result.peg$throw).to.equal(undefined);
+    });
+
     it("throws an exception on syntax error", () => {
       const parser = peg.generate("start = 'a'");
 
       expect(() => { parser.parse("b"); }).to.throw();
     });
 
+    it("gives partial result on syntax error in library mode", () => {
+      const parser = peg.generate("start = 'a'+");
+
+      const result = parser.parse("aab", { peg$library: true });
+      expect(result.peg$result).to.deep.equal(["a", "a"]);
+      expect(result.peg$success).to.equal(false);
+      expect(result.peg$throw).to.throw('Expected "a" or end of input but "b" found.');
+    });
+
     // Regression: https://github.com/peggyjs/peggy/pull/197
     it("correctly describe character class in syntax error", () => {
       const parser = peg.generate("start = [123-5]");
diff --git a/test/cli/fixtures/imports_peggy.js b/test/cli/fixtures/imports_peggy.js
index d302042..7e856c6 100644
--- a/test/cli/fixtures/imports_peggy.js
+++ b/test/cli/fixtures/imports_peggy.js
@@ -457,18 +457,8 @@ function peg$parse(input, options) {
 
   peg$result = peg$startRuleFunction();
 
-  if (options.peg$library) {
-    return /** @type {any} */ ({
-      peg$result,
-      peg$currPos,
-      peg$FAILED,
-      peg$maxFailExpected,
-      peg$maxFailPos,
-    });
-  }
-  if (peg$result !== peg$FAILED && peg$currPos === input.length) {
-    return peg$result;
-  } else {
+  const peg$success = (peg$result !== peg$FAILED && peg$currPos === input.length);
+  function peg$throw() {
     if (peg$result !== peg$FAILED && peg$currPos < input.length) {
       peg$fail(peg$endExpectation());
     }
@@ -481,6 +471,22 @@ function peg$parse(input, options) {
         : peg$computeLocation(peg$maxFailPos, peg$maxFailPos)
     );
   }
+  if (options.peg$library) {
+    return /** @type {any} */ ({
+      peg$result,
+      peg$currPos,
+      peg$FAILED,
+      peg$maxFailExpected,
+      peg$maxFailPos,
+      peg$success,
+      peg$throw: peg$success ? undefined : peg$throw,
+    });
+  }
+  if (peg$success) {
+    return peg$result;
+  } else {
+    peg$throw();
+  }
 }
 
 module.exports = {
diff --git a/test/cli/fixtures/lib.js b/test/cli/fixtures/lib.js
index 65cb219..992db2d 100644
--- a/test/cli/fixtures/lib.js
+++ b/test/cli/fixtures/lib.js
@@ -446,18 +446,8 @@ function peg$parse(input, options) {
 
   peg$result = peg$startRuleFunction();
 
-  if (options.peg$library) {
-    return /** @type {any} */ ({
-      peg$result,
-      peg$currPos,
-      peg$FAILED,
-      peg$maxFailExpected,
-      peg$maxFailPos,
-    });
-  }
-  if (peg$result !== peg$FAILED && peg$currPos === input.length) {
-    return peg$result;
-  } else {
+  const peg$success = (peg$result !== peg$FAILED && peg$currPos === input.length);
+  function peg$throw() {
     if (peg$result !== peg$FAILED && peg$currPos < input.length) {
       peg$fail(peg$endExpectation());
     }
@@ -470,6 +460,22 @@ function peg$parse(input, options) {
         : peg$computeLocation(peg$maxFailPos, peg$maxFailPos)
     );
   }
+  if (options.peg$library) {
+    return /** @type {any} */ ({
+      peg$result,
+      peg$currPos,
+      peg$FAILED,
+      peg$maxFailExpected,
+      peg$maxFailPos,
+      peg$success,
+      peg$throw: peg$success ? undefined : peg$throw,
+    });
+  }
+  if (peg$success) {
+    return peg$result;
+  } else {
+    peg$throw();
+  }
 }
 
 module.exports = {
diff --git a/test/cli/run.spec.ts b/test/cli/run.spec.ts
index 315f4e9..4f802f9 100644
--- a/test/cli/run.spec.ts
+++ b/test/cli/run.spec.ts
@@ -1405,7 +1405,9 @@ error: Rule "unknownRule" is not defined
   'peg$currPos': 3,
   'peg$FAILED': {},
   'peg$maxFailExpected': [],
-  'peg$maxFailPos': 0
+  'peg$maxFailPos': 0,
+  'peg$success': true,
+  'peg$throw': undefined
 }\n`,
     });
   });
diff --git a/test/unit/parser.spec.js b/test/unit/parser.spec.js
index ebbaae9..1e3f2d6 100644
--- a/test/unit/parser.spec.js
+++ b/test/unit/parser.spec.js
@@ -1454,6 +1454,8 @@ c = @'ijkl'
 
   it("handles peg$library", () => {
     const res = parser.parse("foo", { peg$library: true });
+    expect(typeof res.peg$throw).to.eql("function");
+    delete res.peg$throw;
     expect(res).to.eql({
       peg$result: {},
       peg$currPos: 0,
@@ -1466,6 +1468,7 @@ c = @'ijkl'
         { type: "literal", text: "=", ignoreCase: false },
       ],
       peg$maxFailPos: 3,
+      peg$success: false,
     });
   });
 });