@@ -15,6 +15,8 @@ module regex_optimizer |
| 15 | 15 | integer, parameter :: MAX_STATES = 1024 |
| 16 | 16 | integer, parameter :: MAX_PREFIX_LEN = 64 |
| 17 | 17 | integer, parameter :: DFA_CACHE_SIZE = 256 ! Cache recent state transitions |
| 18 | + integer, parameter :: MAX_DFA_STATES = 512 ! Max DFA states before fallback to NFA |
| 19 | + integer, parameter :: DFA_DEAD_STATE = 0 ! Special state: no match possible |
| 18 | 20 | |
| 19 | 21 | !> Bit vector for state sets - much faster than array lookup |
| 20 | 22 | type :: state_set_t |
@@ -39,6 +41,23 @@ module regex_optimizer |
| 39 | 41 | logical :: is_case_insensitive = .false. ! Case sensitivity flag |
| 40 | 42 | end type dfa_cache_entry_t |
| 41 | 43 | |
| 44 | + !> Full DFA state - precomputed transitions for all 256 characters |
| 45 | + type :: dfa_state_t |
| 46 | + integer :: transitions(0:255) = DFA_DEAD_STATE ! Next state for each byte |
| 47 | + type(state_set_t) :: nfa_states ! Corresponding NFA state set |
| 48 | + logical :: is_accept = .false. ! Is this an accepting state? |
| 49 | + integer(8) :: state_hash = 0 ! Hash for lookup |
| 50 | + end type dfa_state_t |
| 51 | + |
| 52 | + !> Compiled DFA for O(n) matching |
| 53 | + type :: compiled_dfa_t |
| 54 | + type(dfa_state_t), allocatable :: states(:) ! DFA states |
| 55 | + integer :: num_states = 0 ! Number of states built |
| 56 | + integer :: start_state = 0 ! Starting DFA state |
| 57 | + logical :: compiled = .false. ! DFA successfully compiled |
| 58 | + logical :: too_large = .false. ! DFA exceeded size limit |
| 59 | + end type compiled_dfa_t |
| 60 | + |
| 42 | 61 | !> Optimized NFA with precomputed data |
| 43 | 62 | type :: optimized_nfa_t |
| 44 | 63 | type(nfa_t) :: nfa ! Original NFA |
@@ -49,6 +68,8 @@ module regex_optimizer |
| 49 | 68 | integer :: skip_table(0:255) = 0 ! Boyer-Moore skip table for prefix |
| 50 | 69 | type(state_set_t) :: start_closure ! Pre-computed start state epsilon closure |
| 51 | 70 | type(dfa_cache_entry_t) :: dfa_cache(DFA_CACHE_SIZE) ! Lazy DFA cache |
| 71 | + type(compiled_dfa_t) :: dfa ! Full compiled DFA (if available) |
| 72 | + logical :: use_dfa = .false. ! Use DFA instead of NFA |
| 52 | 73 | logical :: optimized = .false. |
| 53 | 74 | end type optimized_nfa_t |
| 54 | 75 | |
@@ -158,6 +179,7 @@ contains |
| 158 | 179 | opt%prefix = '' |
| 159 | 180 | opt%anchored_start = .false. |
| 160 | 181 | opt%anchored_end = .false. |
| 182 | + opt%use_dfa = .false. |
| 161 | 183 | |
| 162 | 184 | ! Extract literal prefix and detect anchors |
| 163 | 185 | call extract_prefix_and_anchors(opt) |
@@ -173,6 +195,14 @@ contains |
| 173 | 195 | ! Clear DFA cache |
| 174 | 196 | opt%dfa_cache%valid = .false. |
| 175 | 197 | |
| 198 | + ! Try to compile full DFA for O(n) matching |
| 199 | + ! Only for patterns without any position-dependent transitions (anchors) |
| 200 | + if (.not. has_anchor_transitions(opt%nfa)) then |
| 201 | + call compile_dfa(opt) |
| 202 | + ! DEBUG: Print DFA compilation result (uncomment for debugging) |
| 203 | + ! write(0,*) 'DFA compiled:', opt%use_dfa, 'states:', opt%dfa%num_states, 'too_large:', opt%dfa%too_large |
| 204 | + end if |
| 205 | + |
| 176 | 206 | opt%optimized = .true. |
| 177 | 207 | |
| 178 | 208 | end subroutine optimize_nfa |
@@ -299,6 +329,232 @@ contains |
| 299 | 329 | end do |
| 300 | 330 | end subroutine compute_epsilon_closure_basic |
| 301 | 331 | |
| 332 | + function has_anchor_transitions(nfa) result(has_anchors) |
| 333 | + !> Check if NFA has any anchor transitions (position-dependent) |
| 334 | + !> These include ^, $, \<, \>, \b, \B |
| 335 | + type(nfa_t), intent(in) :: nfa |
| 336 | + logical :: has_anchors |
| 337 | + |
| 338 | + integer :: state, i |
| 339 | + type(nfa_transition_t) :: trans |
| 340 | + |
| 341 | + has_anchors = .false. |
| 342 | + |
| 343 | + do state = 1, nfa%num_states |
| 344 | + do i = 1, nfa%states(state)%num_trans |
| 345 | + trans = nfa%states(state)%trans(i) |
| 346 | + if (trans%trans_type == TRANS_ANCHOR) then |
| 347 | + has_anchors = .true. |
| 348 | + return |
| 349 | + end if |
| 350 | + end do |
| 351 | + end do |
| 352 | + end function has_anchor_transitions |
| 353 | + |
| 354 | + !--------------------------------------------------------------------------- |
| 355 | + ! DFA Compilation: Convert NFA to DFA for O(n) matching |
| 356 | + !--------------------------------------------------------------------------- |
| 357 | + |
| 358 | + subroutine compile_dfa(opt) |
| 359 | + !> Compile NFA to DFA using subset construction |
| 360 | + !> Creates DFA states lazily, stopping if too many states |
| 361 | + type(optimized_nfa_t), intent(inout) :: opt |
| 362 | + |
| 363 | + type(state_set_t) :: start_set, next_set |
| 364 | + integer :: worklist(MAX_DFA_STATES), work_head, work_tail |
| 365 | + integer :: dfa_idx, char_code, next_idx, old_num_states |
| 366 | + |
| 367 | + ! Allocate DFA states |
| 368 | + if (allocated(opt%dfa%states)) deallocate(opt%dfa%states) |
| 369 | + allocate(opt%dfa%states(MAX_DFA_STATES)) |
| 370 | + opt%dfa%num_states = 0 |
| 371 | + opt%dfa%compiled = .false. |
| 372 | + opt%dfa%too_large = .false. |
| 373 | + opt%use_dfa = .false. |
| 374 | + |
| 375 | + ! Compute start state: epsilon closure of NFA start |
| 376 | + call start_set%clear() |
| 377 | + call compute_epsilon_closure_basic(opt%nfa, opt%nfa%start_state, start_set) |
| 378 | + |
| 379 | + if (start_set%is_empty()) return |
| 380 | + |
| 381 | + ! Create initial DFA state |
| 382 | + opt%dfa%num_states = 1 |
| 383 | + opt%dfa%states(1)%nfa_states = start_set |
| 384 | + opt%dfa%states(1)%state_hash = start_set%hash() |
| 385 | + opt%dfa%states(1)%is_accept = is_accepting_set(opt%nfa, start_set) |
| 386 | + opt%dfa%start_state = 1 |
| 387 | + |
| 388 | + ! Initialize worklist with start state |
| 389 | + work_head = 1 |
| 390 | + work_tail = 1 |
| 391 | + worklist(1) = 1 |
| 392 | + |
| 393 | + ! Process worklist: for each DFA state, compute transitions |
| 394 | + do while (work_head <= work_tail) |
| 395 | + dfa_idx = worklist(work_head) |
| 396 | + work_head = work_head + 1 |
| 397 | + |
| 398 | + ! Compute transitions for all 256 characters |
| 399 | + do char_code = 0, 255 |
| 400 | + call next_set%clear() |
| 401 | + |
| 402 | + ! Compute NFA transitions for this character |
| 403 | + call compute_char_transitions_simple(opt%nfa, opt%dfa%states(dfa_idx)%nfa_states, & |
| 404 | + char(char_code), next_set) |
| 405 | + |
| 406 | + ! Compute epsilon closure of result |
| 407 | + if (.not. next_set%is_empty()) then |
| 408 | + call expand_epsilon_closure_simple(opt%nfa, next_set) |
| 409 | + end if |
| 410 | + |
| 411 | + if (next_set%is_empty()) then |
| 412 | + opt%dfa%states(dfa_idx)%transitions(char_code) = DFA_DEAD_STATE |
| 413 | + else |
| 414 | + ! Find or create DFA state for this NFA state set |
| 415 | + old_num_states = opt%dfa%num_states |
| 416 | + next_idx = find_or_create_dfa_state(opt%dfa, next_set, opt%nfa) |
| 417 | + |
| 418 | + if (next_idx == -1) then |
| 419 | + ! Too many DFA states - abort |
| 420 | + opt%dfa%too_large = .true. |
| 421 | + opt%dfa%compiled = .false. |
| 422 | + return |
| 423 | + end if |
| 424 | + |
| 425 | + opt%dfa%states(dfa_idx)%transitions(char_code) = next_idx |
| 426 | + |
| 427 | + ! Add new state to worklist only if it was just created |
| 428 | + if (opt%dfa%num_states > old_num_states) then |
| 429 | + work_tail = work_tail + 1 |
| 430 | + if (work_tail > MAX_DFA_STATES) then |
| 431 | + opt%dfa%too_large = .true. |
| 432 | + opt%dfa%compiled = .false. |
| 433 | + return |
| 434 | + end if |
| 435 | + worklist(work_tail) = next_idx |
| 436 | + end if |
| 437 | + end if |
| 438 | + end do |
| 439 | + end do |
| 440 | + |
| 441 | + opt%dfa%compiled = .true. |
| 442 | + opt%use_dfa = .true. |
| 443 | + |
| 444 | + end subroutine compile_dfa |
| 445 | + |
| 446 | + function find_or_create_dfa_state(dfa, nfa_states, nfa) result(idx) |
| 447 | + !> Find existing DFA state for NFA state set, or create new one |
| 448 | + !> Returns -1 if DFA state limit exceeded |
| 449 | + type(compiled_dfa_t), intent(inout) :: dfa |
| 450 | + type(state_set_t), intent(in) :: nfa_states |
| 451 | + type(nfa_t), intent(in) :: nfa |
| 452 | + integer :: idx |
| 453 | + |
| 454 | + integer(8) :: h |
| 455 | + integer :: i |
| 456 | + |
| 457 | + h = nfa_states%hash() |
| 458 | + |
| 459 | + ! Search existing states |
| 460 | + do i = 1, dfa%num_states |
| 461 | + if (dfa%states(i)%state_hash == h .and. & |
| 462 | + dfa%states(i)%nfa_states%equals(nfa_states)) then |
| 463 | + idx = i |
| 464 | + return |
| 465 | + end if |
| 466 | + end do |
| 467 | + |
| 468 | + ! Create new state |
| 469 | + if (dfa%num_states >= MAX_DFA_STATES) then |
| 470 | + idx = -1 |
| 471 | + return |
| 472 | + end if |
| 473 | + |
| 474 | + dfa%num_states = dfa%num_states + 1 |
| 475 | + idx = dfa%num_states |
| 476 | + dfa%states(idx)%nfa_states = nfa_states |
| 477 | + dfa%states(idx)%state_hash = h |
| 478 | + dfa%states(idx)%is_accept = is_accepting_set(nfa, nfa_states) |
| 479 | + dfa%states(idx)%transitions = DFA_DEAD_STATE |
| 480 | + |
| 481 | + end function find_or_create_dfa_state |
| 482 | + |
| 483 | + subroutine compute_char_transitions_simple(nfa, current, c, next_set) |
| 484 | + !> Compute character transitions without case folding (for DFA compilation) |
| 485 | + type(nfa_t), intent(in) :: nfa |
| 486 | + type(state_set_t), intent(in) :: current |
| 487 | + character(len=1), intent(in) :: c |
| 488 | + type(state_set_t), intent(inout) :: next_set |
| 489 | + |
| 490 | + integer :: state, word_idx, bit_idx, i |
| 491 | + integer(8) :: word, mask |
| 492 | + type(nfa_transition_t) :: trans |
| 493 | + |
| 494 | + do word_idx = 1, size(current%bits) |
| 495 | + word = current%bits(word_idx) |
| 496 | + if (word == 0) cycle |
| 497 | + |
| 498 | + do bit_idx = 0, 63 |
| 499 | + mask = ishft(1_8, bit_idx) |
| 500 | + if (iand(word, mask) /= 0) then |
| 501 | + state = (word_idx - 1) * 64 + bit_idx + 1 |
| 502 | + if (state > nfa%num_states) cycle |
| 503 | + |
| 504 | + do i = 1, nfa%states(state)%num_trans |
| 505 | + trans = nfa%states(state)%trans(i) |
| 506 | + |
| 507 | + select case (trans%trans_type) |
| 508 | + case (TRANS_CHAR) |
| 509 | + if (c == trans%match_char) then |
| 510 | + call next_set%add(trans%target) |
| 511 | + end if |
| 512 | + |
| 513 | + case (TRANS_CLASS) |
| 514 | + if (trans%char_class(ichar(c)) .neqv. trans%negated) then |
| 515 | + call next_set%add(trans%target) |
| 516 | + end if |
| 517 | + |
| 518 | + case (TRANS_ANY) |
| 519 | + if (c /= char(10)) then |
| 520 | + call next_set%add(trans%target) |
| 521 | + end if |
| 522 | + end select |
| 523 | + end do |
| 524 | + end if |
| 525 | + end do |
| 526 | + end do |
| 527 | + end subroutine compute_char_transitions_simple |
| 528 | + |
| 529 | + subroutine expand_epsilon_closure_simple(nfa, state_set) |
| 530 | + !> Expand state set to include epsilon closure (in-place) |
| 531 | + type(nfa_t), intent(in) :: nfa |
| 532 | + type(state_set_t), intent(inout) :: state_set |
| 533 | + |
| 534 | + type(state_set_t) :: result |
| 535 | + integer :: word_idx, bit_idx, state |
| 536 | + integer(8) :: word, mask |
| 537 | + |
| 538 | + call result%clear() |
| 539 | + |
| 540 | + do word_idx = 1, size(state_set%bits) |
| 541 | + word = state_set%bits(word_idx) |
| 542 | + if (word == 0) cycle |
| 543 | + |
| 544 | + do bit_idx = 0, 63 |
| 545 | + mask = ishft(1_8, bit_idx) |
| 546 | + if (iand(word, mask) /= 0) then |
| 547 | + state = (word_idx - 1) * 64 + bit_idx + 1 |
| 548 | + if (state <= nfa%num_states) then |
| 549 | + call compute_epsilon_closure_basic(nfa, state, result) |
| 550 | + end if |
| 551 | + end if |
| 552 | + end do |
| 553 | + end do |
| 554 | + |
| 555 | + call state_set%copy_from(result) |
| 556 | + end subroutine expand_epsilon_closure_simple |
| 557 | + |
| 302 | 558 | !--------------------------------------------------------------------------- |
| 303 | 559 | ! Optimized Search: Use prefix to skip positions |
| 304 | 560 | !--------------------------------------------------------------------------- |
@@ -317,6 +573,13 @@ contains |
| 317 | 573 | |
| 318 | 574 | if (opt%nfa%num_states == 0) return |
| 319 | 575 | |
| 576 | + ! Fast path: use DFA if available (O(n) matching) |
| 577 | + ! DFA only works for case-sensitive matching (case-insensitive would need 2x states) |
| 578 | + if (opt%use_dfa .and. .not. ignore_case) then |
| 579 | + res = dfa_search(opt%dfa, text, text_len) |
| 580 | + return |
| 581 | + end if |
| 582 | + |
| 320 | 583 | ! Fast path: anchored start - only try position 1 |
| 321 | 584 | if (opt%anchored_start) then |
| 322 | 585 | res = optimized_match(opt, text, 1, ignore_case) |
@@ -355,6 +618,75 @@ contains |
| 355 | 618 | |
| 356 | 619 | end function optimized_search |
| 357 | 620 | |
| 621 | + function dfa_search(dfa, text, text_len) result(res) |
| 622 | + !> Fast O(n) DFA-based search |
| 623 | + !> Tries each starting position and returns first match |
| 624 | + type(compiled_dfa_t), intent(in) :: dfa |
| 625 | + character(len=*), intent(in) :: text |
| 626 | + integer, intent(in) :: text_len |
| 627 | + type(match_result_t) :: res |
| 628 | + |
| 629 | + integer :: start_pos |
| 630 | + type(match_result_t) :: try_res |
| 631 | + |
| 632 | + res%matched = .false. |
| 633 | + |
| 634 | + if (.not. dfa%compiled .or. dfa%num_states == 0) return |
| 635 | + |
| 636 | + ! Try each starting position |
| 637 | + do start_pos = 1, text_len + 1 |
| 638 | + try_res = dfa_match(dfa, text, text_len, start_pos) |
| 639 | + if (try_res%matched) then |
| 640 | + res = try_res |
| 641 | + return |
| 642 | + end if |
| 643 | + end do |
| 644 | + |
| 645 | + end function dfa_search |
| 646 | + |
| 647 | + function dfa_match(dfa, text, text_len, start_pos) result(res) |
| 648 | + !> O(n) DFA matching from a specific position |
| 649 | + !> Just follows transition table - no state set operations |
| 650 | + type(compiled_dfa_t), intent(in) :: dfa |
| 651 | + character(len=*), intent(in) :: text |
| 652 | + integer, intent(in) :: text_len, start_pos |
| 653 | + type(match_result_t) :: res |
| 654 | + |
| 655 | + integer :: state, pos, char_code |
| 656 | + |
| 657 | + res%matched = .false. |
| 658 | + |
| 659 | + if (.not. dfa%compiled) return |
| 660 | + |
| 661 | + state = dfa%start_state |
| 662 | + pos = start_pos |
| 663 | + |
| 664 | + ! Check if start state is accepting (empty match) |
| 665 | + if (dfa%states(state)%is_accept) then |
| 666 | + res%matched = .true. |
| 667 | + res%match_start = start_pos |
| 668 | + res%match_end = start_pos - 1 |
| 669 | + end if |
| 670 | + |
| 671 | + ! Process each character |
| 672 | + do while (pos <= text_len) |
| 673 | + char_code = ichar(text(pos:pos)) |
| 674 | + state = dfa%states(state)%transitions(char_code) |
| 675 | + |
| 676 | + if (state == DFA_DEAD_STATE) exit |
| 677 | + |
| 678 | + pos = pos + 1 |
| 679 | + |
| 680 | + ! Check for acceptance (greedy - find longest) |
| 681 | + if (dfa%states(state)%is_accept) then |
| 682 | + res%matched = .true. |
| 683 | + res%match_start = start_pos |
| 684 | + res%match_end = pos - 1 |
| 685 | + end if |
| 686 | + end do |
| 687 | + |
| 688 | + end function dfa_match |
| 689 | + |
| 358 | 690 | function prefix_matches(text, pos, prefix, prefix_len) result(matches) |
| 359 | 691 | character(len=*), intent(in) :: text |
| 360 | 692 | integer, intent(in) :: pos, prefix_len |