Compare commits

..

10 Commits

Author SHA1 Message Date
xenia eb49898a0a create package.nix 2026-07-21 13:29:39 -04:00
xenia 5c43aaa6ef the dragon girls take over: part 2 2026-07-21 13:26:17 -04:00
xenia 1ea4cadd3c correct UTF-8 surrogate range check 2026-07-21 13:18:40 -04:00
Hugo Heuzard 667c8d0945 Use default flags 2026-07-21 13:17:01 -04:00
Hugo Heuzard 409fc80d40 Document longest match behavior 2026-07-21 13:13:11 -04:00
xenia 1b2ea6e119 update opam file 2026-07-21 13:12:36 -04:00
Hugo Heuzard 7606e062aa Refactoring, new type for dfa_state and dfa 2026-07-21 13:11:47 -04:00
Hugo Heuzard 50488309ed Tests: output automata using dot syntax for easier review 2026-07-21 13:11:24 -04:00
Hugo Heuzard e1467df71b Tests: add codegen tests 2026-07-21 13:10:51 -04:00
Hugo Heuzard 15df9eb34f Tests: add a %sedlex_test ppx to help write tests 2026-07-21 13:09:48 -04:00
20 changed files with 394 additions and 111 deletions

View File

@ -79,6 +79,19 @@ where:
Unlike ocamllex, lexers work on stream of Unicode codepoints, not Unlike ocamllex, lexers work on stream of Unicode codepoints, not
bytes. bytes.
Like ocamllex, sedlex uses **longest match** with **first rule priority**:
- The lexer always tries to match the longest possible prefix of the
input. It does so by continuing to read characters as long as some
rule can still match a longer string, while remembering the last
position at which a rule did match.
- When two or more rules match the same longest prefix (a tie), the
rule that appears first in the `match%sedlex` definition wins. For
example, given the rules `| "if" -> ...` and `| Plus ('a'..'z') -> ...`,
the input `"if"` is matched by the first rule because it is listed
first, even though the second rule also accepts `"if"`.
The actions can call functions from the Sedlexing module to extract The actions can call functions from the Sedlexing module to extract
(parts of) the matched lexeme, in the desired encoding. (parts of) the matched lexeme, in the desired encoding.

View File

@ -8,6 +8,8 @@
(authors "xenia <xenia@awoo.systems>") (authors "xenia <xenia@awoo.systems>")
(maintainers "xenia <xenia@awoo.systems") (maintainers "xenia <xenia@awoo.systems")
(homepage "https://git.lain.faith/noslop/noslop-sedlex.git") (homepage "https://git.lain.faith/noslop/noslop-sedlex.git")
(maintenance_intent "(latest)")
(documentation "https://git.lain.faith/noslop/noslop-sedlex/wiki")
(generate_opam_files true) (generate_opam_files true)
(executables_implicit_empty_intf true) (executables_implicit_empty_intf true)

View File

