diff --git a/CHANGELOG.md b/CHANGELOG.md index a416f36..6f40c20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ ## [Unreleased] +## [0.9.0] - 2024-01-16 +### Changed +- Add source type parameter to `Parser`. + ## [0.8.0] - 2023-06-24 ### Added - Added `Parser.prototype.apply` diff --git a/examples/__snapshots__/script.test.ts.snap b/examples/__snapshots__/script.test.ts.snap index 509b9e6..75ffd7e 100644 --- a/examples/__snapshots__/script.test.ts.snap +++ b/examples/__snapshots__/script.test.ts.snap @@ -3,14 +3,14 @@ exports[`script 1`] = ` { "last": null, - "stats": [ + "stmts": [ { "body": { "last": { "elements": [], "type": "Tuple", }, - "stats": [ + "stmts": [ { "expr": { "arguments": [ diff --git a/examples/json.test.ts b/examples/json.test.ts index 9d092e9..f499715 100644 --- a/examples/json.test.ts +++ b/examples/json.test.ts @@ -20,11 +20,8 @@ describe("JSON", () => { }); }); - test("number parsing to fail..", () => { - expect(jsonParser.parse("00")).toHaveProperty("success", false); - expect(jsonParser.parse("- 0")).toHaveProperty("success", false); - expect(jsonParser.parse("0.")).toHaveProperty("success", false); - expect(jsonParser.parse(".0")).toHaveProperty("success", false); + test.each(["00", "- 0", "0.", ".0"])("%o is invalid json.", n => { + expect(jsonParser.parse(n)).toHaveProperty("success", false); }); test.each([ diff --git a/examples/s-expression.test.ts b/examples/s-expression.test.ts new file mode 100644 index 0000000..691b2d4 --- /dev/null +++ b/examples/s-expression.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, test } from "@jest/globals"; +import { EOI } from "../src"; +import { SExpression } from "./s-expression"; + +describe("S-expression", () => { + test("Hello world!", () => { + const source = '(print "Hello world!")'; + expect(SExpression.skip(EOI).parse(source)).toEqual({ + success: true, + index: expect.any(Number), + value: ["print", '"Hello world!"'], + }); + }); + test("quote list", () => { + const source = "'(1 2 3 4)"; + expect(SExpression.skip(EOI).parse(source)).toEqual({ + success: true, + index: expect.any(Number), + value: ["quote", "1", "2", "3", "4"], + }); + }); +}); diff --git a/examples/s-expression.ts b/examples/s-expression.ts new file mode 100644 index 0000000..5c1a950 --- /dev/null +++ b/examples/s-expression.ts @@ -0,0 +1,16 @@ +import { type Parser, choice, el, lazy, many, regex } from "../src"; + +export type SExpression = string | readonly SExpression[]; + +const list = lazy(() => SExpression) + .apply(many) + .between(el("("), el(")")); + +export const SExpression: Parser = choice([ + el("'") + .option() + .and(list) + .map(([quote, list]) => (quote ? ["quote", ...list] : list)), + regex(/"([^"\\]|\\.)*"/), + regex(/[^\s()"]+/), +]).between(regex(/\s*/)); diff --git a/examples/script.test.ts b/examples/script.test.ts index fee9100..2ef6b09 100644 --- a/examples/script.test.ts +++ b/examples/script.test.ts @@ -1,9 +1,9 @@ import { describe, expect, test } from "@jest/globals"; -import { expr, stat } from "./script"; +import { expr, stmt } from "./script"; -describe("stat", () => { +describe("stmt", () => { test("Let", () => { - const result = stat.parse("let hoge = 0;"); + const result = stmt.parse("let hoge = 0;"); expect(result.success && result.value).toEqual({ type: "Let", name: "hoge", @@ -12,17 +12,17 @@ describe("stat", () => { }); test("DefFn", () => { - const result = stat.parse("fn main(arg) {};"); + const result = stmt.parse("fn main(arg) {};"); expect(result.success && result.value).toEqual({ type: "DefFn", name: "main", params: ["arg"], - body: { type: "Block", stats: [], last: null }, + body: { type: "Block", stmts: [], last: null }, }); }); test("Return", () => { - const result = stat.parse("return;"); + const result = stmt.parse("return;"); expect(result.success && result.value).toEqual({ type: "Return", body: null, @@ -30,23 +30,23 @@ describe("stat", () => { }); test("While", () => { - const result = stat.parse("while (false) {};"); + const result = stmt.parse("while (false) {};"); expect(result.success && result.value).toEqual({ type: "While", test: { type: "Bool", value: false }, - body: { type: "Block", stats: [], last: null }, + body: { type: "Block", stmts: [], last: null }, }); }); test("Break", () => { - const result = stat.parse("break;"); + const result = stmt.parse("break;"); expect(result.success && result.value).toEqual({ type: "Break", }); }); test("Expr", () => { - const result = stat.parse("0;"); + const result = stmt.parse("0;"); expect(result.success && result.value).toEqual({ type: "Expr", expr: { type: "Number", value: 0 }, @@ -127,15 +127,15 @@ describe("expr", () => { const result = expr.parse("{}"); expect(result.success && result.value).toEqual({ type: "Block", - stats: [], + stmts: [], last: null, }); }); - test("stats", () => { + test("stmts", () => { const result = expr.parse("{ 0; }"); expect(result.success && result.value).toEqual({ type: "Block", - stats: [{ type: "Expr", expr: { type: "Number", value: 0 } }], + stmts: [{ type: "Expr", expr: { type: "Number", value: 0 } }], last: null, }); }); @@ -143,7 +143,7 @@ describe("expr", () => { const result = expr.parse("{ 0 }"); expect(result.success && result.value).toEqual({ type: "Block", - stats: [], + stmts: [], last: { type: "Number", value: 0 }, }); }); @@ -155,7 +155,7 @@ describe("expr", () => { expect(result.success && result.value).toEqual({ type: "If", test: { type: "Bool", value: true }, - then: { type: "Block", stats: [], last: null }, + then: { type: "Block", stmts: [], last: null }, else: null, }); }); diff --git a/examples/script.ts b/examples/script.ts index 6099d05..8df6d6e 100644 --- a/examples/script.ts +++ b/examples/script.ts @@ -5,7 +5,7 @@ export type Expr = | { type: "Number"; value: number } | { type: "String"; value: string } | { type: "Tuple"; elements: readonly Expr[] } - | { type: "Block"; stats: Stat[]; last: Expr | null } + | { type: "Block"; stmts: Stmt[]; last: Expr | null } | { type: "If"; test: Expr; then: Expr; else: Expr | null } | { type: "Ident"; name: string } | { type: "Call"; callee: Expr; arguments: readonly Expr[] } @@ -39,7 +39,7 @@ const keyword = (keyword: string): P.Parser => { const Ident = P.regex(/\w+/).map(name => ({ type: "Ident", name }) satisfies Expr); -export type Stat = +export type Stmt = | { type: "Let"; name: string; init: Expr } | { type: "DefFn"; name: string; params: readonly string[]; body: Expr } | { type: "Return"; body: Expr | null } @@ -50,7 +50,7 @@ export type Stat = const Let = keyword("let") .then(Ident.between(ws)) .skip(P.el("=")) - .andMap(expr, ({ name }, init): Stat => ({ type: "Let", name, init })); + .andMap(expr, ({ name }, init): Stmt => ({ type: "Let", name, init })); const DefFn = P.seq([ keyword("fn").then(Ident.between(ws)), @@ -60,7 +60,7 @@ const DefFn = P.seq([ .between(P.el("("), P.el(")")) .map(nodes => nodes.map(node => node.name)), expr, -]).map(([{ name }, params, body]) => ({ +]).map(([{ name }, params, body]) => ({ type: "DefFn", name, params, @@ -70,18 +70,18 @@ const DefFn = P.seq([ const Return = keyword("return") .then(expr.option(null)) .skip(ws) - .map(body => ({ type: "Return", body })); + .map(body => ({ type: "Return", body })); const While = keyword("while") .skip(ws) .then(expr.between(P.el("("), P.el(")"))) - .andMap(expr, (test, body): Stat => ({ type: "While", test, body })); + .andMap(expr, (test, body): Stmt => ({ type: "While", test, body })); -const Break = keyword("break").return({ type: "Break" }).skip(ws); +const Break = keyword("break").return({ type: "Break" }).skip(ws); -const Expr = expr.map(expr => ({ type: "Expr", expr })); +const Expr = expr.map(expr => ({ type: "Expr", expr })); -export const stat: P.Parser = P.choice([ +export const stmt: P.Parser = P.choice([ Let, DefFn, Return, @@ -126,10 +126,10 @@ const Tuple = expr .between(P.el("("), P.el(")")) .map(elements => ({ type: "Tuple", elements }) satisfies Expr); -const Block = stat +const Block = stmt .apply(P.many) - .andMap(expr.option(null), (stats, last) => { - return { type: "Block", stats, last } satisfies Expr; + .andMap(expr.option(null), (stmts, last) => { + return { type: "Block", stmts, last } satisfies Expr; }) .skip(ws) .between(P.el("{"), P.el("}")); diff --git a/package-lock.json b/package-lock.json index 62e8aa3..180e0a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parsea", - "version": "0.8.0", + "version": "0.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "parsea", - "version": "0.8.0", + "version": "0.9.0", "license": "MIT", "dependencies": { "emnorst": "^1.0.0-next.2" diff --git a/package.json b/package.json index 2ccf051..2733bc4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parsea", - "version": "0.8.0", + "version": "0.9.0", "description": "parser combinator library for parsing ArrayLike.", "type": "module", "main": "dist/index.cjs", diff --git a/src/combinator.ts b/src/combinator.ts index 3d7eac2..1319d60 100644 --- a/src/combinator.ts +++ b/src/combinator.ts @@ -7,13 +7,11 @@ import { type ParseState, updateState } from "./state"; * Delays variable references until the parser runs. */ export const lazy = (getParser: () => Parser): Parser => { - let parser: Parser; - return new Parser((state, context) => { - if (parser == null) { - parser = getParser(); - } - return parser.run(state, context); + const lazyParser: Parser = new Parser((state, context) => { + // @ts-expect-error readonly + return (lazyParser.run = getParser().run)(state, context); }); + return lazyParser; }; export const notFollowedBy = (parser: Parser): Parser => diff --git a/src/index.ts b/src/index.ts index f1b3713..a342e4f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ export * from "./combinator"; export * from "./do"; -export type { Config, ParseResult, Parsed, Parser } from "./internal"; +export type { Config, ParseResult, Parsed, Parser, Source } from "./internal"; export * from "./primitive"; export * from "./string"; diff --git a/src/string.ts b/src/string.ts index d5e8fa5..e20b139 100644 --- a/src/string.ts +++ b/src/string.ts @@ -110,14 +110,19 @@ export const ANY_CHAR = /* @__PURE__ */ new Parser((state, conte }); export const regexGroup = (re: RegExp): Parser => { - const fixedRegex = new RegExp(`^(?:${re.source})`, re.flags.replace("g", "")); + let flags = re.flags.replace("g", ""); + if (!re.sticky) { + flags += "y"; + } + const fixedRegex = new RegExp(re, flags); return new Parser((state, context) => { if (typeof context.src !== "string") { context.addError(state.i); return null; } - const matchResult = fixedRegex.exec(context.src.slice(state.i)); + fixedRegex.lastIndex = state.i; + const matchResult = fixedRegex.exec(context.src); if (matchResult === null) { context.addError(state.i); return null; @@ -135,10 +140,11 @@ export const regex: { defaultValue: T, ): Parser; } = (re: RegExp, groupId: number | string = 0, defaultValue?: undefined) => - regexGroup(re).map( - matchResult => - (typeof groupId === "number" + regexGroup(re).map(matchResult => { + const groupValue = + typeof groupId === "number" ? matchResult[groupId] : // biome-ignore lint/style/noNonNullAssertion: overrideのため - matchResult.groups?.[groupId]!) ?? defaultValue, - ); + matchResult.groups?.[groupId]!; + return groupValue ?? defaultValue; + });