@ -2,8 +2,7 @@
(names tokenizer regressions complement subtraction repeat performance) (names tokenizer regressions complement subtraction repeat performance)
(libraries noslop-sedlex noslop-sedlex.ppx) (libraries noslop-sedlex noslop-sedlex.ppx)
(preprocess (preprocess
(pps noslop-sedlex.ppx)) (pps noslop-sedlex.ppx)))
(flags :standard -w +39))
(rule (rule
(alias runtest) (alias runtest)

View File

@ -2,22 +2,7 @@
description = "Flake for noslop-sedlex"; description = "Flake for noslop-sedlex";
outputs = { self, dragnpkgs } @ inputs: dragnpkgs.lib.mkFlake { outputs = { self, dragnpkgs } @ inputs: dragnpkgs.lib.mkFlake {
packages.default = { ocamlPackages }: ocamlPackages.buildDunePackage rec { packages.default = { ocamlPackages }: ocamlPackages.callPackage ./package.nix {};
pname = "noslop-sedlex";
version = "3.7+DEV";
src = self;
minimalOCamlVersion = "5.3";
propagatedBuildInputs = with ocamlPackages; [
gen
ppxlib
ppx_expect
];
nativeBuildInputs = with ocamlPackages; [
];
};
devShells.default = { ocamlPackages, stdenv, mkShell }: mkShell { devShells.default = { ocamlPackages, stdenv, mkShell }: mkShell {
packages = with ocamlPackages; [ packages = with ocamlPackages; [

View File

@ -11,6 +11,7 @@ maintainer: ["xenia <xenia@awoo.systems"]
authors: ["xenia <xenia@awoo.systems>"] authors: ["xenia <xenia@awoo.systems>"]
license: "LicenseRef-Proprietary" license: "LicenseRef-Proprietary"
homepage: "https://git.lain.faith/noslop/noslop-sedlex.git" homepage: "https://git.lain.faith/noslop/noslop-sedlex.git"
doc: "https://git.lain.faith/noslop/noslop-sedlex/wiki"
depends: [ depends: [
"ocaml" {>= "4.08"} "ocaml" {>= "4.08"}
"dune" {>= "3.20"} "dune" {>= "3.20"}

54
package.nix Normal file
View File

@ -0,0 +1,54 @@
{
lib,
fetchurl,
buildDunePackage,
gen,
ppxlib,
uchar,
ppx_expect,
}: let
unicodeVersion = "17.0.0";
baseUrl = "https://www.unicode.org/Public/${unicodeVersion}";
DerivedCoreProperties = fetchurl {
url = "${baseUrl}/ucd/DerivedCoreProperties.txt";
hash = "sha256-JMf+0RlcSC+q79XB5+uCHF7h+23gfs26pktWqZ2iLAg=";
};
DerivedGeneralCategory = fetchurl {
url = "${baseUrl}/ucd/extracted/DerivedGeneralCategory.txt";
hash = "sha256-1i5bq3DKdPCZND9xIk+gUcsf3WGhq0XASIxEz8C2EC4=";
};
PropList = fetchurl {
url = "${baseUrl}/ucd/PropList.txt";
hash = "sha256-Ew3N3Kra8HEAi9/OHndD4E/fvJEIhvAX2fmskx2MZN0=";
};
in
buildDunePackage (finalAttrs: {
pname = "noslop-sedlex";
version = "3.7+DEV";
minimalOCamlVersion = "5.3";
src = ./.;
propagatedBuildInputs = [
gen
ppxlib
uchar
];
preBuild = ''
rm src/generator/data/dune
ln -s ${DerivedCoreProperties} src/generator/data/DerivedCoreProperties.txt
ln -s ${DerivedGeneralCategory} src/generator/data/DerivedGeneralCategory.txt
ln -s ${PropList} src/generator/data/PropList.txt
'';
checkInputs = [
ppx_expect
];
doCheck = true;
dontStrip = true;
})

View File

@ -1,6 +1,5 @@
(* The package sedlex is released under the terms of an MIT-like license. *)
(* See the attached LICENSE file. *)
(* Copyright 2005, 2013 by Alain Frisch and LexiFi. *) (* Copyright 2005, 2013 by Alain Frisch and LexiFi. *)
(* Copyright 2026 by xenia <xenia@awoo.systems> *)
(* Character sets are represented as lists of intervals. The (* Character sets are represented as lists of intervals. The
intervals must be non-overlapping and not collapsable, and the list intervals must be non-overlapping and not collapsable, and the list

View File

@ -1,6 +1,5 @@
(* The package sedlex is released under the terms of an MIT-like license. *)
(* See the attached LICENSE file. *)
(* Copyright 2005, 2013 by Alain Frisch and LexiFi. *) (* Copyright 2005, 2013 by Alain Frisch and LexiFi. *)
(* Copyright 2026 by xenia <xenia@awoo.systems> *)
(** Representation of sets of unicode code points. *) (** Representation of sets of unicode code points. *)

View File

@ -2,5 +2,4 @@
(name sedlex) (name sedlex)
(public_name noslop-sedlex) (public_name noslop-sedlex)
(wrapped false) (wrapped false)
(libraries gen) (libraries gen))
(flags :standard -w +A-4-9 -safe-string))

View File

@ -1,6 +1,5 @@
(* The package sedlex is released under the terms of an MIT-like license. *)
(* See the attached LICENSE file. *)
(* Copyright 2005, 2013 by Alain Frisch and LexiFi. *) (* Copyright 2005, 2013 by Alain Frisch and LexiFi. *)
(* Copyright 2026 by xenia <xenia@awoo.systems> *)
exception InvalidCodepoint of int exception InvalidCodepoint of int
exception MalFormed exception MalFormed
@ -455,7 +454,7 @@ module Utf8 = struct
let p = let p =
((n1 land 0x0f) lsl 12) lor ((n2 land 0x3f) lsl 6) lor (n3 land 0x3f) ((n1 land 0x0f) lsl 12) lor ((n2 land 0x3f) lsl 6) lor (n3 land 0x3f)
in in
if p >= 0xd800 && p <= 0xdf00 then raise MalFormed; if p >= 0xd800 && p <= 0xdfff then raise MalFormed;
p p
let check_four n1 n2 n3 n4 = let check_four n1 n2 n3 n4 =

View File

@ -1,6 +1,5 @@
(* The package sedlex is released under the terms of an MIT-like license. *)
(* See the attached LICENSE file. *)
(* Copyright 2005, 2013 by Alain Frisch and LexiFi. *) (* Copyright 2005, 2013 by Alain Frisch and LexiFi. *)
(* Copyright 2026 by xenia <xenia@awoo.systems> *)
(** Runtime support for lexers generated by [sedlex]. *) (** Runtime support for lexers generated by [sedlex]. *)

View File

@ -5,9 +5,7 @@
(libraries ppxlib noslop-sedlex noslop-sedlex.utils) (libraries ppxlib noslop-sedlex noslop-sedlex.utils)
(ppx_runtime_libraries noslop-sedlex) (ppx_runtime_libraries noslop-sedlex)
(preprocess (preprocess
(pps ppxlib.metaquot)) (pps ppxlib.metaquot)))
(flags
(:standard -w -9)))
(rule (rule
(targets unicode.ml) (targets unicode.ml)

View File

@ -1,6 +1,5 @@
(* The package sedlex is released under the terms of an MIT-like license. *)
(* See the attached LICENSE file. *)
(* Copyright 2005, 2013 by Alain Frisch and LexiFi. *) (* Copyright 2005, 2013 by Alain Frisch and LexiFi. *)
(* Copyright 2026 by xenia <xenia@awoo.systems> *)
open Ppxlib open Ppxlib
open Ast_builder.Default open Ast_builder.Default
@ -198,14 +197,14 @@ let best_final final =
let state_fun state = Printf.sprintf "__sedlex_state_%i" state let state_fun state = Printf.sprintf "__sedlex_state_%i" state
let call_state lexbuf auto state = let call_state lexbuf auto state =
let trans, final = auto.(state) in let { Sedlex.trans; finals } = auto.(state) in
if Array.length trans = 0 then ( if Array.length trans = 0 then (
match best_final final with match best_final finals with
| Some i -> eint ~loc:default_loc i | Some i -> eint ~loc:default_loc i
| None -> assert false) | None -> assert false)
else appfun (state_fun state) [lexbuf] else appfun (state_fun state) [lexbuf]
let gen_state (lexbuf_name, lexbuf) auto i (trans, final) = let gen_state (lexbuf_name, lexbuf) auto i { Sedlex.trans; finals } =
let loc = default_loc in let loc = default_loc in
let partition = Array.map fst trans in let partition = Array.map fst trans in
let cases = let cases =
@ -235,7 +234,7 @@ let gen_state (lexbuf_name, lexbuf) auto i (trans, final) =
~expr:(Exp.fun_ ~loc Nolabel None lhs body); ~expr:(Exp.fun_ ~loc Nolabel None lhs body);
] ]
in in
match best_final final with match best_final finals with
| None -> ret (body ()) | None -> ret (body ())
| Some _ when Array.length trans = 0 -> [] | Some _ when Array.length trans = 0 -> []
| Some i -> | Some i ->
@ -249,25 +248,19 @@ let gen_recflag auto =
in states with no further transitions. *) in states with no further transitions. *)
try try
Array.iter Array.iter
(fun (trans_i, _) -> (fun { Sedlex.trans; _ } ->
Array.iter Array.iter
(fun (_, j) -> (fun (_, j) ->
let trans_j, _ = auto.(j) in if Array.length auto.(j).Sedlex.trans > 0 then raise Exit)
if Array.length trans_j > 0 then raise Exit) trans)
trans_i)
auto; auto;
Nonrecursive Nonrecursive
with Exit -> Recursive with Exit -> Recursive
let gen_definition ((_, lexbuf) as lexbuf_with_name) l error = let gen_definition ((_, lexbuf) as lexbuf_with_name) auto l error =
let loc = default_loc in let loc = default_loc in
let brs = Array.of_list l in
let auto = Sedlex.compile (Array.map fst brs) in
let cases = let cases =
Array.to_list List.mapi (fun i (_, e) -> case ~lhs:(pint ~loc i) ~guard:None ~rhs:e) l
(Array.mapi
(fun i (_, e) -> case ~lhs:(pint ~loc i) ~guard:None ~rhs:e)
brs)
in in
let states = Array.mapi (gen_state lexbuf_with_name auto) auto in let states = Array.mapi (gen_state lexbuf_with_name auto) auto in
let states = List.flatten (Array.to_list states) in let states = List.flatten (Array.to_list states) in
@ -332,21 +325,21 @@ let rec repeat r = function
| n, m -> Sedlex.seq r (repeat r (n - 1, m - 1)) | n, m -> Sedlex.seq r (repeat r (n - 1, m - 1))
let regexp_of_pattern env = let regexp_of_pattern env =
let rec char_pair_op func name ~encoding p tuple = let rec char_pair_op func name ~encoding ~loc tuple =
(* Construct something like Sub(a,b) *) (* Construct something like Sub(a,b) *)
match tuple with match tuple with
| Some { ppat_desc = Ppat_tuple [p0; p1] } -> | Some { ppat_desc = Ppat_tuple [p0; p1]; _ } ->
begin match func (aux ~encoding p0) (aux ~encoding p1) with begin match func (aux ~encoding p0) (aux ~encoding p1) with
| Some r -> r | Some r -> r
| None -> | None ->
err p.ppat_loc err loc
"the %s operator can only applied to single-character length \ "the %s operator can only applied to single-character length \
regexps" regexps"
name name
end end
| _ -> | _ ->
err p.ppat_loc "the %s operator requires two arguments, like %s(a,b)" err loc "the %s operator requires two arguments, like %s(a,b)" name
name name name
and aux ~encoding p = and aux ~encoding p =
(* interpret one pattern node *) (* interpret one pattern node *)
match p.ppat_desc with match p.ppat_desc with
@ -355,18 +348,18 @@ let regexp_of_pattern env =
List.fold_left List.fold_left
(fun r p -> Sedlex.seq r (aux ~encoding p)) (fun r p -> Sedlex.seq r (aux ~encoding p))
(aux ~encoding p) pl (aux ~encoding p) pl
| Ppat_construct ({ txt = Lident "Star" }, Some (_, p)) -> | Ppat_construct ({ txt = Lident "Star"; _ }, Some (_, p)) ->
Sedlex.rep (aux ~encoding p) Sedlex.rep (aux ~encoding p)
| Ppat_construct ({ txt = Lident "Plus" }, Some (_, p)) -> | Ppat_construct ({ txt = Lident "Plus"; _ }, Some (_, p)) ->
Sedlex.plus (aux ~encoding p) Sedlex.plus (aux ~encoding p)
| Ppat_construct ({ txt = Lident "Utf8" }, Some (_, p)) -> | Ppat_construct ({ txt = Lident "Utf8"; _ }, Some (_, p)) ->
aux ~encoding:Utf8 p aux ~encoding:Utf8 p
| Ppat_construct ({ txt = Lident "Latin1" }, Some (_, p)) -> | Ppat_construct ({ txt = Lident "Latin1"; _ }, Some (_, p)) ->
aux ~encoding:Latin1 p aux ~encoding:Latin1 p
| Ppat_construct ({ txt = Lident "Ascii" }, Some (_, p)) -> | Ppat_construct ({ txt = Lident "Ascii"; _ }, Some (_, p)) ->
aux ~encoding:Ascii p aux ~encoding:Ascii p
| Ppat_construct | Ppat_construct
( { txt = Lident "Rep" }, ( { txt = Lident "Rep"; _ },
Some Some
( _, ( _,
{ {
@ -377,8 +370,10 @@ let regexp_of_pattern env =
{ {
ppat_desc = ppat_desc =
Ppat_constant (i1 as i2) | Ppat_interval (i1, i2); Ppat_constant (i1 as i2) | Ppat_interval (i1, i2);
_;
}; };
]; ];
_;
} ) ) -> } ) ) ->
begin match (i1, i2) with begin match (i1, i2) with
| Pconst_integer (i1, _), Pconst_integer (i2, _) -> | Pconst_integer (i1, _), Pconst_integer (i2, _) ->
@ -389,11 +384,11 @@ let regexp_of_pattern env =
| _ -> | _ ->
err p.ppat_loc "Rep must take an integer constant or interval" err p.ppat_loc "Rep must take an integer constant or interval"
end end
| Ppat_construct ({ txt = Lident "Rep" }, _) -> | Ppat_construct ({ txt = Lident "Rep"; _ }, _) ->
err p.ppat_loc "the Rep operator takes 2 arguments" err p.ppat_loc "the Rep operator takes 2 arguments"
| Ppat_construct ({ txt = Lident "Opt" }, Some (_, p)) -> | Ppat_construct ({ txt = Lident "Opt"; _ }, Some (_, p)) ->
Sedlex.alt Sedlex.eps (aux ~encoding p) Sedlex.alt Sedlex.eps (aux ~encoding p)
| Ppat_construct ({ txt = Lident "Compl" }, arg) -> | Ppat_construct ({ txt = Lident "Compl"; _ }, arg) ->
begin match arg with begin match arg with
| Some (_, p0) -> | Some (_, p0) ->
begin match Sedlex.compl (aux ~encoding p0) with begin match Sedlex.compl (aux ~encoding p0) with
@ -405,16 +400,16 @@ let regexp_of_pattern env =
end end
| _ -> err p.ppat_loc "the Compl operator requires an argument" | _ -> err p.ppat_loc "the Compl operator requires an argument"
end end
| Ppat_construct ({ txt = Lident "Sub" }, arg) -> | Ppat_construct ({ txt = Lident "Sub"; _ }, arg) ->
char_pair_op ~encoding Sedlex.subtract "Sub" p char_pair_op ~encoding Sedlex.subtract "Sub" ~loc:p.ppat_loc
(Option.map (fun (_, arg) -> arg) arg) (Option.map (fun (_, arg) -> arg) arg)
| Ppat_construct ({ txt = Lident "Intersect" }, arg) -> | Ppat_construct ({ txt = Lident "Intersect"; _ }, arg) ->
char_pair_op ~encoding Sedlex.intersection "Intersect" p char_pair_op ~encoding Sedlex.intersection "Intersect" ~loc:p.ppat_loc
(Option.map (fun (_, arg) -> arg) arg) (Option.map (fun (_, arg) -> arg) arg)
| Ppat_construct ({ txt = Lident "Chars" }, arg) -> ( | Ppat_construct ({ txt = Lident "Chars"; _ }, arg) -> (
let const = let const =
match arg with match arg with
| Some (_, { ppat_desc = Ppat_constant const }) -> Some const | Some (_, { ppat_desc = Ppat_constant const; _ }) -> Some const
| _ -> None | _ -> None
in in
match const with match const with
@ -459,7 +454,7 @@ let regexp_of_pattern env =
Sedlex.chars (Cset.singleton (codepoint (int_of_string i))) Sedlex.chars (Cset.singleton (codepoint (int_of_string i)))
| _ -> err p.ppat_loc "this pattern is not a valid regexp" | _ -> err p.ppat_loc "this pattern is not a valid regexp"
end end
| Ppat_var { txt = x } -> | Ppat_var { txt = x; _ } ->
begin try StringMap.find x env begin try StringMap.find x env
with Not_found -> err p.ppat_loc "unbound regexp %s" x with Not_found -> err p.ppat_loc "unbound regexp %s" x
end end
@ -467,6 +462,46 @@ let regexp_of_pattern env =
in in
aux ~encoding:Ascii aux ~encoding:Ascii
let handle_sedlex_match ~env ~map_rhs match_expr =
let lexbuf =
match match_expr with
| { pexp_desc = Pexp_match (lexbuf, _); _ } -> (
match lexbuf with
| { pexp_desc = Pexp_ident { txt = Lident txt; _ }; _ } ->
(txt, lexbuf)
| _ ->
err lexbuf.pexp_loc
"the matched expression must be a single identifier")
| _ ->
err match_expr.pexp_loc
"the %%sedlex extension is only recognized on match expressions"
in
let cases =
match match_expr with
| { pexp_desc = Pexp_match (_, cases); _ } -> cases
| _ -> assert false
in
let cases = List.rev cases in
let error =
match List.hd cases with
| { pc_lhs = [%pat? _]; pc_rhs = e; pc_guard = None } -> map_rhs e
| { pc_lhs = p; _ } ->
err p.ppat_loc "the last branch must be a catch-all error case"
in
let cases = List.rev (List.tl cases) in
let cases =
List.map
(function
| { pc_lhs = p; pc_rhs = e; pc_guard = None } ->
(regexp_of_pattern env p, map_rhs e)
| { pc_guard = Some e; _ } ->
err e.pexp_loc "'when' guards are not supported")
cases
in
let brs = Array.of_list cases in
let auto = Sedlex.compile (Array.map fst brs) in
(gen_definition lexbuf auto cases error, auto)
let previous = ref [] let previous = ref []
let regexps = ref [] let regexps = ref []
let should_set_cookies = ref false let should_set_cookies = ref false
@ -481,37 +516,11 @@ let mapper =
method! expression e = method! expression e =
match e with match e with
| [%expr [%sedlex [%e? { pexp_desc = Pexp_match (lexbuf, cases) }]]] -> | [%expr [%sedlex [%e? { pexp_desc = Pexp_match _; _ } as match_expr]]]
let lexbuf = ->
match lexbuf with fst (handle_sedlex_match ~env ~map_rhs:this#expression match_expr)
| { pexp_desc = Pexp_ident { txt = Lident txt } } ->
(txt, lexbuf)
| _ ->
err lexbuf.pexp_loc
"the matched expression must be a single identifier"
in
let cases = List.rev cases in
let error =
match List.hd cases with
| { pc_lhs = [%pat? _]; pc_rhs = e; pc_guard = None } ->
this#expression e
| { pc_lhs = p } ->
err p.ppat_loc
"the last branch must be a catch-all error case"
in
let cases = List.rev (List.tl cases) in
let cases =
List.map
(function
| { pc_lhs = p; pc_rhs = e; pc_guard = None } ->
(regexp_of_pattern env p, this#expression e)
| { pc_guard = Some e } ->
err e.pexp_loc "'when' guards are not supported")
cases
in
gen_definition lexbuf cases error
| [%expr | [%expr
let [%p? { ppat_desc = Ppat_var { txt = name } }] = let [%p? { ppat_desc = Ppat_var { txt = name; _ }; _ }] =
[%sedlex.regexp? [%p? p]] [%sedlex.regexp? [%p? p]]
in in
[%e? body]] -> [%e? body]] ->
@ -531,7 +540,7 @@ let mapper =
(List.map (List.map
(function (function
| [%stri | [%stri
let [%p? { ppat_desc = Ppat_var { txt = name } }] = let [%p? { ppat_desc = Ppat_var { txt = name; _ }; _ }] =
[%sedlex.regexp? [%p? p]]] as i -> [%sedlex.regexp? [%p? p]]] as i ->
regexps := i :: !regexps; regexps := i :: !regexps;
mapper := !mapper#define_regexp name p; mapper := !mapper#define_regexp name p;
@ -556,7 +565,7 @@ let mapper =
let pre_handler cookies = let pre_handler cookies =
previous := previous :=
match Driver.Cookies.get cookies "sedlex.regexps" Ast_pattern.__ with match Driver.Cookies.get cookies "sedlex.regexps" Ast_pattern.__ with
| Some { pexp_desc = Pexp_extension (_, PStr l) } -> l | Some { pexp_desc = Pexp_extension (_, PStr l); _ } -> l
| Some _ -> assert false | Some _ -> assert false
| None -> [] | None -> []

View File

@ -1,6 +1,5 @@
(* The package sedlex is released under the terms of an MIT-like license. *)
(* See the attached LICENSE file. *)
(* Copyright 2005, 2013 by Alain Frisch and LexiFi. *) (* Copyright 2005, 2013 by Alain Frisch and LexiFi. *)
(* Copyright 2026 by xenia <xenia@awoo.systems> *)
module Cset = Sedlex_cset module Cset = Sedlex_cset
@ -25,7 +24,7 @@ let new_node () =
let seq r1 r2 succ = r1 (r2 succ) let seq r1 r2 succ = r1 (r2 succ)
let is_chars final = function let is_chars final = function
| { eps = []; trans = [(c, f)] } when f == final -> Some c | { eps = []; trans = [(c, f)]; _ } when f == final -> Some c
| _ -> None | _ -> None
let chars c succ = let chars c succ =
@ -117,6 +116,9 @@ let transition (state : state) =
Array.sort (fun (c1, _) (c2, _) -> compare c1 c2) t; Array.sort (fun (c1, _) (c2, _) -> compare c1 c2) t;
t t
type dfa_state = { trans : (Cset.t * int) array; finals : bool array }
type dfa = dfa_state array
let compile rs = let compile rs =
let rs = Array.map compile_re rs in let rs = Array.map compile_re rs in
let counter = ref 0 in let counter = ref 0 in
@ -131,7 +133,7 @@ let compile rs =
let trans = transition state in let trans = transition state in
let trans = Array.map (fun (p, t) -> (p, aux t)) trans in let trans = Array.map (fun (p, t) -> (p, aux t)) trans in
let finals = Array.map (fun (_, f) -> List.memq f state) rs in let finals = Array.map (fun (_, f) -> List.memq f state) rs in
Hashtbl.add states_def i (trans, finals); Hashtbl.add states_def i { trans; finals };
i i
in in
let init = ref [] in let init = ref [] in
@ -139,3 +141,56 @@ let compile rs =
let i = aux !init in let i = aux !init in
assert (i = 0); assert (i = 0);
Array.init !counter (Hashtbl.find states_def) Array.init !counter (Hashtbl.find states_def)
let cset_to_label cset =
let escape_dot c =
match c with
| '"' -> "\\\""
| '\\' -> "\\\\"
| '<' -> "\\<"
| '>' -> "\\>"
| _ -> String.make 1 c
in
let format_interval (lo, hi) =
if lo = -1 && hi = -1 then "EOF"
else if lo = hi then
if lo >= 32 && lo <= 126 then "'" ^ escape_dot (Char.chr lo) ^ "'"
else Printf.sprintf "U+%04X" lo
else if lo >= 32 && lo <= 126 && hi >= 32 && hi <= 126 then
"'" ^ escape_dot (Char.chr lo) ^ "'-'" ^ escape_dot (Char.chr hi) ^ "'"
else Printf.sprintf "U+%04X-U+%04X" lo hi
in
String.concat ", "
(List.map format_interval (cset : Cset.t :> (int * int) list))
let dfa_to_dot dfa =
let buf = Buffer.create 1024 in
let bprintf = Printf.bprintf in
bprintf buf "digraph {\n";
bprintf buf " rankdir=LR;\n";
bprintf buf " node [shape=circle];\n\n";
bprintf buf " _start [shape=point];\n";
bprintf buf " _start -> state0;\n\n";
Array.iteri
(fun i { trans; finals } ->
let accepted =
let acc = ref [] in
for r = Array.length finals - 1 downto 0 do
if finals.(r) then acc := r :: !acc
done;
!acc
in
(match accepted with
| [] -> bprintf buf " state%d [label=\"%d\"];\n" i i
| rules ->
bprintf buf
" state%d [label=\"%d\\n[rule %s]\", shape=doublecircle];\n" i i
(String.concat "," (List.map string_of_int rules)));
Array.iter
(fun (cset, target) ->
let label = cset_to_label cset in
bprintf buf " state%d -> state%d [label=\"%s\"];\n" i target label)
trans)
dfa;
bprintf buf "}\n";
Buffer.contents buf

View File

@ -1,6 +1,5 @@
(* The package sedlex is released under the terms of an MIT-like license. *)
(* See the attached LICENSE file. *)
(* Copyright 2005, 2013 by Alain Frisch and LexiFi. *) (* Copyright 2005, 2013 by Alain Frisch and LexiFi. *)
(* Copyright 2026 by xenia <xenia@awoo.systems> *)
type regexp type regexp
@ -22,4 +21,8 @@ val intersection : regexp -> regexp -> regexp option
(* If each argument is a single [chars] regexp, returns a regexp (* If each argument is a single [chars] regexp, returns a regexp
which matches the intersection set. Otherwise returns [None]. *) which matches the intersection set. Otherwise returns [None]. *)
val compile : regexp array -> ((Sedlex_cset.t * int) array * bool array) array type dfa_state = { trans : (Sedlex_cset.t * int) array; finals : bool array }
type dfa = dfa_state array
val compile : regexp array -> dfa
val dfa_to_dot : dfa -> string

View File

@ -1,5 +1,4 @@
(* The package sedlex is released under the terms of an MIT-like license. *)
(* See the attached LICENSE file. *)
(* Copyright 2005, 2013 by Alain Frisch and LexiFi. *) (* Copyright 2005, 2013 by Alain Frisch and LexiFi. *)
(* Copyright 2026 by xenia <xenia@awoo.systems> *)
include Sedlex_utils.Cset include Sedlex_utils.Cset

8
test/codegen/dune Normal file
View File

@ -0,0 +1,8 @@
(library
(name sedlex_gen_test)
(libraries noslop-sedlex)
(inline_tests)
(enabled_if
(>= %{ocaml_version} 4.14))
(preprocess
(pps ppx_sedlex_test ppx_expect)))

120
test/codegen/test_gen.ml Normal file
View File

@ -0,0 +1,120 @@
let%expect_test "simple string match" =
(match%sedlex_test buf with "ab" | "de" -> () | _ -> ());
[%expect
{|
DOT:
digraph {
rankdir=LR;
node [shape=circle];
_start [shape=point];
_start -> state0;
state0 [label="0"];
state0 -> state1 [label="'a'"];
state0 -> state3 [label="'d'"];
state1 [label="1"];
state1 -> state2 [label="'b'"];
state2 [label="2\n[rule 0]", shape=doublecircle];
state3 [label="3"];
state3 -> state2 [label="'e'"];
}
CODE:
let rec __sedlex_state_0 buf =
match __sedlex_partition_1 (Sedlexing.__private__next_int buf) with
| 0 -> __sedlex_state_1 buf
| 1 -> __sedlex_state_3 buf
| _ -> Sedlexing.backtrack buf
and __sedlex_state_1 buf =
match __sedlex_partition_2 (Sedlexing.__private__next_int buf) with
| 0 -> 0
| _ -> Sedlexing.backtrack buf
and __sedlex_state_3 buf =
match __sedlex_partition_3 (Sedlexing.__private__next_int buf) with
| 0 -> 0
| _ -> Sedlexing.backtrack buf in
Sedlexing.start buf; (match __sedlex_state_0 buf with | 0 -> () | _ -> ())
|}]
let%expect_test "character class" =
(match%sedlex_test buf with Plus 'a' .. 'z' -> () | _ -> ());
[%expect
{|
DOT:
digraph {
rankdir=LR;
node [shape=circle];
_start [shape=point];
_start -> state0;
state0 [label="0"];
state0 -> state1 [label="'a'-'z'"];
state1 [label="1\n[rule 0]", shape=doublecircle];
state1 -> state1 [label="'a'-'z'"];
}
CODE:
let rec __sedlex_state_0 buf =
match __sedlex_partition_1 (Sedlexing.__private__next_int buf) with
| 0 -> __sedlex_state_1 buf
| _ -> Sedlexing.backtrack buf
and __sedlex_state_1 buf =
Sedlexing.mark buf 0;
(match __sedlex_partition_1 (Sedlexing.__private__next_int buf) with
| 0 -> __sedlex_state_1 buf
| _ -> Sedlexing.backtrack buf) in
Sedlexing.start buf; (match __sedlex_state_0 buf with | 0 -> () | _ -> ())
|}]
let%expect_test "multi-rule" =
(match%sedlex_test buf with
| "ab" -> ()
| "de" -> ()
| Plus '0' .. '9' -> ()
| _ -> ());
[%expect
{|
DOT:
digraph {
rankdir=LR;
node [shape=circle];
_start [shape=point];
_start -> state0;
state0 [label="0"];
state0 -> state1 [label="'0'-'9'"];
state0 -> state2 [label="'a'"];
state0 -> state4 [label="'d'"];
state1 [label="1\n[rule 2]", shape=doublecircle];
state1 -> state1 [label="'0'-'9'"];
state2 [label="2"];
state2 -> state3 [label="'b'"];
state3 [label="3\n[rule 0]", shape=doublecircle];
state4 [label="4"];
state4 -> state5 [label="'e'"];
state5 [label="5\n[rule 1]", shape=doublecircle];
}
CODE:
let rec __sedlex_state_0 buf =
match __sedlex_partition_1 (Sedlexing.__private__next_int buf) with
| 0 -> __sedlex_state_1 buf
| 1 -> __sedlex_state_2 buf
| 2 -> __sedlex_state_4 buf
| _ -> Sedlexing.backtrack buf
and __sedlex_state_1 buf =
Sedlexing.mark buf 2;
(match __sedlex_partition_2 (Sedlexing.__private__next_int buf) with
| 0 -> __sedlex_state_1 buf
| _ -> Sedlexing.backtrack buf)
and __sedlex_state_2 buf =
match __sedlex_partition_3 (Sedlexing.__private__next_int buf) with
| 0 -> 0
| _ -> Sedlexing.backtrack buf
and __sedlex_state_4 buf =
match __sedlex_partition_4 (Sedlexing.__private__next_int buf) with
| 0 -> 1
| _ -> Sedlexing.backtrack buf in
Sedlexing.start buf;
(match __sedlex_state_0 buf with | 0 -> () | 1 -> () | 2 -> () | _ -> ())
|}]

6
test/ppx_test/dune Normal file
View File

@ -0,0 +1,6 @@
(library
(name ppx_sedlex_test)
(kind ppx_rewriter)
(libraries ppxlib noslop-sedlex.ppx)
(preprocess
(pps ppxlib.metaquot)))

View File

@ -0,0 +1,36 @@
open Ppxlib
module P = Sedlex_ppx.Ppx_sedlex
module S = Sedlex_ppx.Sedlex
let reset_state () =
P.partition_counter := 0;
P.table_counter := 0;
Hashtbl.clear P.partitions;
Hashtbl.clear P.tables
let clear_tables () =
Hashtbl.clear P.partitions;
Hashtbl.clear P.tables
let expand ~ctxt:_ expr =
reset_state ();
let loc = Location.none in
let code_expr, auto =
P.handle_sedlex_match ~env:P.builtin_regexps ~map_rhs:Fun.id expr
in
let code_str = Pprintast.string_of_expression code_expr in
let dot_str = S.dfa_to_dot auto in
clear_tables ();
[%expr
print_string "DOT:\n";
print_string [%e Ast_builder.Default.estring ~loc dot_str];
print_string "CODE:\n";
print_string [%e Ast_builder.Default.estring ~loc code_str];
print_newline ()]
let ext =
Extension.V3.declare "sedlex_test" Extension.Context.expression
Ast_pattern.(single_expr_payload __)
expand
let () = Driver.register_transformation "sedlex_test" ~extensions:[ext]