From 4b6fe1fe9f61fb9d638d6365f5938b2881c824a1 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Fri, 15 Jul 2022 21:42:56 +0200 Subject: [PATCH 01/64] Remove SIMD and inline generator Honestly, four generators and a tokenizer was excessive. Inline generator was probably not really used, and the new simd generator made the goto generator obsolete. Remove inline and old goto generator, and rename simd generator goto. --- docs/src/index.md | 4 +- src/codegen.jl | 139 +++------------------------------------------- test/runtests.jl | 4 +- test/simd.jl | 10 ++-- test/test01.jl | 3 +- test/test02.jl | 3 +- test/test03.jl | 3 +- test/test04.jl | 3 +- test/test05.jl | 3 +- test/test06.jl | 3 +- test/test11.jl | 4 +- test/test13.jl | 32 ----------- test/test14.jl | 18 ++---- test/test15.jl | 3 +- test/test16.jl | 33 +++++------ test/test17.jl | 6 +- test/test18.jl | 3 +- 17 files changed, 60 insertions(+), 214 deletions(-) delete mode 100644 test/test13.jl diff --git a/docs/src/index.md b/docs/src/index.md index b48a6f76..b6e8fec6 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -385,9 +385,9 @@ elseif cs < 0 end ``` -Automa.jl has four kinds of code generators. The first and default one uses two lookup tables to pick up the next state and the actions for the current state and input. The second one expands these lookup tables into a series of if-else branches. The third one is based on `@goto` jumps. The fourth one is identitical to the third one, except uses SIMD operations where applicable. These four code generators are named as `:table`, `:inline`, `:goto`, and `:simd`, respectively. To sepcify a code generator, you can pass the `code=:table|:inline|:goto|:simd` argument to `Automa.generate_exec_code`. The generated code size and its runtime speed highly depends on the machine and actions. However, as a rule of thumb, the code size and the runtime speed follow this order (i.e. `:table` will generates the smallest but the slowest code while `:simd` will the largest but the fastest). Also, specifying `checkbounds=false` turns off bounds checking while executing and often improves the runtime performance slightly. +Automa.jl has four kinds of code generators. The first and default one uses two lookup tables to pick up the next state and the actions for the current state and input. The second one expands these lookup tables into a series of if-else branches. The third one is based on `@goto` jumps. The fourth one is identitical to the third one, except uses SIMD operations where applicable. These two code generators are named as `:table`, and `:goto`, respectively. To sepcify a code generator, you can pass the `code=:table|:goto` argument to `Automa.generate_exec_code`. The generated code size and its runtime speed highly depends on the machine and actions. However, as a rule of thumb, `:table` is simpler with smaller code, but is also slower.Also, specifying `checkbounds=false` turns off bounds checking while executing and often improves the runtime performance slightly. -Note that the `:simd` generator has several requirements: +Note that the `:goto` generator has more requirements than the `:table` generator: * First, `boundscheck=false` must be set * Second, `loopunroll` must be the default `0` (as loops are SIMD unrolled) * Third, `getbyte` must be the default `Base.getindex` diff --git a/src/codegen.jl b/src/codegen.jl index 3d2ef052..426dd151 100644 --- a/src/codegen.jl +++ b/src/codegen.jl @@ -53,7 +53,7 @@ Arguments --------- - `vars`: variable names used in generated code -- `generator`: code generator (`:table`, `:inline` or `:goto`) +- `generator`: code generator (`:table` or `:goto`) - `checkbounds`: flag of bounds check - `loopunroll`: loop unroll factor (≥ 0) - `getbyte`: function of byte access (i.e. `getbyte(data, p)`) @@ -62,7 +62,7 @@ Arguments function CodeGenContext(; vars::Variables=Variables(:p, :p_end, :p_eof, :ts, :te, :cs, :data, gensym(), gensym()), generator::Symbol=:table, - checkbounds::Bool=true, + checkbounds::Bool=generator == :table, loopunroll::Integer=0, getbyte::Function=Base.getindex, clean::Bool=false) @@ -72,24 +72,20 @@ function CodeGenContext(; throw(ArgumentError("loop unrolling is not supported for $(generator)")) end # special conditions for simd generator - if generator == :simd + if generator == :goto if loopunroll != 0 - throw(ArgumentError("SIMD generator does not support unrolling")) + throw(ArgumentError("GOTO generator does not support unrolling")) elseif getbyte != Base.getindex - throw(ArgumentError("SIMD generator only support Base.getindex")) + throw(ArgumentError("GOTO generator only support Base.getindex")) elseif checkbounds - throw(ArgumentError("SIMD generator does not support boundscheck")) + throw(ArgumentError("GOTO generator does not support boundscheck")) end end # check generator if generator == :table generator = generate_table_code - elseif generator == :inline - generator = generate_inline_code elseif generator == :goto generator = generate_goto_code - elseif generator == :simd - generator = generate_simd_code else throw(ArgumentError("invalid code generator: $(generator)")) end @@ -174,7 +170,7 @@ function generate_transition_table(machine::Machine) end for s in traverse(machine.start), (e, t) in s.edges if !isempty(e.precond) - error("precondition is not supported in the table-based code generator; try code=:inline or :goto") + error("precondition is not supported in the table-based code generator; try code=:goto") end for l in e.labels trans_table[l+1,s.state] = t.state @@ -210,40 +206,6 @@ function generate_action_dispatch_code(ctx::CodeGenContext, machine::Machine, ac return action_dispatch_code, action_code end -function generate_inline_code(ctx::CodeGenContext, machine::Machine, actions::Dict{Symbol,Expr}) - trans_code = generate_transition_code(ctx, machine, actions) - eof_action_code = generate_eof_action_code(ctx, machine, actions) - getbyte_code = generate_getbyte_code(ctx) - final_state_code = generate_final_state_mem_code(ctx, machine) - return quote - $(ctx.vars.mem) = $(SizedMemory)($(ctx.vars.data)) - while $(ctx.vars.p) ≤ $(ctx.vars.p_end) && $(ctx.vars.cs) > 0 - $(getbyte_code) - $(trans_code) - $(ctx.vars.p) += 1 - end - if $(ctx.vars.p) > $(ctx.vars.p_eof) ≥ 0 && $(final_state_code) - $(eof_action_code) - $(ctx.vars.cs) = 0 - elseif $(ctx.vars.cs) < 0 - $(ctx.vars.p) -= 1 - end - end -end - -function generate_transition_code(ctx::CodeGenContext, machine::Machine, actions::Dict{Symbol,Expr}) - default = :($(ctx.vars.cs) = -$(ctx.vars.cs)) - return foldr(default, traverse(machine.start)) do s, els - then = foldr(default, optimize_edge_order(s.edges)) do edge, els′ - e, t = edge - action_code = rewrite_special_macros(ctx, generate_action_code(e.actions, actions), false) - then′ = :($(ctx.vars.cs) = $(t.state); $(action_code)) - return Expr(:if, generate_condition_code(ctx, e, actions), then′, els′) - end - return Expr(:if, state_condition(ctx, s.state), then, els) - end -end - function generate_goto_code(ctx::CodeGenContext, machine::Machine, actions::Dict{Symbol,Expr}) actions_in = Dict{Node,Set{Vector{Symbol}}}() for s in traverse(machine.start), (e, t) in s.edges @@ -259,87 +221,6 @@ function generate_goto_code(ctx::CodeGenContext, machine::Machine, actions::Dict end end - blocks = Expr[] - for s in traverse(machine.start) - block = Expr(:block) - for (names, label) in action_label[s] - if isempty(names) - continue - end - append_code!(block, quote - @label $(label) - $(rewrite_special_macros(ctx, generate_action_code(names, actions), false, s.state)) - @goto $(Symbol("state_", s.state)) - end) - end - append_code!(block, quote - @label $(Symbol("state_", s.state)) - $(ctx.vars.p) += 1 - if $(ctx.vars.p) > $(ctx.vars.p_end) - $(ctx.vars.cs) = $(s.state) - @goto exit - end - end) - default = :($(ctx.vars.cs) = $(-s.state); @goto exit) - dispatch_code = foldr(default, optimize_edge_order(s.edges)) do edge, els - e, t = edge - if isempty(e.actions) - if ctx.loopunroll > 0 && s.state == t.state && length(e.labels) ≥ 4 - then = generate_unrolled_loop(ctx, e, t) - else - then = :(@goto $(Symbol("state_", t.state))) - end - else - then = :(@goto $(action_label[t][action_names(e.actions)])) - end - return Expr(:if, generate_condition_code(ctx, e, actions), then, els) - end - append_code!(block, quote - @label $(Symbol("state_case_", s.state)) - $(generate_getbyte_code(ctx)) - $(dispatch_code) - end) - push!(blocks, block) - end - - enter_code = foldr(:(@goto exit), machine.states) do s, els - return Expr(:if, :($(ctx.vars.cs) == $(s)), :(@goto $(Symbol("state_case_", s))), els) - end - - eof_action_code = rewrite_special_macros(ctx, generate_eof_action_code(ctx, machine, actions), true) - final_state_code = generate_final_state_mem_code(ctx, machine) - - return quote - if $(ctx.vars.p) > $(ctx.vars.p_end) - @goto exit - end - $(ctx.vars.mem) = $(SizedMemory)($(ctx.vars.data)) - $(enter_code) - $(Expr(:block, blocks...)) - @label exit - if $(ctx.vars.p) > $(ctx.vars.p_eof) ≥ 0 && $(final_state_code) - $(eof_action_code) - $(ctx.vars.cs) = 0 - end - end -end - -function generate_simd_code(ctx::CodeGenContext, machine::Machine, actions::Dict{Symbol,Expr}) - ## SAME AS GOTO BEGIN - actions_in = Dict{Node,Set{Vector{Symbol}}}() - for s in traverse(machine.start), (e, t) in s.edges - push!(get!(actions_in, t, Set{Vector{Symbol}}()), action_names(e.actions)) - end - action_label = Dict{Node,Dict{Vector{Symbol},Symbol}}() - for s in traverse(machine.start) - action_label[s] = Dict() - if haskey(actions_in, s) - for (i, names) in enumerate(actions_in[s]) - action_label[s][names] = Symbol("state_", s.state, "_action_", i) - end - end - end - blocks = Expr[] for s in traverse(machine.start) block = Expr(:block) @@ -363,7 +244,6 @@ function generate_simd_code(ctx::CodeGenContext, machine::Machine, actions::Dict end end) - ### END SAME simd, non_simd = peel_simd_edge(s) simd_code = if simd !== nothing quote @@ -387,7 +267,7 @@ function generate_simd_code(ctx::CodeGenContext, machine::Machine, actions::Dict end return Expr(:if, generate_condition_code(ctx, e, actions), then, els) end - # BEGIN SAME AGAIN + append_code!(block, quote @label $(Symbol("state_case_", s.state)) $(simd_code) @@ -590,7 +470,7 @@ function generate_input_error_code(ctx::CodeGenContext, machine::Machine) end end -# Used by the :table and :inline code generators. +# Used by the :table code generator. function rewrite_special_macros(ctx::CodeGenContext, ex::Expr, eof::Bool) args = [] for arg in ex.args @@ -688,7 +568,6 @@ function peel_simd_edge(node) return simd, non_simd end - # Sort edges by its size in descending order. function optimize_edge_order(edges) return sort!(copy(edges), by=e->length(e[1].labels), rev=true) diff --git a/test/runtests.jl b/test/runtests.jl index 71015a1c..5bc082c2 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -90,7 +90,7 @@ include("test09.jl") include("test10.jl") include("test11.jl") include("test12.jl") -include("test13.jl") +# test13 tested functionality now removed. include("test14.jl") include("test15.jl") include("test16.jl") @@ -359,7 +359,7 @@ end loopcode = quote found && @goto __return__ end -context = Automa.CodeGenContext(generator=:goto) +context = Automa.CodeGenContext(generator=:goto, checkbounds=false) Automa.Stream.generate_reader( :readrecord!, machine, diff --git a/test/simd.jl b/test/simd.jl index d3acb1fd..91ece82e 100644 --- a/test/simd.jl +++ b/test/simd.jl @@ -1,9 +1,9 @@ # Test codegencontext @testset "CodeGenContext" begin @test_throws ArgumentError Automa.CodeGenContext(generator=:fdjfhkdj) - @test_throws ArgumentError Automa.CodeGenContext(generator=:simd) - @test_throws ArgumentError Automa.CodeGenContext(generator=:simd, checkbounds=false, loopunroll=2) - @test_throws ArgumentError Automa.CodeGenContext(generator=:simd, checkbounds=false, getbyte=identity) + @test_throws ArgumentError Automa.CodeGenContext(generator=:goto) + @test_throws ArgumentError Automa.CodeGenContext(generator=:goto, checkbounds=false, loopunroll=2) + @test_throws ArgumentError Automa.CodeGenContext(generator=:goto, checkbounds=false, getbyte=identity) end import Automa @@ -18,7 +18,7 @@ import Automa.RegExp: @re_str Automa.compile(re.opt(rec) * re.rep(re"\n" * rec)) end - context = Automa.CodeGenContext(generator=:simd, checkbounds=false) + context = Automa.CodeGenContext(generator=:goto, checkbounds=false) @eval function is_valid_fasta(data::String) $(Automa.generate_init_code(context, machine)) @@ -29,7 +29,7 @@ import Automa.RegExp: @re_str s1 = ">seq\nTAGGCTA\n>hello\nAJKGMP" s2 = ">seq1\nTAGGC" - s3 = ">verylongsequencewherethesimdkicksin\nQ" + s3 = ">verylongsequencewherethesimdkicksinmakeitevenlongertobesure\nQ" for (seq, isvalid) in [(s1, true), (s2, false), (s3, true)] @test is_valid_fasta(seq) == isvalid diff --git a/test/test01.jl b/test/test01.jl index 7cd6946c..da8d9668 100644 --- a/test/test01.jl +++ b/test/test01.jl @@ -11,7 +11,8 @@ using Test machine = Automa.compile(re) @test occursin(r"^Automa.Machine\(<.*>\)$", repr(machine)) - for generator in (:table, :inline, :goto), checkbounds in (true, false), clean in (true, false) + for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) + (generator == :goto && checkbounds) && continue ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) init_code = Automa.generate_init_code(ctx, machine) exec_code = Automa.generate_exec_code(ctx, machine, :debug) diff --git a/test/test02.jl b/test/test02.jl index 9bcb0084..271c0ea8 100644 --- a/test/test02.jl +++ b/test/test02.jl @@ -27,7 +27,8 @@ using Test @test last == 0 @test actions == [:enter_re,:enter_a,:final_a,:exit_a,:enter_b,:final_b,:final_re,:exit_b,:exit_re] - for generator in (:table, :inline, :goto), checkbounds in (true, false), clean in (true, false) + for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) + (generator == :goto && checkbounds) && continue ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) init_code = Automa.generate_init_code(ctx, machine) exec_code = Automa.generate_exec_code(ctx, machine, :debug) diff --git a/test/test03.jl b/test/test03.jl index d91c2a84..7421ce90 100644 --- a/test/test03.jl +++ b/test/test03.jl @@ -13,7 +13,8 @@ using Test machine = Automa.compile(fasta) - for generator in (:table, :inline, :goto), checkbounds in (true, false), clean in (true, false) + for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) + (generator == :goto && checkbounds) && continue ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) init_code = Automa.generate_init_code(ctx, machine) exec_code = Automa.generate_exec_code(ctx, machine) diff --git a/test/test04.jl b/test/test04.jl index 625af5a8..2a75bf50 100644 --- a/test/test04.jl +++ b/test/test04.jl @@ -12,7 +12,8 @@ using Test machine = Automa.compile(beg_a_end_b) - for generator in (:table, :inline, :goto), checkbounds in (true, false), clean in (true, false) + for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) + (generator == :goto && checkbounds) && continue ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) init_code = Automa.generate_init_code(ctx, machine) exec_code = Automa.generate_exec_code(ctx, machine) diff --git a/test/test05.jl b/test/test05.jl index b87fc725..5eb21bd6 100644 --- a/test/test05.jl +++ b/test/test05.jl @@ -16,7 +16,8 @@ using Test machine = Automa.compile(token) - for generator in (:table, :inline, :goto), checkbounds in (true, false), clean in (true, false) + for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) + (generator == :goto && checkbounds) && continue ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) init_code = Automa.generate_init_code(ctx, machine) exec_code = Automa.generate_exec_code(ctx, machine, :debug) diff --git a/test/test06.jl b/test/test06.jl index cbb7f213..652352be 100644 --- a/test/test06.jl +++ b/test/test06.jl @@ -23,7 +23,8 @@ using Test end end - for generator in (:table, :inline, :goto), checkbounds in (true, false), clean in (true, false) + for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) + (generator == :goto && checkbounds) && continue ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) run! = @eval function (state, data) ret = [] diff --git a/test/test11.jl b/test/test11.jl index 8720b8c8..5d3b123d 100644 --- a/test/test11.jl +++ b/test/test11.jl @@ -22,8 +22,8 @@ using Test ctx = Automa.CodeGenContext(generator=:table) @test_throws ErrorException Automa.generate_exec_code(ctx, machine, actions) - for generator in (:inline, :goto), checkbounds in (true, false), clean in (true, false) - ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) + for clean in (true, false) + ctx = Automa.CodeGenContext(generator=:goto, checkbounds=false, clean=clean) validate = @eval function (data, n) logger = Symbol[] $(Automa.generate_init_code(ctx, machine)) diff --git a/test/test13.jl b/test/test13.jl deleted file mode 100644 index 80eefa5f..00000000 --- a/test/test13.jl +++ /dev/null @@ -1,32 +0,0 @@ -module Test13 - -import Automa -import Automa.RegExp: @re_str -const re = Automa.RegExp -using Test - -@testset "Test13" begin - abra = re"abra" - ca = re"(ca)+" - ca.actions[:enter] = [:ca_enter] - ca.actions[:exit] = [:ca_exit] - dabra = re"dabra" - machine = Automa.compile(re.cat(abra, ca, dabra)) - ctx = Automa.CodeGenContext(generator=:inline, clean=true) - @eval function validate(data) - logger = Symbol[] - $(Automa.generate_init_code(ctx, machine)) - p_end = p_eof = sizeof(data) - $(Automa.generate_exec_code(ctx, machine, :debug)) - return logger, cs == 0 ? :ok : cs < 0 ? :error : :incomplete - end - @test validate(b"a") == ([], :incomplete) - @test validate(b"abrac") == ([:ca_enter], :incomplete) - @test validate(b"abraca") == ([:ca_enter], :incomplete) - @test validate(b"abracad") == ([:ca_enter, :ca_exit], :incomplete) - @test validate(b"abracadabra") == ([:ca_enter, :ca_exit], :ok) - @test validate(b"abracacadabra") == ([:ca_enter, :ca_exit], :ok) - @test validate(b"abrad") == ([], :error) -end - -end diff --git a/test/test14.jl b/test/test14.jl index ed73f09e..04b23685 100644 --- a/test/test14.jl +++ b/test/test14.jl @@ -17,15 +17,7 @@ using Test return p, cs end - ctx = Automa.CodeGenContext(generator=:inline) - @eval function validate_inline(data) - $(Automa.generate_init_code(ctx, machine)) - p_end = p_eof = sizeof(data) - $(Automa.generate_exec_code(ctx, machine)) - return p, cs - end - - ctx = Automa.CodeGenContext(generator=:goto) + ctx = Automa.CodeGenContext(generator=:goto, checkbounds=false) @eval function validate_goto(data) $(Automa.generate_init_code(ctx, machine)) p_end = p_eof = sizeof(data) @@ -33,10 +25,10 @@ using Test return p, cs end - @test validate_table(b"") == validate_inline(b"") == validate_goto(b"") - @test validate_table(b"a") == validate_inline(b"a") == validate_goto(b"a") - @test validate_table(b"b") == validate_inline(b"b") == validate_goto(b"b") - @test validate_table(b"ab") == validate_inline(b"ab") == validate_goto(b"ab") + @test validate_table(b"") == validate_goto(b"") + @test validate_table(b"a") == validate_goto(b"a") + @test validate_table(b"b") == validate_goto(b"b") + @test validate_table(b"ab") == validate_goto(b"ab") end end diff --git a/test/test15.jl b/test/test15.jl index f68824d2..45f35424 100644 --- a/test/test15.jl +++ b/test/test15.jl @@ -22,7 +22,8 @@ using Test @test last == 0 @test actions == [:enter, :all, :final, :exit, :enter, :all, :final, :exit] - for generator in (:table, :inline, :goto), checkbounds in (true, false), clean in (true, false) + for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) + (generator == :goto && checkbounds) && continue ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) validate = @eval function (data) logger = Symbol[] diff --git a/test/test16.jl b/test/test16.jl index 8ea63dc4..1427ff2a 100644 --- a/test/test16.jl +++ b/test/test16.jl @@ -6,29 +6,26 @@ using Test @testset "Test16" begin @test_throws ArgumentError Automa.CodeGenContext(generator=:table, loopunroll=1) - @test_throws ArgumentError Automa.CodeGenContext(generator=:inline, loopunroll=1) @test_throws ArgumentError Automa.CodeGenContext(generator=:goto, loopunroll=-1) re = re"A+(B+C)*(D|E)+" machine = Automa.compile(re) - for checkbounds in (true, false), loopunroll in 0:4 - ctx = Automa.CodeGenContext(generator=:goto, checkbounds=checkbounds, loopunroll=loopunroll) - init_code = Automa.generate_init_code(ctx, machine) - exec_code = Automa.generate_exec_code(ctx, machine) - validate = @eval function (data) - $(init_code) - p_end = p_eof = lastindex(data) - $(exec_code) - return cs == 0 - end - @test validate(b"ABCD") - @test validate(b"AABCD") - @test validate(b"AAABBCD") - @test validate(b"AAAABCD") - @test validate(b"AAAABBBBBCD") - @test validate(b"AAAAAAAAAAAAAABBBBBCBCBBBBBBBBCDE") - @test validate(b"AAAAAAAAAAAAAABBBBBCBCBBBBBBBBCDEDDDDDDEDEDDDEEEEED") + ctx = Automa.CodeGenContext(generator=:goto, checkbounds=false) + init_code = Automa.generate_init_code(ctx, machine) + exec_code = Automa.generate_exec_code(ctx, machine) + validate = @eval function (data) + $(init_code) + p_end = p_eof = lastindex(data) + $(exec_code) + return cs == 0 end + @test validate(b"ABCD") + @test validate(b"AABCD") + @test validate(b"AAABBCD") + @test validate(b"AAAABCD") + @test validate(b"AAAABBBBBCD") + @test validate(b"AAAAAAAAAAAAAABBBBBCBCBBBBBBBBCDE") + @test validate(b"AAAAAAAAAAAAAABBBBBCBCBBBBBBBBCDEDDDDDDEDEDDDEEEEED") end end diff --git a/test/test17.jl b/test/test17.jl index e6a6b9e5..aa7948b8 100644 --- a/test/test17.jl +++ b/test/test17.jl @@ -11,7 +11,8 @@ using Test machine1 = Automa.compile(re1) @test occursin(r"^Automa.Machine\(<.*>\)$", repr(machine1)) - for generator in (:table, :inline, :goto), checkbounds in (true, false), clean in (true, false) + for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) + (generator == :goto && checkbounds) && continue ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) init_code = Automa.generate_init_code(ctx, machine1) exec_code = Automa.generate_exec_code(ctx, machine1, :debug) @@ -33,7 +34,8 @@ using Test machine2 = Automa.compile(re2) @test occursin(r"^Automa.Machine\(<.*>\)$", repr(machine2)) - for generator in (:table, :inline, :goto), checkbounds in (true, false), clean in (true, false) + for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) + (generator == :goto && checkbounds) && continue ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) init_code = Automa.generate_init_code(ctx, machine2) exec_code = Automa.generate_exec_code(ctx, machine2, :debug) diff --git a/test/test18.jl b/test/test18.jl index 469e0532..b5b573c4 100644 --- a/test/test18.jl +++ b/test/test18.jl @@ -6,7 +6,8 @@ using Test @testset "Test18" begin machine = Automa.compile(re"\0\a\b\t\n\v\r\x00\xff\xFF[\\][^\\]") - for generator in (:table, :inline, :goto), checkbounds in (true, false), clean in (true, false) + for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) + (generator == :goto && checkbounds) && continue ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) init_code = Automa.generate_init_code(ctx, machine) exec_code = Automa.generate_exec_code(ctx, machine) From 93aef959c0e59bfa61abaeb6fb5837de2023a273 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Sat, 16 Jul 2022 14:35:37 +0200 Subject: [PATCH 02/64] Remove loop unrolling --- benchmark/runbenchmarks.jl | 8 +++---- docs/src/index.md | 3 +-- src/codegen.jl | 48 ++------------------------------------ test/simd.jl | 2 -- test/test16.jl | 3 --- 5 files changed, 7 insertions(+), 57 deletions(-) diff --git a/benchmark/runbenchmarks.jl b/benchmark/runbenchmarks.jl index 36c8167d..970803af 100644 --- a/benchmark/runbenchmarks.jl +++ b/benchmark/runbenchmarks.jl @@ -37,7 +37,7 @@ end @assert match(data) println("Automa.jl: ", @benchmark match(data)) -context = Automa.CodeGenContext(generator=:goto, checkbounds=false, loopunroll=10) +context = Automa.CodeGenContext(generator=:goto, checkbounds=false) @eval function match(data) $(Automa.generate_init_code(context, machine)) p_end = p_eof = lastindex(data) @@ -69,7 +69,7 @@ end @assert match(data) println("Automa.jl: ", @benchmark match(data)) -context = Automa.CodeGenContext(generator=:goto, checkbounds=false, loopunroll=10) +context = Automa.CodeGenContext(generator=:goto, checkbounds=false) @eval function match(data) $(Automa.generate_init_code(context, machine)) p_end = p_eof = lastindex(data) @@ -101,7 +101,7 @@ end @assert match(data) println("Automa.jl: ", @benchmark match(data)) -context = Automa.CodeGenContext(generator=:goto, checkbounds=false, loopunroll=10) +context = Automa.CodeGenContext(generator=:goto, checkbounds=false) @eval function match(data) $(Automa.generate_init_code(context, machine)) p_end = p_eof = lastindex(data) @@ -133,7 +133,7 @@ end @assert match(data) println("Automa.jl: ", @benchmark match(data)) -context = Automa.CodeGenContext(generator=:goto, checkbounds=false, loopunroll=10) +context = Automa.CodeGenContext(generator=:goto, checkbounds=false) @eval function match(data) $(Automa.generate_init_code(context, machine)) p_end = p_eof = lastindex(data) diff --git a/docs/src/index.md b/docs/src/index.md index b6e8fec6..f8b74364 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -389,5 +389,4 @@ Automa.jl has four kinds of code generators. The first and default one uses two Note that the `:goto` generator has more requirements than the `:table` generator: * First, `boundscheck=false` must be set -* Second, `loopunroll` must be the default `0` (as loops are SIMD unrolled) -* Third, `getbyte` must be the default `Base.getindex` +* Second, `getbyte` must be the default `Base.getindex` diff --git a/src/codegen.jl b/src/codegen.jl index 426dd151..479e1d42 100644 --- a/src/codegen.jl +++ b/src/codegen.jl @@ -32,7 +32,6 @@ struct CodeGenContext vars::Variables generator::Function checkbounds::Bool - loopunroll::Int getbyte::Function clean::Bool end @@ -42,7 +41,6 @@ end vars=Variables(:p, :p_end, :p_eof, :ts, :te, :cs, :data, gensym(), gensym()), generator=:table, checkbounds=true, - loopunroll=0, getbyte=Base.getindex, clean=false ) @@ -55,7 +53,6 @@ Arguments - `vars`: variable names used in generated code - `generator`: code generator (`:table` or `:goto`) - `checkbounds`: flag of bounds check -- `loopunroll`: loop unroll factor (≥ 0) - `getbyte`: function of byte access (i.e. `getbyte(data, p)`) - `clean`: flag of code cleansing """ @@ -63,19 +60,11 @@ function CodeGenContext(; vars::Variables=Variables(:p, :p_end, :p_eof, :ts, :te, :cs, :data, gensym(), gensym()), generator::Symbol=:table, checkbounds::Bool=generator == :table, - loopunroll::Integer=0, getbyte::Function=Base.getindex, clean::Bool=false) - if loopunroll < 0 - throw(ArgumentError("loop unroll factor must be a non-negative integer")) - elseif loopunroll > 0 && generator != :goto - throw(ArgumentError("loop unrolling is not supported for $(generator)")) - end # special conditions for simd generator if generator == :goto - if loopunroll != 0 - throw(ArgumentError("GOTO generator does not support unrolling")) - elseif getbyte != Base.getindex + if getbyte != Base.getindex throw(ArgumentError("GOTO generator only support Base.getindex")) elseif checkbounds throw(ArgumentError("GOTO generator does not support boundscheck")) @@ -89,7 +78,7 @@ function CodeGenContext(; else throw(ArgumentError("invalid code generator: $(generator)")) end - return CodeGenContext(vars, generator, checkbounds, loopunroll, getbyte, clean) + return CodeGenContext(vars, generator, checkbounds, getbyte, clean) end """ @@ -307,39 +296,6 @@ function append_code!(block::Expr, code::Expr) return block end -function generate_unrolled_loop(ctx::CodeGenContext, edge::Edge, t::Node) - # Generated code looks like this (when unroll=2): - # while p + 2 ≤ p_end - # l1 = $(getbyte)(data, p + 1) - # !$(generate_membership_code(:l1, e.labels)) && break - # l2 = $(getbyte)(data, p + 2) - # !$(generate_membership_code(:l2, e.labels)) && break - # p += 2 - # end - # @goto ... - @assert ctx.loopunroll > 0 - body = :(begin end) - for k in 1:ctx.loopunroll - l = Symbol(ctx.vars.byte, k) - push!( - body.args, - quote - $(generate_getbyte_code(ctx, l, k)) - $(generate_membership_code(l, edge.labels)) || begin - $(ctx.vars.p) += $(k-1) - break - end - end) - end - push!(body.args, :($(ctx.vars.p) += $(ctx.loopunroll))) - quote - while $(ctx.vars.p) + $(ctx.loopunroll) ≤ $(ctx.vars.p_end) - $(body) - end - @goto $(Symbol("state_", t.state)) - end -end - # Note: This function has been carefully crafted to produce (nearly) optimal # assembly code for AVX2-capable CPUs. Change with great care. function generate_simd_loop(ctx::CodeGenContext, bs::ByteSet) diff --git a/test/simd.jl b/test/simd.jl index 91ece82e..73132dee 100644 --- a/test/simd.jl +++ b/test/simd.jl @@ -1,8 +1,6 @@ # Test codegencontext @testset "CodeGenContext" begin @test_throws ArgumentError Automa.CodeGenContext(generator=:fdjfhkdj) - @test_throws ArgumentError Automa.CodeGenContext(generator=:goto) - @test_throws ArgumentError Automa.CodeGenContext(generator=:goto, checkbounds=false, loopunroll=2) @test_throws ArgumentError Automa.CodeGenContext(generator=:goto, checkbounds=false, getbyte=identity) end diff --git a/test/test16.jl b/test/test16.jl index 1427ff2a..52a85525 100644 --- a/test/test16.jl +++ b/test/test16.jl @@ -5,9 +5,6 @@ import Automa.RegExp: @re_str using Test @testset "Test16" begin - @test_throws ArgumentError Automa.CodeGenContext(generator=:table, loopunroll=1) - @test_throws ArgumentError Automa.CodeGenContext(generator=:goto, loopunroll=-1) - re = re"A+(B+C)*(D|E)+" machine = Automa.compile(re) ctx = Automa.CodeGenContext(generator=:goto, checkbounds=false) From 67556afc38dd139b2c1b0231876d82a700f19c12 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Sat, 16 Jul 2022 14:52:06 +0200 Subject: [PATCH 03/64] Allow CodeGenContext to be elided --- src/codegen.jl | 21 +++++++++++++++++---- test/test03.jl | 14 ++++++++++---- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/codegen.jl b/src/codegen.jl index 479e1d42..a70586ed 100644 --- a/src/codegen.jl +++ b/src/codegen.jl @@ -36,9 +36,13 @@ struct CodeGenContext clean::Bool end +# Add these here so they can be used in CodeGenContext below +function generate_table_code end +function generate_goto_code end + """ CodeGenContext(; - vars=Variables(:p, :p_end, :p_eof, :ts, :te, :cs, :data, gensym(), gensym()), + vars=Variables(:p, :p_end, :p_eof, :ts, :te, :cs, :data, gensym(), :byte), generator=:table, checkbounds=true, getbyte=Base.getindex, @@ -57,7 +61,7 @@ Arguments - `clean`: flag of code cleansing """ function CodeGenContext(; - vars::Variables=Variables(:p, :p_end, :p_eof, :ts, :te, :cs, :data, gensym(), gensym()), + vars::Variables=Variables(:p, :p_end, :p_eof, :ts, :te, :cs, :data, gensym(), :byte), generator::Symbol=:table, checkbounds::Bool=generator == :table, getbyte::Function=Base.getindex, @@ -81,10 +85,13 @@ function CodeGenContext(; return CodeGenContext(vars, generator, checkbounds, getbyte, clean) end +const DefaultCodeGenContext = CodeGenContext() + """ - generate_init_code(context::CodeGenContext, machine::Machine)::Expr + generate_init_code([::CodeGenContext], machine::Machine)::Expr Generate variable initialization code. +If not passed, the context defaults to `DefaultCodeGenContext` """ function generate_init_code(ctx::CodeGenContext, machine::Machine) return quote @@ -94,11 +101,13 @@ function generate_init_code(ctx::CodeGenContext, machine::Machine) $(ctx.vars.cs)::Int = $(machine.start_state) end end +generate_init_code(machine::Machine) = generate_init_code(DefaultCodeGenContext, machine) """ - generate_exec_code(ctx::CodeGenContext, machine::Machine, actions=nothing)::Expr + generate_exec_code([::CodeGenContext], machine::Machine, actions=nothing)::Expr Generate machine execution code with actions. +If not passed, the context defaults to `DefaultCodeGenContext` """ function generate_exec_code(ctx::CodeGenContext, machine::Machine, actions=nothing) # make actions @@ -119,6 +128,10 @@ function generate_exec_code(ctx::CodeGenContext, machine::Machine, actions=nothi return code end +function generate_exec_code(machine::Machine, actions=nothing) + generate_exec_code(DefaultCodeGenContext, machine, actions) +end + function generate_table_code(ctx::CodeGenContext, machine::Machine, actions::Dict{Symbol,Expr}) action_dispatch_code, set_act_code = generate_action_dispatch_code(ctx, machine, actions) trans_table = generate_transition_table(machine) diff --git a/test/test03.jl b/test/test03.jl index 7421ce90..0b0519fb 100644 --- a/test/test03.jl +++ b/test/test03.jl @@ -14,10 +14,16 @@ using Test machine = Automa.compile(fasta) for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) - (generator == :goto && checkbounds) && continue - ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) - init_code = Automa.generate_init_code(ctx, machine) - exec_code = Automa.generate_exec_code(ctx, machine) + # Test the default CTX, if none is passed. + # We use the otherwise invalid combinarion :goto && checkbounds to do this + if generator == :goto && checkbounds + init_code = Automa.generate_init_code(machine) + exec_code = Automa.generate_exec_code(machine) + else + ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) + init_code = Automa.generate_init_code(ctx, machine) + exec_code = Automa.generate_exec_code(ctx, machine) + end validate = @eval function (data) $(init_code) p_end = p_eof = lastindex(data) From 088b9e4a9b485d3ff461a0f0e030f8233e1ec507 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Sat, 16 Jul 2022 15:03:41 +0200 Subject: [PATCH 04/64] Update benchmarks --- benchmark/runbenchmarks.jl | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/benchmark/runbenchmarks.jl b/benchmark/runbenchmarks.jl index 970803af..8aefc4a9 100644 --- a/benchmark/runbenchmarks.jl +++ b/benchmark/runbenchmarks.jl @@ -27,7 +27,7 @@ println("PCRE: ", @benchmark match(data)) machine = Automa.compile(re"([A-z]*\r?\n)*") VISUALIZE && writesvg("case1", machine) -context = Automa.CodeGenContext(generator=:goto, checkbounds=false) +context = Automa.CodeGenContext() @eval function match(data) $(Automa.generate_init_code(context, machine)) p_end = p_eof = lastindex(data) @@ -37,7 +37,7 @@ end @assert match(data) println("Automa.jl: ", @benchmark match(data)) -context = Automa.CodeGenContext(generator=:goto, checkbounds=false) +context = Automa.CodeGenContext(generator=:goto) @eval function match(data) $(Automa.generate_init_code(context, machine)) p_end = p_eof = lastindex(data) @@ -45,7 +45,7 @@ context = Automa.CodeGenContext(generator=:goto, checkbounds=false) return cs == 0 end @assert match(data) -println("Automa.jl (unrolled): ", @benchmark match(data)) +println("Automa.jl (goto): ", @benchmark match(data)) # Case 2 @@ -59,7 +59,7 @@ println("PCRE: ", @benchmark match(data)) machine = Automa.compile(re"([A-Za-z]*\r?\n)*") VISUALIZE && writesvg("case2", machine) -context = Automa.CodeGenContext(generator=:goto, checkbounds=false) +context = Automa.CodeGenContext() @eval function match(data) $(Automa.generate_init_code(context, machine)) p_end = p_eof = lastindex(data) @@ -69,7 +69,7 @@ end @assert match(data) println("Automa.jl: ", @benchmark match(data)) -context = Automa.CodeGenContext(generator=:goto, checkbounds=false) +context = Automa.CodeGenContext(generator=:goto) @eval function match(data) $(Automa.generate_init_code(context, machine)) p_end = p_eof = lastindex(data) @@ -77,7 +77,7 @@ context = Automa.CodeGenContext(generator=:goto, checkbounds=false) return cs == 0 end @assert match(data) -println("Automa.jl (unrolled): ", @benchmark match(data)) +println("Automa.jl (goto): ", @benchmark match(data)) # Case 3 @@ -91,7 +91,7 @@ println("PCRE: ", @benchmark match(data)) machine = Automa.compile(re"([ACGTacgt]*\r?\n)*") VISUALIZE && writesvg("case3", machine) -context = Automa.CodeGenContext(generator=:goto, checkbounds=false) +context = Automa.CodeGenContext() @eval function match(data) $(Automa.generate_init_code(context, machine)) p_end = p_eof = lastindex(data) @@ -101,7 +101,7 @@ end @assert match(data) println("Automa.jl: ", @benchmark match(data)) -context = Automa.CodeGenContext(generator=:goto, checkbounds=false) +context = Automa.CodeGenContext(generator=:goto) @eval function match(data) $(Automa.generate_init_code(context, machine)) p_end = p_eof = lastindex(data) @@ -109,7 +109,7 @@ context = Automa.CodeGenContext(generator=:goto, checkbounds=false) return cs == 0 end @assert match(data) -println("Automa.jl (unrolled): ", @benchmark match(data)) +println("Automa.jl (goto): ", @benchmark match(data)) # Case 4 @@ -123,7 +123,7 @@ println("PCRE: ", @benchmark match(data)) machine = Automa.compile(re"([A-Za-z\*-]*\r?\n)*") VISUALIZE && writesvg("case4", machine) -context = Automa.CodeGenContext(generator=:goto, checkbounds=false) +context = Automa.CodeGenContext() @eval function match(data) $(Automa.generate_init_code(context, machine)) p_end = p_eof = lastindex(data) @@ -133,7 +133,7 @@ end @assert match(data) println("Automa.jl: ", @benchmark match(data)) -context = Automa.CodeGenContext(generator=:goto, checkbounds=false) +context = Automa.CodeGenContext(generator=:goto) @eval function match(data) $(Automa.generate_init_code(context, machine)) p_end = p_eof = lastindex(data) @@ -141,4 +141,4 @@ context = Automa.CodeGenContext(generator=:goto, checkbounds=false) return cs == 0 end @assert match(data) -println("Automa.jl (unrolled): ", @benchmark match(data)) +println("Automa.jl (goto): ", @benchmark match(data)) From 8ad05033532143476932b6c1a89116f7ccff245c Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Sun, 17 Jul 2022 13:03:02 +0200 Subject: [PATCH 05/64] Saner default for p_eof and p_end --- README.md | 1 - benchmark/runbenchmarks.jl | 8 -------- docs/src/index.md | 8 ++------ example/fasta.jl | 1 - example/numbers.jl | 1 - example/tokenizer.jl | 1 - src/Stream.jl | 4 ++++ src/codegen.jl | 9 +++++---- src/tokenizer.jl | 4 ++-- test/simd.jl | 1 - test/test01.jl | 1 - test/test02.jl | 1 - test/test03.jl | 1 - test/test04.jl | 1 - test/test05.jl | 1 - test/test06.jl | 10 +++++----- test/test07.jl | 3 --- test/test08.jl | 1 - test/test09.jl | 1 - test/test11.jl | 1 - test/test12.jl | 1 - test/test14.jl | 2 -- test/test15.jl | 1 - test/test16.jl | 1 - test/test17.jl | 2 -- test/test18.jl | 1 - 26 files changed, 18 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index fb702b40..5afad714 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,6 @@ context = Automa.CodeGenContext() tokens = Tuple{Symbol,String}[] mark = 0 $(Automa.generate_init_code(context, machine)) - p_end = p_eof = lastindex(data) emit(kind) = push!(tokens, (kind, data[mark:p-1])) $(Automa.generate_exec_code(context, machine, actions)) return tokens, cs == 0 ? :ok : cs < 0 ? :error : :incomplete diff --git a/benchmark/runbenchmarks.jl b/benchmark/runbenchmarks.jl index 8aefc4a9..4ad95708 100644 --- a/benchmark/runbenchmarks.jl +++ b/benchmark/runbenchmarks.jl @@ -30,7 +30,6 @@ VISUALIZE && writesvg("case1", machine) context = Automa.CodeGenContext() @eval function match(data) $(Automa.generate_init_code(context, machine)) - p_end = p_eof = lastindex(data) $(Automa.generate_exec_code(context, machine)) return cs == 0 end @@ -40,7 +39,6 @@ println("Automa.jl: ", @benchmark match(data)) context = Automa.CodeGenContext(generator=:goto) @eval function match(data) $(Automa.generate_init_code(context, machine)) - p_end = p_eof = lastindex(data) $(Automa.generate_exec_code(context, machine)) return cs == 0 end @@ -62,7 +60,6 @@ VISUALIZE && writesvg("case2", machine) context = Automa.CodeGenContext() @eval function match(data) $(Automa.generate_init_code(context, machine)) - p_end = p_eof = lastindex(data) $(Automa.generate_exec_code(context, machine)) return cs == 0 end @@ -72,7 +69,6 @@ println("Automa.jl: ", @benchmark match(data)) context = Automa.CodeGenContext(generator=:goto) @eval function match(data) $(Automa.generate_init_code(context, machine)) - p_end = p_eof = lastindex(data) $(Automa.generate_exec_code(context, machine)) return cs == 0 end @@ -94,7 +90,6 @@ VISUALIZE && writesvg("case3", machine) context = Automa.CodeGenContext() @eval function match(data) $(Automa.generate_init_code(context, machine)) - p_end = p_eof = lastindex(data) $(Automa.generate_exec_code(context, machine)) return cs == 0 end @@ -104,7 +99,6 @@ println("Automa.jl: ", @benchmark match(data)) context = Automa.CodeGenContext(generator=:goto) @eval function match(data) $(Automa.generate_init_code(context, machine)) - p_end = p_eof = lastindex(data) $(Automa.generate_exec_code(context, machine)) return cs == 0 end @@ -126,7 +120,6 @@ VISUALIZE && writesvg("case4", machine) context = Automa.CodeGenContext() @eval function match(data) $(Automa.generate_init_code(context, machine)) - p_end = p_eof = lastindex(data) $(Automa.generate_exec_code(context, machine)) return cs == 0 end @@ -136,7 +129,6 @@ println("Automa.jl: ", @benchmark match(data)) context = Automa.CodeGenContext(generator=:goto) @eval function match(data) $(Automa.generate_init_code(context, machine)) - p_end = p_eof = lastindex(data) $(Automa.generate_exec_code(context, machine)) return cs == 0 end diff --git a/docs/src/index.md b/docs/src/index.md index f8b74364..db99479d 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -52,7 +52,6 @@ context = Automa.CodeGenContext() tokens = Tuple{Symbol,String}[] mark = 0 $(Automa.generate_init_code(context, machine)) - p_end = p_eof = lastindex(data) emit(kind) = push!(tokens, (kind, data[mark:p-1])) $(Automa.generate_exec_code(context, machine, actions)) return tokens, cs == 0 ? :ok : cs < 0 ? :error : :incomplete @@ -250,9 +249,6 @@ context = Automa.CodeGenContext() # generate code to initialize variables used by FSM $(Automa.generate_init_code(context, machine)) - # set end and EOF positions of data buffer - p_end = p_eof = lastindex(data) - # generate code to execute FSM $(Automa.generate_exec_code(context, machine, actions)) @@ -297,8 +293,8 @@ generates variable declatarions used by the finite state machine (FSM). julia> Automa.generate_init_code(context, machine) quote # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 67: p::Int = 1 # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 68: - p_end::Int = 0 # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 69: - p_eof::Int = -1 # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 70: + p_end::Int = sizeof(data) # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 69: + p_eof::Int = p_end # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 70: cs::Int = 1 end diff --git a/example/fasta.jl b/example/fasta.jl index 5ce0ef49..2a06f4dd 100644 --- a/example/fasta.jl +++ b/example/fasta.jl @@ -63,7 +63,6 @@ context = Automa.CodeGenContext(generator=:goto, checkbounds=false) # Initialize variables used by the state machine. $(Automa.generate_init_code(context, fasta_machine)) - p_end = p_eof = lastindex(data) # This is the main loop to iterate over the input data. $(Automa.generate_exec_code(context, fasta_machine, fasta_actions)) diff --git a/example/numbers.jl b/example/numbers.jl index a7ce5ee9..070bdd36 100644 --- a/example/numbers.jl +++ b/example/numbers.jl @@ -43,7 +43,6 @@ context = Automa.CodeGenContext() tokens = Tuple{Symbol,String}[] mark = 0 $(Automa.generate_init_code(context, machine)) - p_end = p_eof = lastindex(data) emit(kind) = push!(tokens, (kind, data[mark:p-1])) $(Automa.generate_exec_code(context, machine, actions)) return tokens, cs == 0 ? :ok : cs < 0 ? :error : :incomplete diff --git a/example/tokenizer.jl b/example/tokenizer.jl index c001e178..dda38ce3 100644 --- a/example/tokenizer.jl +++ b/example/tokenizer.jl @@ -50,7 +50,6 @@ run(`dot -Tsvg -o minijulia.svg minijulia.dot`) context = Automa.CodeGenContext() @eval function tokenize(data) $(Automa.generate_init_code(context, minijulia)) - p_end = p_eof = sizeof(data) tokens = Tuple{Symbol,String}[] emit(kind) = push!(tokens, (kind, data[ts:te])) while p ≤ p_eof && cs > 0 diff --git a/src/Stream.jl b/src/Stream.jl index daa1eb85..d62cae86 100644 --- a/src/Stream.jl +++ b/src/Stream.jl @@ -110,6 +110,10 @@ function generate_reader( buffer = stream.state.buffer1 data = buffer.data $(Automa.generate_init_code(context, machine)) + # Overwrite these for Stream, since we don't know EOF or end, + # as this is set in the __exec__ part depending on the stream state. + $(context.vars.p_end) = 0 + $(context.vars.p_eof) = -1 $(initcode) @label __exec__ diff --git a/src/codegen.jl b/src/codegen.jl index a70586ed..7d1442a0 100644 --- a/src/codegen.jl +++ b/src/codegen.jl @@ -94,11 +94,12 @@ Generate variable initialization code. If not passed, the context defaults to `DefaultCodeGenContext` """ function generate_init_code(ctx::CodeGenContext, machine::Machine) + vars = ctx.vars return quote - $(ctx.vars.p)::Int = 1 - $(ctx.vars.p_end)::Int = 0 - $(ctx.vars.p_eof)::Int = -1 - $(ctx.vars.cs)::Int = $(machine.start_state) + $(vars.p)::Int = 1 + $(vars.p_end)::Int = sizeof($(vars.data)) + $(vars.p_eof)::Int = $(vars.p_end) + $(vars.cs)::Int = $(machine.start_state) end end generate_init_code(machine::Machine) = generate_init_code(DefaultCodeGenContext, machine) diff --git a/src/tokenizer.jl b/src/tokenizer.jl index ba860d18..d7fea0f4 100644 --- a/src/tokenizer.jl +++ b/src/tokenizer.jl @@ -49,8 +49,8 @@ end function generate_init_code(ctx::CodeGenContext, tokenizer::Tokenizer) quote $(ctx.vars.p)::Int = 1 - $(ctx.vars.p_end)::Int = 0 - $(ctx.vars.p_eof)::Int = -1 + $(ctx.vars.p_end)::Int = sizeof($(ctx.vars.data)) + $(ctx.vars.p_eof)::Int = $(ctx.vars.p_end) $(ctx.vars.ts)::Int = 0 $(ctx.vars.te)::Int = 0 $(ctx.vars.cs)::Int = $(tokenizer.machine.start_state) diff --git a/test/simd.jl b/test/simd.jl index 73132dee..d1e4c2e5 100644 --- a/test/simd.jl +++ b/test/simd.jl @@ -20,7 +20,6 @@ import Automa.RegExp: @re_str @eval function is_valid_fasta(data::String) $(Automa.generate_init_code(context, machine)) - p_end = p_eof = ncodeunits(data) $(Automa.generate_exec_code(context, machine, nothing)) return p == ncodeunits(data) + 1 end diff --git a/test/test01.jl b/test/test01.jl index da8d9668..dde98589 100644 --- a/test/test01.jl +++ b/test/test01.jl @@ -19,7 +19,6 @@ using Test validate = @eval function (data) logger = Symbol[] $(init_code) - p_end = p_eof = lastindex(data) $(exec_code) return cs == 0, logger end diff --git a/test/test02.jl b/test/test02.jl index 271c0ea8..3ea94fae 100644 --- a/test/test02.jl +++ b/test/test02.jl @@ -35,7 +35,6 @@ using Test validate = @eval function (data) logger = Symbol[] $(init_code) - p_end = p_eof = lastindex(data) $(exec_code) return cs == 0, logger end diff --git a/test/test03.jl b/test/test03.jl index 0b0519fb..a25394ae 100644 --- a/test/test03.jl +++ b/test/test03.jl @@ -26,7 +26,6 @@ using Test end validate = @eval function (data) $(init_code) - p_end = p_eof = lastindex(data) $(exec_code) return cs == 0 end diff --git a/test/test04.jl b/test/test04.jl index 2a75bf50..917c46cf 100644 --- a/test/test04.jl +++ b/test/test04.jl @@ -19,7 +19,6 @@ using Test exec_code = Automa.generate_exec_code(ctx, machine) validate = @eval function (data) $(init_code) - p_end = p_eof = lastindex(data) $(exec_code) return cs == 0 end diff --git a/test/test05.jl b/test/test05.jl index 5eb21bd6..34542cb5 100644 --- a/test/test05.jl +++ b/test/test05.jl @@ -24,7 +24,6 @@ using Test validate = @eval function (data) logger = Symbol[] $(init_code) - p_end = p_eof = lastindex(data) $(exec_code) return cs == 0, logger end diff --git a/test/test06.jl b/test/test06.jl index 652352be..1d0ede2b 100644 --- a/test/test06.jl +++ b/test/test06.jl @@ -17,8 +17,8 @@ using Test @eval mutable struct MachineState p::Int cs::Int - function MachineState() - $(Automa.generate_init_code(Automa.CodeGenContext(), machine)) + function MachineState(data) + $(Automa.generate_init_code(machine)) return new(p, cs) end end @@ -28,16 +28,16 @@ using Test ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) run! = @eval function (state, data) ret = [] + $(Automa.generate_init_code(machine)) p = state.p cs = state.cs - p_end = p_eof = lastindex(data) - $(Automa.generate_exec_code(ctx, machine, actions)) + $(Automa.generate_exec_code(machine, actions)) state.p = p state.cs = cs return ret end - state = MachineState() data = b"foo foofoo foo" + state = MachineState(data) @test run!(state, data) == [1:3] @test run!(state, data) == [5:7] @test run!(state, data) == [9:10] diff --git a/test/test07.jl b/test/test07.jl index acd50fd1..33af2335 100644 --- a/test/test07.jl +++ b/test/test07.jl @@ -10,7 +10,6 @@ using Test ctx = Automa.CodeGenContext() @eval function ismatch1(data) $(Automa.generate_init_code(ctx, machine)) - p_end = p_eof = lastindex(data) $(Automa.generate_exec_code(ctx, machine)) return cs == 0 end @@ -25,7 +24,6 @@ using Test ctx = Automa.CodeGenContext() @eval function ismatch2(data) $(Automa.generate_init_code(ctx, machine)) - p_end = p_eof = lastindex(data) $(Automa.generate_exec_code(ctx, machine)) return cs == 0 end @@ -41,7 +39,6 @@ using Test ctx = Automa.CodeGenContext() @eval function ismatch3(data) $(Automa.generate_init_code(ctx, machine)) - p_end = p_eof = lastindex(data) $(Automa.generate_exec_code(ctx, machine)) return cs == 0 end diff --git a/test/test08.jl b/test/test08.jl index 96f1e7b6..7de056be 100644 --- a/test/test08.jl +++ b/test/test08.jl @@ -31,7 +31,6 @@ using Test tokens = Tuple{Symbol,String}[] mark = 0 $(Automa.generate_init_code(ctx, machine)) - p_end = p_eof = lastindex(data) $(Automa.generate_exec_code(ctx, machine, actions)) return tokens, cs == 0 ? :ok : cs < 0 ? :error : :incomplete end diff --git a/test/test09.jl b/test/test09.jl index e48b99f1..bfe244f7 100644 --- a/test/test09.jl +++ b/test/test09.jl @@ -15,7 +15,6 @@ using Test @eval function tokenize(data) $(Automa.generate_init_code(ctx, tokenizer)) - p_end = p_eof = sizeof(data) tokens = Tuple{Symbol,String}[] emit(kind, range) = push!(tokens, (kind, data[range])) while p ≤ p_eof && cs > 0 diff --git a/test/test11.jl b/test/test11.jl index 5d3b123d..b4348cdf 100644 --- a/test/test11.jl +++ b/test/test11.jl @@ -27,7 +27,6 @@ using Test validate = @eval function (data, n) logger = Symbol[] $(Automa.generate_init_code(ctx, machine)) - p_end = p_eof = sizeof(data) $(Automa.generate_exec_code(ctx, machine, actions)) return logger, cs == 0 ? :ok : cs < 0 ? :error : :incomplete end diff --git a/test/test12.jl b/test/test12.jl index 3ae888dc..7616def5 100644 --- a/test/test12.jl +++ b/test/test12.jl @@ -14,7 +14,6 @@ using Test @eval function validate(data) logger = Symbol[] $(Automa.generate_init_code(ctx, machine)) - p_end = p_eof = sizeof(data) $(Automa.generate_exec_code(ctx, machine, :debug)) return logger, cs == 0 ? :ok : cs < 0 ? :error : :incomplete end diff --git a/test/test14.jl b/test/test14.jl index 04b23685..5dd6b675 100644 --- a/test/test14.jl +++ b/test/test14.jl @@ -12,7 +12,6 @@ using Test ctx = Automa.CodeGenContext(generator=:table) @eval function validate_table(data) $(Automa.generate_init_code(ctx, machine)) - p_end = p_eof = sizeof(data) $(Automa.generate_exec_code(ctx, machine)) return p, cs end @@ -20,7 +19,6 @@ using Test ctx = Automa.CodeGenContext(generator=:goto, checkbounds=false) @eval function validate_goto(data) $(Automa.generate_init_code(ctx, machine)) - p_end = p_eof = sizeof(data) $(Automa.generate_exec_code(ctx, machine)) return p, cs end diff --git a/test/test15.jl b/test/test15.jl index 45f35424..77bd670a 100644 --- a/test/test15.jl +++ b/test/test15.jl @@ -28,7 +28,6 @@ using Test validate = @eval function (data) logger = Symbol[] $(Automa.generate_init_code(ctx, machine)) - p_end = p_eof = sizeof(data) $(Automa.generate_exec_code(ctx, machine, :debug)) return logger, cs == 0 ? :ok : cs < 0 ? :error : :incomplete end diff --git a/test/test16.jl b/test/test16.jl index 52a85525..282b72b0 100644 --- a/test/test16.jl +++ b/test/test16.jl @@ -12,7 +12,6 @@ using Test exec_code = Automa.generate_exec_code(ctx, machine) validate = @eval function (data) $(init_code) - p_end = p_eof = lastindex(data) $(exec_code) return cs == 0 end diff --git a/test/test17.jl b/test/test17.jl index aa7948b8..e30f057e 100644 --- a/test/test17.jl +++ b/test/test17.jl @@ -19,7 +19,6 @@ using Test validate = @eval function (data) logger = Symbol[] $(init_code) - p_end = p_eof = lastindex(data) $(exec_code) return cs == 0, logger end @@ -42,7 +41,6 @@ using Test validate = @eval function (data) logger = Symbol[] $(init_code) - p_end = p_eof = lastindex(data) $(exec_code) return cs == 0, logger end diff --git a/test/test18.jl b/test/test18.jl index b5b573c4..40063fc1 100644 --- a/test/test18.jl +++ b/test/test18.jl @@ -13,7 +13,6 @@ using Test exec_code = Automa.generate_exec_code(ctx, machine) validate = @eval function (data) $(init_code) - p_end = p_eof = lastindex(data) $(exec_code) return cs == 0 end From 1e95ee44f7ae76d1e5cba4f5f8c2e72065390d71 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Sun, 17 Jul 2022 13:23:20 +0200 Subject: [PATCH 06/64] Add generate_code convenience function --- benchmark/runbenchmarks.jl | 24 ++++++++---------------- docs/src/index.md | 7 ++----- docs/src/references.md | 1 + example/fasta.jl | 7 ++----- src/codegen.jl | 14 ++++++++++++++ test/simd.jl | 3 +-- test/test01.jl | 6 ++---- test/test02.jl | 6 ++---- test/test03.jl | 11 ++++------- test/test05.jl | 6 ++---- test/test07.jl | 9 +++------ test/test08.jl | 3 +-- test/test11.jl | 3 +-- test/test12.jl | 3 +-- test/test14.jl | 6 ++---- test/test15.jl | 3 +-- test/test16.jl | 6 ++---- test/test17.jl | 12 ++++-------- test/test18.jl | 6 ++---- 19 files changed, 55 insertions(+), 81 deletions(-) diff --git a/benchmark/runbenchmarks.jl b/benchmark/runbenchmarks.jl index 4ad95708..208f809c 100644 --- a/benchmark/runbenchmarks.jl +++ b/benchmark/runbenchmarks.jl @@ -29,8 +29,7 @@ machine = Automa.compile(re"([A-z]*\r?\n)*") VISUALIZE && writesvg("case1", machine) context = Automa.CodeGenContext() @eval function match(data) - $(Automa.generate_init_code(context, machine)) - $(Automa.generate_exec_code(context, machine)) + $(Automa.generate_code(context, machine)) return cs == 0 end @assert match(data) @@ -38,8 +37,7 @@ println("Automa.jl: ", @benchmark match(data)) context = Automa.CodeGenContext(generator=:goto) @eval function match(data) - $(Automa.generate_init_code(context, machine)) - $(Automa.generate_exec_code(context, machine)) + $(Automa.generate_code(context, machine)) return cs == 0 end @assert match(data) @@ -59,8 +57,7 @@ machine = Automa.compile(re"([A-Za-z]*\r?\n)*") VISUALIZE && writesvg("case2", machine) context = Automa.CodeGenContext() @eval function match(data) - $(Automa.generate_init_code(context, machine)) - $(Automa.generate_exec_code(context, machine)) + $(Automa.generate_code(context, machine)) return cs == 0 end @assert match(data) @@ -68,8 +65,7 @@ println("Automa.jl: ", @benchmark match(data)) context = Automa.CodeGenContext(generator=:goto) @eval function match(data) - $(Automa.generate_init_code(context, machine)) - $(Automa.generate_exec_code(context, machine)) + $(Automa.generate_code(context, machine)) return cs == 0 end @assert match(data) @@ -89,8 +85,7 @@ machine = Automa.compile(re"([ACGTacgt]*\r?\n)*") VISUALIZE && writesvg("case3", machine) context = Automa.CodeGenContext() @eval function match(data) - $(Automa.generate_init_code(context, machine)) - $(Automa.generate_exec_code(context, machine)) + $(Automa.generate_code(context, machine)) return cs == 0 end @assert match(data) @@ -98,8 +93,7 @@ println("Automa.jl: ", @benchmark match(data)) context = Automa.CodeGenContext(generator=:goto) @eval function match(data) - $(Automa.generate_init_code(context, machine)) - $(Automa.generate_exec_code(context, machine)) + $(Automa.generate_code(context, machine)) return cs == 0 end @assert match(data) @@ -119,8 +113,7 @@ machine = Automa.compile(re"([A-Za-z\*-]*\r?\n)*") VISUALIZE && writesvg("case4", machine) context = Automa.CodeGenContext() @eval function match(data) - $(Automa.generate_init_code(context, machine)) - $(Automa.generate_exec_code(context, machine)) + $(Automa.generate_code(context, machine)) return cs == 0 end @assert match(data) @@ -128,8 +121,7 @@ println("Automa.jl: ", @benchmark match(data)) context = Automa.CodeGenContext(generator=:goto) @eval function match(data) - $(Automa.generate_init_code(context, machine)) - $(Automa.generate_exec_code(context, machine)) + $(Automa.generate_code(context, machine)) return cs == 0 end @assert match(data) diff --git a/docs/src/index.md b/docs/src/index.md index db99479d..9578ad02 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -246,11 +246,8 @@ context = Automa.CodeGenContext() # initialize a result variable count = 0 - # generate code to initialize variables used by FSM - $(Automa.generate_init_code(context, machine)) - - # generate code to execute FSM - $(Automa.generate_exec_code(context, machine, actions)) + # Generate code to initialize FSM and execute main loop + $(Automa.generate_code(context, machine)) # check if FSM properly finished if cs != 0 diff --git a/docs/src/references.md b/docs/src/references.md index 3ba9d073..7ddb5a75 100644 --- a/docs/src/references.md +++ b/docs/src/references.md @@ -16,6 +16,7 @@ Code generator ```@docs Automa.Variables Automa.CodeGenContext +Automa.generate_code Automa.generate_init_code Automa.generate_exec_code ``` diff --git a/example/fasta.jl b/example/fasta.jl index 2a06f4dd..39be125a 100644 --- a/example/fasta.jl +++ b/example/fasta.jl @@ -61,11 +61,8 @@ context = Automa.CodeGenContext(generator=:goto, checkbounds=false) identifier = description = "" buffer = IOBuffer() - # Initialize variables used by the state machine. - $(Automa.generate_init_code(context, fasta_machine)) - - # This is the main loop to iterate over the input data. - $(Automa.generate_exec_code(context, fasta_machine, fasta_actions)) + # Generate code for initialization and main loop + $(Automa.generate_code(context, fasta_machine, fasta_actions)) # Check the last state the machine reached. if cs != 0 diff --git a/src/codegen.jl b/src/codegen.jl index 7d1442a0..19aad95e 100644 --- a/src/codegen.jl +++ b/src/codegen.jl @@ -87,6 +87,20 @@ end const DefaultCodeGenContext = CodeGenContext() +""" + generate_code([::CodeGenContext], machine::Machine, actions=nothing)::Expr + +Generate init and exec code for `machine`. +Shorthand for `generate_init_code(ctx, machine); generate_action_code(ctx, machine, actions)` +""" +function generate_code(ctx::CodeGenContext, machine::Machine, actions=nothing) + return quote + $(generate_init_code(ctx, machine)) + $(generate_exec_code(ctx, machine, actions)) + end +end +generate_code(machine::Machine, actions=nothing) = generate_code(DefaultCodeGenContext, machine, actions) + """ generate_init_code([::CodeGenContext], machine::Machine)::Expr diff --git a/test/simd.jl b/test/simd.jl index d1e4c2e5..6c979f3c 100644 --- a/test/simd.jl +++ b/test/simd.jl @@ -19,8 +19,7 @@ import Automa.RegExp: @re_str context = Automa.CodeGenContext(generator=:goto, checkbounds=false) @eval function is_valid_fasta(data::String) - $(Automa.generate_init_code(context, machine)) - $(Automa.generate_exec_code(context, machine, nothing)) + $(Automa.generate_code(context, machine)) return p == ncodeunits(data) + 1 end diff --git a/test/test01.jl b/test/test01.jl index dde98589..3550ae0f 100644 --- a/test/test01.jl +++ b/test/test01.jl @@ -14,12 +14,10 @@ using Test for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) (generator == :goto && checkbounds) && continue ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) - init_code = Automa.generate_init_code(ctx, machine) - exec_code = Automa.generate_exec_code(ctx, machine, :debug) + code = Automa.generate_code(ctx, machine, :debug) validate = @eval function (data) logger = Symbol[] - $(init_code) - $(exec_code) + $(code) return cs == 0, logger end @test validate(b"") == (true, [:enter, :exit]) diff --git a/test/test02.jl b/test/test02.jl index 3ea94fae..8be6723b 100644 --- a/test/test02.jl +++ b/test/test02.jl @@ -30,12 +30,10 @@ using Test for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) (generator == :goto && checkbounds) && continue ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) - init_code = Automa.generate_init_code(ctx, machine) - exec_code = Automa.generate_exec_code(ctx, machine, :debug) + code = (Automa.generate_code(ctx, machine, :debug)) validate = @eval function (data) logger = Symbol[] - $(init_code) - $(exec_code) + $(code) return cs == 0, logger end @test validate(b"b") == (true, [:enter_re,:enter_a,:exit_a,:enter_b,:final_b,:final_re,:exit_b,:exit_re]) diff --git a/test/test03.jl b/test/test03.jl index a25394ae..7772b6bb 100644 --- a/test/test03.jl +++ b/test/test03.jl @@ -16,17 +16,14 @@ using Test for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) # Test the default CTX, if none is passed. # We use the otherwise invalid combinarion :goto && checkbounds to do this - if generator == :goto && checkbounds - init_code = Automa.generate_init_code(machine) - exec_code = Automa.generate_exec_code(machine) + code = if generator == :goto && checkbounds + Automa.generate_code(machine) else ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) - init_code = Automa.generate_init_code(ctx, machine) - exec_code = Automa.generate_exec_code(ctx, machine) + Automa.generate_code(ctx, machine) end validate = @eval function (data) - $(init_code) - $(exec_code) + $(code) return cs == 0 end @test validate(b"") == true diff --git a/test/test05.jl b/test/test05.jl index 34542cb5..9f63cda3 100644 --- a/test/test05.jl +++ b/test/test05.jl @@ -19,12 +19,10 @@ using Test for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) (generator == :goto && checkbounds) && continue ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) - init_code = Automa.generate_init_code(ctx, machine) - exec_code = Automa.generate_exec_code(ctx, machine, :debug) + code = Automa.generate_code(ctx, machine, :debug) validate = @eval function (data) logger = Symbol[] - $(init_code) - $(exec_code) + $(code) return cs == 0, logger end @test validate(b"if") == (true, [:keyword]) diff --git a/test/test07.jl b/test/test07.jl index 33af2335..64b994b9 100644 --- a/test/test07.jl +++ b/test/test07.jl @@ -9,8 +9,7 @@ using Test machine = Automa.compile(re1) ctx = Automa.CodeGenContext() @eval function ismatch1(data) - $(Automa.generate_init_code(ctx, machine)) - $(Automa.generate_exec_code(ctx, machine)) + $(Automa.generate_code(ctx, machine)) return cs == 0 end @test ismatch1(b"ab") @@ -23,8 +22,7 @@ using Test machine = Automa.compile(re2) ctx = Automa.CodeGenContext() @eval function ismatch2(data) - $(Automa.generate_init_code(ctx, machine)) - $(Automa.generate_exec_code(ctx, machine)) + $(Automa.generate_code(ctx, machine)) return cs == 0 end @test ismatch2(b"ab") @@ -38,8 +36,7 @@ using Test machine = Automa.compile(re3) ctx = Automa.CodeGenContext() @eval function ismatch3(data) - $(Automa.generate_init_code(ctx, machine)) - $(Automa.generate_exec_code(ctx, machine)) + $(Automa.generate_code(ctx, machine)) return cs == 0 end @test ismatch3(b"a.*b") diff --git a/test/test08.jl b/test/test08.jl index 7de056be..674017e2 100644 --- a/test/test08.jl +++ b/test/test08.jl @@ -30,8 +30,7 @@ using Test @eval function tokenize(data) tokens = Tuple{Symbol,String}[] mark = 0 - $(Automa.generate_init_code(ctx, machine)) - $(Automa.generate_exec_code(ctx, machine, actions)) + $(Automa.generate_code(ctx, machine, actions)) return tokens, cs == 0 ? :ok : cs < 0 ? :error : :incomplete end diff --git a/test/test11.jl b/test/test11.jl index b4348cdf..a9f10a8a 100644 --- a/test/test11.jl +++ b/test/test11.jl @@ -26,8 +26,7 @@ using Test ctx = Automa.CodeGenContext(generator=:goto, checkbounds=false, clean=clean) validate = @eval function (data, n) logger = Symbol[] - $(Automa.generate_init_code(ctx, machine)) - $(Automa.generate_exec_code(ctx, machine, actions)) + $(Automa.generate_code(ctx, machine, actions)) return logger, cs == 0 ? :ok : cs < 0 ? :error : :incomplete end @test validate(b"a\n", 0) == ([], :error) diff --git a/test/test12.jl b/test/test12.jl index 7616def5..6750dc06 100644 --- a/test/test12.jl +++ b/test/test12.jl @@ -13,8 +13,7 @@ using Test ctx = Automa.CodeGenContext() @eval function validate(data) logger = Symbol[] - $(Automa.generate_init_code(ctx, machine)) - $(Automa.generate_exec_code(ctx, machine, :debug)) + $(Automa.generate_code(ctx, machine, :debug)) return logger, cs == 0 ? :ok : cs < 0 ? :error : :incomplete end @test validate(b"") == ([], :ok) diff --git a/test/test14.jl b/test/test14.jl index 5dd6b675..093294f0 100644 --- a/test/test14.jl +++ b/test/test14.jl @@ -11,15 +11,13 @@ using Test ctx = Automa.CodeGenContext(generator=:table) @eval function validate_table(data) - $(Automa.generate_init_code(ctx, machine)) - $(Automa.generate_exec_code(ctx, machine)) + $(Automa.generate_code(ctx, machine)) return p, cs end ctx = Automa.CodeGenContext(generator=:goto, checkbounds=false) @eval function validate_goto(data) - $(Automa.generate_init_code(ctx, machine)) - $(Automa.generate_exec_code(ctx, machine)) + $(Automa.generate_code(ctx, machine)) return p, cs end diff --git a/test/test15.jl b/test/test15.jl index 77bd670a..449c77d0 100644 --- a/test/test15.jl +++ b/test/test15.jl @@ -27,8 +27,7 @@ using Test ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) validate = @eval function (data) logger = Symbol[] - $(Automa.generate_init_code(ctx, machine)) - $(Automa.generate_exec_code(ctx, machine, :debug)) + $(Automa.generate_code(ctx, machine, :debug)) return logger, cs == 0 ? :ok : cs < 0 ? :error : :incomplete end @test validate(b"ab") == ([:enter, :all, :final, :exit, :enter, :all, :final, :exit], :ok) diff --git a/test/test16.jl b/test/test16.jl index 282b72b0..8de05ccb 100644 --- a/test/test16.jl +++ b/test/test16.jl @@ -8,11 +8,9 @@ using Test re = re"A+(B+C)*(D|E)+" machine = Automa.compile(re) ctx = Automa.CodeGenContext(generator=:goto, checkbounds=false) - init_code = Automa.generate_init_code(ctx, machine) - exec_code = Automa.generate_exec_code(ctx, machine) + code = Automa.generate_code(ctx, machine) validate = @eval function (data) - $(init_code) - $(exec_code) + $(code) return cs == 0 end @test validate(b"ABCD") diff --git a/test/test17.jl b/test/test17.jl index e30f057e..1aa7059f 100644 --- a/test/test17.jl +++ b/test/test17.jl @@ -14,12 +14,10 @@ using Test for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) (generator == :goto && checkbounds) && continue ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) - init_code = Automa.generate_init_code(ctx, machine1) - exec_code = Automa.generate_exec_code(ctx, machine1, :debug) + code = Automa.generate_code(ctx, machine1, :debug) validate = @eval function (data) logger = Symbol[] - $(init_code) - $(exec_code) + $(code) return cs == 0, logger end @@ -36,12 +34,10 @@ using Test for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) (generator == :goto && checkbounds) && continue ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) - init_code = Automa.generate_init_code(ctx, machine2) - exec_code = Automa.generate_exec_code(ctx, machine2, :debug) + code = Automa.generate_code(ctx, machine2, :debug) validate = @eval function (data) logger = Symbol[] - $(init_code) - $(exec_code) + $(code) return cs == 0, logger end diff --git a/test/test18.jl b/test/test18.jl index 40063fc1..07a60c7f 100644 --- a/test/test18.jl +++ b/test/test18.jl @@ -9,11 +9,9 @@ using Test for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) (generator == :goto && checkbounds) && continue ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) - init_code = Automa.generate_init_code(ctx, machine) - exec_code = Automa.generate_exec_code(ctx, machine) + code = Automa.generate_code(ctx, machine) validate = @eval function (data) - $(init_code) - $(exec_code) + $(code) return cs == 0 end @test validate(b"abracadabra") == false From cbc82152458d5b85831cc15c4b754e4cbc7d1f76 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Sun, 17 Jul 2022 13:43:36 +0200 Subject: [PATCH 07/64] Add validator convenience function --- src/codegen.jl | 26 ++++++++++++++++++++++++++ test/runtests.jl | 1 + test/validator.jl | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 test/validator.jl diff --git a/src/codegen.jl b/src/codegen.jl index 19aad95e..0e8c5241 100644 --- a/src/codegen.jl +++ b/src/codegen.jl @@ -87,6 +87,32 @@ end const DefaultCodeGenContext = CodeGenContext() +""" + generate_validator_function(name::Symbol, machine::Machine, goto=false) + +Generate code that, when evaluated, defines a function named `name`, which takes a +single argument `data`, interpreted as a sequence of bytes. +The function returns `nothing` if `data` matches `Machine`, else the index of the first +invalid byte. If the machine reached unexpected EOF, returns `sizeof(data) + 1`. +If `goto`, the function uses the faster but more complicated `:goto` code. +""" +function generate_validator_function(name::Symbol, machine::Machine, goto::Bool=false) + ctx = goto ? CodeGenContext(generator=:goto) : DefaultCodeGenContext + return quote + """ + $($(name))(data)::Union{Int, Nothing} + + Checks if `data`, interpreted as a bytearray, conforms to the given `Automa.Machine`. + Returns `nothing` if it does, else the byte index of the first invalid byte. + If the machine reached unexpected EOF, returns `sizeof(data) + 1`. + """ + function $(name)(data) + $(generate_code(ctx, machine)) + iszero(cs) ? nothing : p + end + end +end + """ generate_code([::CodeGenContext], machine::Machine, actions=nothing)::Expr diff --git a/test/runtests.jl b/test/runtests.jl index 5bc082c2..46c44b87 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -98,6 +98,7 @@ include("test17.jl") include("test18.jl") include("simd.jl") include("unicode.jl") +include("validator.jl") module TestFASTA using Test diff --git a/test/validator.jl b/test/validator.jl new file mode 100644 index 00000000..d8ebd691 --- /dev/null +++ b/test/validator.jl @@ -0,0 +1,36 @@ +module Validator + +import Automa +import Automa.RegExp: @re_str +using Test + +@testset "Validator" begin + machine = let + Automa.compile(re"a(bc)*|(def)|x+" | re"def" | re"x+") + end + eval(Automa.generate_validator_function(:foobar, machine, false)) + eval(Automa.generate_validator_function(:barfoo, machine, true)) + + for good_data in [ + "def" + "abc" + "abcbcbcbcbc" + "x" + "xxxxxx" + ] + @test foobar(good_data) === barfoo(good_data) === nothing + end + + for bad_data in [ + "", + "abcabc", + "abcbb", + "abcbcb", + "defdef", + "xabc" + ] + @test foobar(bad_data) === barfoo(bad_data) !== nothing + end +end + +end # module \ No newline at end of file From 7c57e3c70632cba79378890272feba90fe290555 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Thu, 21 Jul 2022 15:00:35 +0200 Subject: [PATCH 08/64] Enable ambiguity check --- src/dfa.jl | 2 +- src/machine.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dfa.jl b/src/dfa.jl index 90650508..929266eb 100644 --- a/src/dfa.jl +++ b/src/dfa.jl @@ -37,7 +37,7 @@ function validate(dfa::DFA) end end -function nfa2dfa(nfa::NFA, unambiguous::Bool=false) +function nfa2dfa(nfa::NFA, unambiguous::Bool=true) newnodes = Dict{Set{NFANode},DFANode}() new(S) = get!(newnodes, S, DFANode(nfa.final ∈ S, S)) isvisited(S) = haskey(newnodes, S) diff --git a/src/machine.jl b/src/machine.jl index 5399fa6e..a5f9df94 100644 --- a/src/machine.jl +++ b/src/machine.jl @@ -94,7 +94,7 @@ machine let end ``` """ -function compile(re::RegExp.RE; optimize::Bool=true, unambiguous::Bool=false) +function compile(re::RegExp.RE; optimize::Bool=true, unambiguous::Bool=true) dfa = nfa2dfa(remove_dead_nodes(re2nfa(re)), unambiguous) if optimize dfa = remove_dead_nodes(reduce_nodes(dfa)) From 1e79027d859fd2669b64bdb53795c868783152a1 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Sat, 23 Jul 2022 18:09:01 +0200 Subject: [PATCH 09/64] Enforce action dict symbols are same as machines Before this PR, a user could forget or mis-spell a symbol in the action Dict passed to generate_exec_code. Now add a check to throw an error if this happens. --- src/codegen.jl | 42 ++++++++++++++++++++++-------------------- src/machine.jl | 21 +++++++++++++++++++++ test/runtests.jl | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 20 deletions(-) diff --git a/src/codegen.jl b/src/codegen.jl index 0e8c5241..db457bb3 100644 --- a/src/codegen.jl +++ b/src/codegen.jl @@ -152,17 +152,33 @@ If not passed, the context defaults to `DefaultCodeGenContext` """ function generate_exec_code(ctx::CodeGenContext, machine::Machine, actions=nothing) # make actions - if actions === nothing - actions = Dict{Symbol,Expr}(a => quote nothing end for a in action_names(machine)) + actions_dict::Dict{Symbol, Expr} = if actions === nothing + Dict{Symbol,Expr}(a => quote nothing end for a in machine_names(machine)) elseif actions == :debug - actions = debug_actions(machine) + debug_actions(machine) elseif isa(actions, AbstractDict{Symbol,Expr}) - actions = Dict{Symbol,Expr}(collect(actions)) + d = Dict{Symbol,Expr}(collect(actions)) + + # check the set of actions is same as that of machine's + machine_acts = machine_names(machine) + dict_actions = Set(k for (k,v) in d) + for act in machine_acts + if act ∈ dict_actions + delete!(dict_actions, act) + else + error("Action \"$act\" of machine not present in input action Dict") + end + end + if length(dict_actions) > 0 + error("Action \"$(first(dict_actions))\" not present in machine") + end + d else throw(ArgumentError("invalid actions argument")) end + # generate code - code = ctx.generator(ctx, machine, actions) + code = ctx.generator(ctx, machine, actions_dict) if ctx.clean code = cleanup(code) end @@ -543,25 +559,11 @@ function cleanup(ex::Expr) return Expr(ex.head, args...) end -function action_names(machine::Machine) - actions = Set{Symbol}() - for s in traverse(machine.start) - for (e, t) in s.edges - union!(actions, a.name for a in e.actions) - end - end - for as in values(machine.eof_actions) - union!(actions, a.name for a in as) - end - return actions -end - function debug_actions(machine::Machine) - actions = action_names(machine) function log_expr(name) return :(push!(logger, $(QuoteNode(name)))) end - return Dict{Symbol,Expr}(name => log_expr(name) for name in actions) + return Dict{Symbol,Expr}(name => log_expr(name) for name in machine_names(machine)) end "If possible, remove self-simd edge." diff --git a/src/machine.jl b/src/machine.jl index a5f9df94..90c36f85 100644 --- a/src/machine.jl +++ b/src/machine.jl @@ -31,6 +31,27 @@ struct Machine eof_actions::Dict{Int,ActionList} end +function action_names(machine::Machine) + actions = Set{Symbol}() + for s in traverse(machine.start) + for (e, t) in s.edges + union!(actions, a.name for a in e.actions) + end + end + for as in values(machine.eof_actions) + union!(actions, a.name for a in as) + end + return actions +end + +function machine_names(machine::Machine) + actions = action_names(machine) + for node in traverse(machine.start), (e, _) in node.edges + union!(actions, e.precond.names) + end + return actions +end + function Base.show(io::IO, machine::Machine) print(io, summary(machine), "()") end diff --git a/test/runtests.jl b/test/runtests.jl index 46c44b87..6c224bb1 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -220,6 +220,38 @@ Automa.Stream.generate_reader(:stripwhitespace, machine, actions=actions, initco end end +@testset "Incorrect action names" begin + machine = let + a = re"abc" + a.actions[:enter] = [:foo, :bar] + b = re"bcd" + b.actions[:all] = [:qux] + b.when = :froom + c = re"x*" + c.actions[:exit] = [] + Automa.compile(Automa.RegExp.cat(c, a | b)) + end + ctx = Automa.CodeGenContext(generator=:goto) + actions = Dict( + :foo => quote nothing end, + :bar => quote nothing end, + :qux => quote nothing end, + :froom => quote 1 == 1.0 end + ) + + # Just test whether it throws or not + @test Automa.generate_exec_code(ctx, machine, nothing) isa Any + @test Automa.generate_exec_code(ctx, machine, :debug) isa Any + @test Automa.generate_exec_code(ctx, machine, actions) isa Any + @test_throws Exception Automa.generate_exec_code(ctx, machine, Dict{Symbol, Expr}()) + delete!(actions, :froom) + @test_throws Exception Automa.generate_exec_code(ctx, machine, actions) + actions[:froom] = quote nothing end + actions[:missing_symbol] = quote nothing end + @test_throws Exception Automa.generate_exec_code(ctx, machine, actions) + @test Automa.generate_exec_code(ctx, machine, :debug) isa Any +end + # Three-column BED file format. cat = Automa.RegExp.cat rep = Automa.RegExp.rep From 5357b75f86d86bb63054c39b1b7bde189a7c238f Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Sun, 24 Jul 2022 10:33:38 +0200 Subject: [PATCH 10/64] Add check for invalid RE.action keys nfa2dfa now errors if any RE object has a .actions field with an unsupported key. This prevents a user from mistyping e.g. `pat.actions[:etner] = [:foo]` and having it silently do nothing. --- src/nfa.jl | 9 +++++++++ test/runtests.jl | 14 ++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/nfa.jl b/src/nfa.jl index 82f2fa1e..be35a69f 100644 --- a/src/nfa.jl +++ b/src/nfa.jl @@ -30,6 +30,8 @@ function iseps(e::Edge) return isempty(e.labels) end +const ACCEPTED_KEYS = [:enter, :exit, :all, :final] + function re2nfa(re::RegExp.RE, predefined_actions::Dict{Symbol,Action}=Dict{Symbol,Action}()) actions = Dict{Tuple{RegExp.RE,Symbol},Action}() action_order = 1 @@ -53,6 +55,13 @@ function re2nfa(re::RegExp.RE, predefined_actions::Dict{Symbol,Action}=Dict{Symb # Thompson's construction. function rec!(start, re) + # Validate keys + for (k, v) in re.actions + if k ∉ ACCEPTED_KEYS + error("Bad key in RE.actions: \"$k\". Accepted keys are: $(string(ACCEPTED_KEYS))") + end + end + if haskey(re.actions, :enter) start_in = NFANode() push!(start.edges, (Edge(eps, make_action_list(re, re.actions[:enter])), start_in)) diff --git a/test/runtests.jl b/test/runtests.jl index 6c224bb1..ed2db426 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -252,6 +252,20 @@ end @test Automa.generate_exec_code(ctx, machine, :debug) isa Any end +@testset "Invalid RE.actions keys" begin + @test_throws Exception let + a = re"abc" + a.actions[:badkey] = [:foo] + Automa.compile(a) + end + + @test let + a = re"abc" + a.actions[:enter] = [:foo] + Automa.compile(a) + end isa Any +end + # Three-column BED file format. cat = Automa.RegExp.cat rep = Automa.RegExp.rep From 5d5e517acd040e167cad2df32f3abdf375a1fbe8 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Sun, 24 Jul 2022 11:07:20 +0200 Subject: [PATCH 11/64] Add input error code to generate_code function The user probably wants to use the input error code in most use cases, except when running a machine in execute mode, debug mode, or running a validator. Adding the input error code to generated Readers is a challenge for another day --- src/codegen.jl | 22 +++++++++++++++++++--- test/simd.jl | 7 ++----- test/test03.jl | 9 +++++---- test/test07.jl | 9 ++++++--- test/test08.jl | 3 ++- test/test11.jl | 3 ++- test/test14.jl | 6 ++++-- test/test16.jl | 5 +++-- test/test18.jl | 7 +++---- 9 files changed, 46 insertions(+), 25 deletions(-) diff --git a/src/codegen.jl b/src/codegen.jl index db457bb3..8b9e5705 100644 --- a/src/codegen.jl +++ b/src/codegen.jl @@ -107,7 +107,8 @@ function generate_validator_function(name::Symbol, machine::Machine, goto::Bool= If the machine reached unexpected EOF, returns `sizeof(data) + 1`. """ function $(name)(data) - $(generate_code(ctx, machine)) + $(generate_init_code(ctx, machine)) + $(generate_exec_code(ctx, machine)) iszero(cs) ? nothing : p end end @@ -117,12 +118,26 @@ end generate_code([::CodeGenContext], machine::Machine, actions=nothing)::Expr Generate init and exec code for `machine`. -Shorthand for `generate_init_code(ctx, machine); generate_action_code(ctx, machine, actions)` +Shorthand for: +``` +generate_init_code(ctx, machine) +generate_action_code(ctx, machine, actions) +generate_input_error_code(ctx, machine) [elided if actions == :debug] +``` """ function generate_code(ctx::CodeGenContext, machine::Machine, actions=nothing) + # If actions are :debug, the user presumably wants to programatically + # check what happens to the machine, which is not made easier by + # throwing an error. + error_code = if actions != :debug + generate_input_error_code(ctx, machine) + else + quote nothing end + end return quote $(generate_init_code(ctx, machine)) $(generate_exec_code(ctx, machine, actions)) + $(error_code) end end generate_code(machine::Machine, actions=nothing) = generate_code(DefaultCodeGenContext, machine, actions) @@ -136,6 +151,7 @@ If not passed, the context defaults to `DefaultCodeGenContext` function generate_init_code(ctx::CodeGenContext, machine::Machine) vars = ctx.vars return quote + $(vars.byte)::UInt8 = 0x00 $(vars.p)::Int = 1 $(vars.p_end)::Int = sizeof($(vars.data)) $(vars.p_eof)::Int = $(vars.p_end) @@ -490,7 +506,7 @@ function generate_input_error_code(ctx::CodeGenContext, machine::Machine) vars = ctx.vars return quote if $(vars.cs) < 0 - $byte_symbol = ($(vars.p_eof > -1) && $(vars.p) > $(vars.p_eof)) ? nothing : $(vars.byte) + $byte_symbol = ($(vars.p_eof) > -1 && $(vars.p) > $(vars.p_eof)) ? nothing : $(vars.byte) Automa.throw_input_error($(machine), -$(vars.cs), $byte_symbol, $(vars.mem), $(vars.p)) end end diff --git a/test/simd.jl b/test/simd.jl index 6c979f3c..987f366f 100644 --- a/test/simd.jl +++ b/test/simd.jl @@ -18,17 +18,14 @@ import Automa.RegExp: @re_str context = Automa.CodeGenContext(generator=:goto, checkbounds=false) - @eval function is_valid_fasta(data::String) - $(Automa.generate_code(context, machine)) - return p == ncodeunits(data) + 1 - end + eval(Automa.generate_validator_function(:is_valid_fasta, machine, true)) s1 = ">seq\nTAGGCTA\n>hello\nAJKGMP" s2 = ">seq1\nTAGGC" s3 = ">verylongsequencewherethesimdkicksinmakeitevenlongertobesure\nQ" for (seq, isvalid) in [(s1, true), (s2, false), (s3, true)] - @test is_valid_fasta(seq) == isvalid + @test is_valid_fasta(seq) isa (isvalid ? Nothing : Integer) end end diff --git a/test/test03.jl b/test/test03.jl index 7772b6bb..f02daabf 100644 --- a/test/test03.jl +++ b/test/test03.jl @@ -16,14 +16,15 @@ using Test for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) # Test the default CTX, if none is passed. # We use the otherwise invalid combinarion :goto && checkbounds to do this - code = if generator == :goto && checkbounds - Automa.generate_code(machine) + (init_code, exec_code) = if generator == :goto && checkbounds + (Automa.generate_init_code(machine), Automa.generate_exec_code(machine)) else ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) - Automa.generate_code(ctx, machine) + (Automa.generate_init_code(ctx, machine), Automa.generate_exec_code(ctx, machine)) end validate = @eval function (data) - $(code) + $(init_code) + $(exec_code) return cs == 0 end @test validate(b"") == true diff --git a/test/test07.jl b/test/test07.jl index 64b994b9..33af2335 100644 --- a/test/test07.jl +++ b/test/test07.jl @@ -9,7 +9,8 @@ using Test machine = Automa.compile(re1) ctx = Automa.CodeGenContext() @eval function ismatch1(data) - $(Automa.generate_code(ctx, machine)) + $(Automa.generate_init_code(ctx, machine)) + $(Automa.generate_exec_code(ctx, machine)) return cs == 0 end @test ismatch1(b"ab") @@ -22,7 +23,8 @@ using Test machine = Automa.compile(re2) ctx = Automa.CodeGenContext() @eval function ismatch2(data) - $(Automa.generate_code(ctx, machine)) + $(Automa.generate_init_code(ctx, machine)) + $(Automa.generate_exec_code(ctx, machine)) return cs == 0 end @test ismatch2(b"ab") @@ -36,7 +38,8 @@ using Test machine = Automa.compile(re3) ctx = Automa.CodeGenContext() @eval function ismatch3(data) - $(Automa.generate_code(ctx, machine)) + $(Automa.generate_init_code(ctx, machine)) + $(Automa.generate_exec_code(ctx, machine)) return cs == 0 end @test ismatch3(b"a.*b") diff --git a/test/test08.jl b/test/test08.jl index 674017e2..7de056be 100644 --- a/test/test08.jl +++ b/test/test08.jl @@ -30,7 +30,8 @@ using Test @eval function tokenize(data) tokens = Tuple{Symbol,String}[] mark = 0 - $(Automa.generate_code(ctx, machine, actions)) + $(Automa.generate_init_code(ctx, machine)) + $(Automa.generate_exec_code(ctx, machine, actions)) return tokens, cs == 0 ? :ok : cs < 0 ? :error : :incomplete end diff --git a/test/test11.jl b/test/test11.jl index a9f10a8a..b4348cdf 100644 --- a/test/test11.jl +++ b/test/test11.jl @@ -26,7 +26,8 @@ using Test ctx = Automa.CodeGenContext(generator=:goto, checkbounds=false, clean=clean) validate = @eval function (data, n) logger = Symbol[] - $(Automa.generate_code(ctx, machine, actions)) + $(Automa.generate_init_code(ctx, machine)) + $(Automa.generate_exec_code(ctx, machine, actions)) return logger, cs == 0 ? :ok : cs < 0 ? :error : :incomplete end @test validate(b"a\n", 0) == ([], :error) diff --git a/test/test14.jl b/test/test14.jl index 093294f0..5dd6b675 100644 --- a/test/test14.jl +++ b/test/test14.jl @@ -11,13 +11,15 @@ using Test ctx = Automa.CodeGenContext(generator=:table) @eval function validate_table(data) - $(Automa.generate_code(ctx, machine)) + $(Automa.generate_init_code(ctx, machine)) + $(Automa.generate_exec_code(ctx, machine)) return p, cs end ctx = Automa.CodeGenContext(generator=:goto, checkbounds=false) @eval function validate_goto(data) - $(Automa.generate_code(ctx, machine)) + $(Automa.generate_init_code(ctx, machine)) + $(Automa.generate_exec_code(ctx, machine)) return p, cs end diff --git a/test/test16.jl b/test/test16.jl index 8de05ccb..5f224b0a 100644 --- a/test/test16.jl +++ b/test/test16.jl @@ -8,9 +8,10 @@ using Test re = re"A+(B+C)*(D|E)+" machine = Automa.compile(re) ctx = Automa.CodeGenContext(generator=:goto, checkbounds=false) - code = Automa.generate_code(ctx, machine) + code = validate = @eval function (data) - $(code) + $(Automa.generate_init_code(ctx, machine)) + $(Automa.generate_exec_code(ctx, machine)) return cs == 0 end @test validate(b"ABCD") diff --git a/test/test18.jl b/test/test18.jl index 07a60c7f..1e89e5f6 100644 --- a/test/test18.jl +++ b/test/test18.jl @@ -12,11 +12,10 @@ using Test code = Automa.generate_code(ctx, machine) validate = @eval function (data) $(code) - return cs == 0 end - @test validate(b"abracadabra") == false - @test validate(b"\0\a\b\t\n\v\r\x00\xff\xFF\\!") == true - @test validate(b"\0\a\b\t\n\v\r\x00\xff\xFF\\\\") == false + @test_throws Exception validate(b"abracadabra") + @test validate(b"\0\a\b\t\n\v\r\x00\xff\xFF\\!") === nothing + @test validate(b"\0\a\b\t\n\v\r\x00\xff\xFF\\\\") === nothing end end From d23847211d25d5fdee0952229f4db52402091438 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Mon, 25 Jul 2022 11:30:36 +0200 Subject: [PATCH 12/64] Export user-facing names One of the issues with using Automa is that its lack of exports makes it unclear what is internal/external. Also, the three typical using-statements needed to use Automa is just visual noise. --- src/Automa.jl | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Automa.jl b/src/Automa.jl index 06d0cbe4..22108267 100644 --- a/src/Automa.jl +++ b/src/Automa.jl @@ -37,4 +37,24 @@ include("codegen.jl") include("tokenizer.jl") include("Stream.jl") +const RE = Automa.RegExp +using .RegExp: @re_str, opt, rep, rep1 + +# This list of exports lists the API +export RE, + @re_str, + CodeGenContext, + compile, + + # user-facing generator functions + generate_validator_function, + generate_init_code, + generate_exec_code, + generate_code, + + # cat and alt is not exported in favor of * and | + opt, + rep, + rep1 + end # module From f333cfb1d1dda2041a6ff3ae32f87407e5391e4f Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Wed, 27 Jul 2022 15:44:38 +0200 Subject: [PATCH 13/64] Check preconditions before declaring NFA ambiguous If two edges in equivalent paths have distinct preconditions, these could be used to distinguish the two edges, and so they should not provoke an error. --- src/dfa.jl | 12 +++++++++++- test/runtests.jl | 1 + test/test19.jl | 9 +++++++-- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/dfa.jl b/src/dfa.jl index 929266eb..70b78d55 100644 --- a/src/dfa.jl +++ b/src/dfa.jl @@ -195,10 +195,20 @@ function validate_paths( for j in i+1:length(paths) edge2, node2, actions2 = paths[j] # If either ends with EOF, they don't have same conditions and we can continue + # If only one is an EOF, they are distinct (edge1 === nothing) ⊻ (edge2 === nothing) && continue + # If they have same actions, there is no conflict actions1 == actions2 && continue eof = (edge1 === nothing) & (edge2 === nothing) - !(eof || overlaps(edge1, edge2)) && continue + + if !eof + # If they are real edges but do not overlap, there is no conflict + overlaps(edge1, edge2) || continue + + # If the FSM may disambiguate the two edges based on preconditions + # there is no conflict (or, rather, we can't prove a conflict. + has_potentially_conflicting_precond(edge1, edge2) && continue + end # Now we know there is an ambiguity, so we just need to create # an informative error diff --git a/test/runtests.jl b/test/runtests.jl index ed2db426..19721499 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -96,6 +96,7 @@ include("test15.jl") include("test16.jl") include("test17.jl") include("test18.jl") +include("test19.jl") include("simd.jl") include("unicode.jl") include("validator.jl") diff --git a/test/test19.jl b/test/test19.jl index e5e80547..d658ddca 100644 --- a/test/test19.jl +++ b/test/test19.jl @@ -38,8 +38,13 @@ using Test B.actions[:exit] = [:exit_B] @test Automa.compile(A | B) isa Automa.Machine - # TODO: Also need test for whether ambiguous edges can be - # resolved by conflicting preconditions and allow compilation. + # Test that conflicting edges can be known to be distinct + # with different conditions. + A = re"XY" + A.when = :cond + B = re"XZ" + A.actions[:enter] = [:enter_A] + @test Automa.compile(A | B, unambiguous=true) isa Automa.Machine end end From a743327bd3bcc7598888833f1ab677347b916ade Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Thu, 28 Jul 2022 09:03:33 +0200 Subject: [PATCH 14/64] Fix EOF check in machine error code --- src/machine.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/machine.jl b/src/machine.jl index 90c36f85..cc8bc1a8 100644 --- a/src/machine.jl +++ b/src/machine.jl @@ -189,7 +189,7 @@ function throw_input_error( buf = IOBuffer() @assert index <= lastindex(memory) + 1 # Print position in memory - is_eof = index == lastindex(memory) + 1 + is_eof = index > lastindex(memory) @assert byte isa (is_eof ? Nothing : UInt8) slice = max(1,index-100):index - is_eof bytes = repr(String([memory[i] for i in slice])) From 52bcd12f14ddeabaa95b4bf0392945179ae3f390 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Thu, 28 Jul 2022 09:30:32 +0200 Subject: [PATCH 15/64] Do not store gensym symbols in default CodeGenContext The gensym'd symbols in Automa's constant expressions are computed on precompilation of Automa. These can then clash with symbols that are computed on the precompilation of any particular generated code in downstream packages leading to very confusing bugs. --- src/codegen.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/codegen.jl b/src/codegen.jl index 8b9e5705..5e0c5e41 100644 --- a/src/codegen.jl +++ b/src/codegen.jl @@ -42,7 +42,7 @@ function generate_goto_code end """ CodeGenContext(; - vars=Variables(:p, :p_end, :p_eof, :ts, :te, :cs, :data, gensym(), :byte), + vars=Variables(:p, :p_end, :p_eof, :ts, :te, :cs, :data, :mem, :byte), generator=:table, checkbounds=true, getbyte=Base.getindex, @@ -61,7 +61,7 @@ Arguments - `clean`: flag of code cleansing """ function CodeGenContext(; - vars::Variables=Variables(:p, :p_end, :p_eof, :ts, :te, :cs, :data, gensym(), :byte), + vars::Variables=Variables(:p, :p_end, :p_eof, :ts, :te, :cs, :data, :mem, :byte), generator::Symbol=:table, checkbounds::Bool=generator == :table, getbyte::Function=Base.getindex, @@ -213,7 +213,7 @@ function generate_table_code(ctx::CodeGenContext, machine::Machine, actions::Dic eof_action_code = generate_eof_action_code(ctx, machine, actions) final_state_code = generate_final_state_mem_code(ctx, machine) return quote - $(ctx.vars.mem) = $(SizedMemory)($(ctx.vars.data)) + $(ctx.vars.mem)::Automa.SizedMemory = $(SizedMemory)($(ctx.vars.data)) while $(ctx.vars.p) ≤ $(ctx.vars.p_end) && $(ctx.vars.cs) > 0 $(getbyte_code) $(set_act_code) From 01e26e543ea36eb1fdcad0757297362b54b4f018 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Sat, 30 Jul 2022 21:48:47 +0200 Subject: [PATCH 16/64] Add more comments --- src/codegen.jl | 84 +++++++++++++++++++++++++++++++++++++++++++++++--- src/edge.jl | 4 +++ src/precond.jl | 14 ++++++++- src/re.jl | 5 +++ 4 files changed, 102 insertions(+), 5 deletions(-) diff --git a/src/codegen.jl b/src/codegen.jl index 5e0c5e41..850fb76f 100644 --- a/src/codegen.jl +++ b/src/codegen.jl @@ -58,7 +58,7 @@ Arguments - `generator`: code generator (`:table` or `:goto`) - `checkbounds`: flag of bounds check - `getbyte`: function of byte access (i.e. `getbyte(data, p)`) -- `clean`: flag of code cleansing +- `clean`: flag of code cleansing, e.g. removing line comments """ function CodeGenContext(; vars::Variables=Variables(:p, :p_end, :p_eof, :ts, :te, :cs, :data, :mem, :byte), @@ -109,7 +109,8 @@ function generate_validator_function(name::Symbol, machine::Machine, goto::Bool= function $(name)(data) $(generate_init_code(ctx, machine)) $(generate_exec_code(ctx, machine)) - iszero(cs) ? nothing : p + # By convention, Automa lets cs be 0 if machine executed correctly. + iszero($(ctx.vars.cs)) ? nothing : p end end end @@ -213,23 +214,36 @@ function generate_table_code(ctx::CodeGenContext, machine::Machine, actions::Dic eof_action_code = generate_eof_action_code(ctx, machine, actions) final_state_code = generate_final_state_mem_code(ctx, machine) return quote + # Preserve data because SizedMemory is just a pointer + GC.@preserve $(ctx.vars.data) begin $(ctx.vars.mem)::Automa.SizedMemory = $(SizedMemory)($(ctx.vars.data)) + # For each input byte... while $(ctx.vars.p) ≤ $(ctx.vars.p_end) && $(ctx.vars.cs) > 0 + # Load byte $(getbyte_code) + # Get an integer corresponding to the set of actions that will be taken + # for this particular input at this stage (possibly nothing) $(set_act_code) + # Update state by a simple lookup in a table based on current state and input $(set_cs_code) + # Go through an if-else list of all actions to match the action integet obtained + # above, and execute the matching set of actions $(action_dispatch_code) $(ctx.vars.p) += 1 end + # If we're out of bytes and in an accept state, find the correct EOF action + # and execute it, then set cs to 0 to signify correct execution if $(ctx.vars.p) > $(ctx.vars.p_eof) ≥ 0 && $(final_state_code) $(eof_action_code) $(ctx.vars.cs) = 0 elseif $(ctx.vars.cs) < 0 $(ctx.vars.p) -= 1 end + end # GC.@preserve block end end +# Smallest int type that n fits in function smallest_int(n::Integer) for T in [Int8, Int16, Int32, Int64] n <= typemax(T) && return T @@ -237,6 +251,8 @@ function smallest_int(n::Integer) @assert false end +# The table is a 256xnstates byte lookup table, such that table[input,cs] will give +# the next state. function generate_transition_table(machine::Machine) nstates = length(machine.states) trans_table = Matrix{smallest_int(nstates)}(undef, 256, nstates) @@ -244,6 +260,8 @@ function generate_transition_table(machine::Machine) trans_table[:,j] .= -j end for s in traverse(machine.start), (e, t) in s.edges + # Preconditions work by inserting if/else statements into the code. + # It's hard to see how we could fit it into the table-based generator if !isempty(e.precond) error("precondition is not supported in the table-based code generator; try code=:goto") end @@ -258,6 +276,9 @@ function generate_action_dispatch_code(ctx::CodeGenContext, machine::Machine, ac nactions = length(actions) T = smallest_int(nactions) action_table = fill(zero(T), (256, length(machine.states))) + # Each edge with actions is a Vector{Symbol} with action names. + # Enumerate them, by mapping the vector to an integer. + # This way, each set of actions is mapped to an integer (call it: action int) action_ids = Dict{Vector{Symbol},T}() for s in traverse(machine.start) for (e, t) in s.edges @@ -265,6 +286,8 @@ function generate_action_dispatch_code(ctx::CodeGenContext, machine::Machine, ac continue end id = get!(action_ids, action_names(e.actions), length(action_ids) + 1) + # In the action table, the current state as well as the input byte gives the + # action int (see above) to execute on this transition for l in e.labels action_table[l+1,s.state] = id end @@ -272,20 +295,28 @@ function generate_action_dispatch_code(ctx::CodeGenContext, machine::Machine, ac end act = gensym() default = :() + # This creates code of the form: If act == 1 (actions in action int == 1) + # else if act == 2 (... etc) action_dispatch_code = foldr(default, action_ids) do names_id, els names, id = names_id action_code = rewrite_special_macros(ctx, generate_action_code(names, actions), false) return Expr(:if, :($(act) == $(id)), action_code, els) end + # Action code is: Get the action int from the state and current input byte + # Action dispatch code: The thing made above action_code = :(@inbounds $(act) = Int($(action_table)[($(ctx.vars.cs) - 1) << 8 + $(ctx.vars.byte) + 1])) return action_dispatch_code, action_code end function generate_goto_code(ctx::CodeGenContext, machine::Machine, actions::Dict{Symbol,Expr}) + # All the sets of actions (each set being a vector) on edges leading to a + # given machine node. actions_in = Dict{Node,Set{Vector{Symbol}}}() for s in traverse(machine.start), (e, t) in s.edges push!(get!(actions_in, t, Set{Vector{Symbol}}()), action_names(e.actions)) end + # Assign each action a unique name based on the destination node the edge is on, + # and an integer, e.g. state_2_action_5 action_label = Dict{Node,Dict{Vector{Symbol},Symbol}}() for s in traverse(machine.start) action_label[s] = Dict() @@ -296,10 +327,13 @@ function generate_goto_code(ctx::CodeGenContext, machine::Machine, actions::Dict end end + # Main loop expression blocks blocks = Expr[] for s in traverse(machine.start) block = Expr(:block) for (names, label) in action_label[s] + # These blocks are goto'd directly, when encountering the right edge. Their content + # if of the form execute action, then go to the state the edge was pointing to if isempty(names) continue end @@ -310,6 +344,8 @@ function generate_goto_code(ctx::CodeGenContext, machine::Machine, actions::Dict end) end + # This is the code of each state. The pointer is incremented, you @goto the exit + # if EOF, else continue to the code created below append_code!(block, quote @label $(Symbol("state_", s.state)) $(ctx.vars.p) += 1 @@ -319,6 +355,11 @@ function generate_goto_code(ctx::CodeGenContext, machine::Machine, actions::Dict end end) + # SIMD code is special: If a node has a self-edge with no preconditions or actions, + # then the machine can skip ahead until the input is no longer in that edge's byteset. + # This can be effectively SIMDd + # If such an edge is detected, we treat it specially with code here, and leave the + # non-SIMDable edges for below simd, non_simd = peel_simd_edge(s) simd_code = if simd !== nothing quote @@ -331,8 +372,13 @@ function generate_goto_code(ctx::CodeGenContext, machine::Machine, actions::Dict else :() end - + + # If no inputs match, then we set cs = -cs to signal error, and go to exit default = :($(ctx.vars.cs) = $(-s.state); @goto exit) + + # For each edge in optimized order, check if the conditions for taking that edge + # is met. If so, go to the edge's actions if it has any actions, else go directly + # to the destination state dispatch_code = foldr(default, optimize_edge_order(non_simd)) do edge, els e, t = edge if isempty(e.actions) @@ -343,6 +389,7 @@ function generate_goto_code(ctx::CodeGenContext, machine::Machine, actions::Dict return Expr(:if, generate_condition_code(ctx, e, actions), then, els) end + # Here we simply add the code created above to the list of expressions append_code!(block, quote @label $(Symbol("state_case_", s.state)) $(simd_code) @@ -352,11 +399,20 @@ function generate_goto_code(ctx::CodeGenContext, machine::Machine, actions::Dict push!(blocks, block) end + # In the beginning of the code generated here, the machine may not be in start state 1. + # E.g. it may be resuming. So, we generate a list of if-else statements that simply check + # the starting state, then directly goto that state. + # In cases where the starting state is hardcoded as a constant, (which is quite often!) + # hopefully the Julia compiler will optimize this block away. enter_code = foldr(:(@goto exit), machine.states) do s, els return Expr(:if, :($(ctx.vars.cs) == $(s)), :(@goto $(Symbol("state_case_", s))), els) end + # When EOF, go through a list of if/else statements: If cs == 1, do this, elseif + # cs == 2 do that etc eof_action_code = rewrite_special_macros(ctx, generate_eof_action_code(ctx, machine, actions), true) + + # Check the final state is an accept state, in an efficient manner final_state_code = generate_final_state_mem_code(ctx, machine) return quote @@ -385,9 +441,14 @@ end # Note: This function has been carefully crafted to produce (nearly) optimal # assembly code for AVX2-capable CPUs. Change with great care. function generate_simd_loop(ctx::CodeGenContext, bs::ByteSet) + # ScanByte finds first byte in a byteset. We want to find first + # byte NOT in this byteset, as this is where we can no longer skip ahead to byteset = ~ScanByte.ByteSet(bs) bsym = gensym() quote + # We wrap this in an Automa function, because otherwise the generated code + # would have a reference to ScanByte, which the user may not have imported. + # But they surely have imported Automa. $bsym = Automa.loop_simd( $(ctx.vars.mem).ptr + $(ctx.vars.p) - 1, ($(ctx.vars.p_end) - $(ctx.vars.p) + 1) % UInt, @@ -401,10 +462,13 @@ function generate_simd_loop(ctx::CodeGenContext, bs::ByteSet) end end +# Necessary wrapper function, see comment in `generate_simd_loop` @inline function loop_simd(ptr::Ptr, len::UInt, valbs::Val) ScanByte.memchr(ptr, len, valbs) end +# Make if/else statements for each state that is an acceptable end state, and execute +# the actions attached with ending in this state. function generate_eof_action_code(ctx::CodeGenContext, machine::Machine, actions::Dict{Symbol,Expr}) return foldr(:(), machine.eof_actions) do s_as, els s, as = s_as @@ -501,6 +565,8 @@ function generate_membership_code(var::Symbol, set::ByteSet) end end +# Create a user-friendly informative error if a bad input is seen. +# Defined in machine.jl, see that file. function generate_input_error_code(ctx::CodeGenContext, machine::Machine) byte_symbol = gensym() vars = ctx.vars @@ -557,6 +623,8 @@ function isescape(arg) return arg isa Expr && arg.head == :macrocall && arg.args[1] == Symbol("@escape") end +# Clean created code of e.g. Automa source code comments. +# By default not executed, as it's handy for debugging. function cleanup(ex::Expr) args = [] for arg in ex.args @@ -586,8 +654,15 @@ end function peel_simd_edge(node) non_simd = Tuple{Edge, Node}[] simd = nothing + # A simd-edge has no actions or preconditions, and its source is same as destination. + # that means the machine can just skip ahead for (e, t) in node.edges if t === node && isempty(e.actions) && isempty(e.precond) + # There should only be 1 SIMD edge possible, if not, the machine + # was not properly optimized by Automa, since SIMD edges should be + # collapsable, as they have the same actions, preconditions and target node, + # namely none, none and self. + @assert simd === nothing simd = e else push!(non_simd, (e, t)) @@ -601,7 +676,8 @@ function optimize_edge_order(edges) return sort!(copy(edges), by=e->length(e[1].labels), rev=true) end -# Generic foldr. +# Generic foldr. We have this here because using Base's foldr requires the iterator +# to have a reverse method, whereas this one doesn't (but is much less efficient) function foldr(op::Function, x0, xs) function rec(xs, s) if s === nothing diff --git a/src/edge.jl b/src/edge.jl index 4416d6dc..078fa89c 100644 --- a/src/edge.jl +++ b/src/edge.jl @@ -1,6 +1,10 @@ # Edge # ==== +# An edge connects two nodes in the FSM. The labels is a set of bytes that, if the +# input is in that set, the edge may be taken. +# Precond is like an if-statement, if that condition is fulfilled, take the edge +# The actions is a list of names of Julia code to execute, if edge is taken. struct Edge labels::ByteSet precond::Precondition diff --git a/src/precond.jl b/src/precond.jl index 2ac0e3a0..8c0a8f89 100644 --- a/src/precond.jl +++ b/src/precond.jl @@ -1,6 +1,7 @@ # Precondition # ============ +# See comments on Precondition. primitive type Value 8 end const NONE = reinterpret(Value, 0x00) @@ -24,7 +25,18 @@ function Base.:&(v1::Value, v2::Value) return convert(Value, convert(UInt8, v1) & convert(UInt8, v2)) end - +# A Precondition is a list of conditions. Each condition has a symbol, which is used to +# look up in a dict for an Expr object like :(a > 1) that should evaluate to a Bool. +# This allows Automa to add if/else statements in generated code, like +# if (a > 1) && (b < 5) for a Precondition with two symbols. +# The Value is whether the condition is negated. +# Automa's optimization of the graph may negate, or manipulate the expressions using +# boolean logic. +# There are four options for expr E: +# 1. E & !E (i.e. always false. This code is never even generated, just a literal false is) +# 2. E (i.e. the pure condition) +# 3. !E (i.e. the negated condition) +# 4. E | !E (i.e. always true. Like 1., this is encoded as a literal `true` in generated code). struct Precondition names::Vector{Symbol} values::Vector{Value} diff --git a/src/re.jl b/src/re.jl index d6ebd77b..66d78986 100644 --- a/src/re.jl +++ b/src/re.jl @@ -9,6 +9,11 @@ function gen_empty_names() return Symbol[] end +# Head: What kind of regex, like cat, or rep, or opt etc. +# args: the content of the regex itself. Maybe should be type stable? +# actions: Julia code to be executed when matching the regex. See Automa docs +# when: a Precondition that is checked when every byte in the regex is matched. +# See comments on Precondition struct mutable struct RE head::Symbol args::Vector From 46f8cfb3a38bba0ead3c31c4c514132317492674 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Mon, 1 Aug 2022 14:55:21 +0200 Subject: [PATCH 17/64] Also trigger default error when cs > 0 --- src/codegen.jl | 3 ++- test/test18.jl | 22 +++++++++++++--------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/codegen.jl b/src/codegen.jl index 850fb76f..531780e0 100644 --- a/src/codegen.jl +++ b/src/codegen.jl @@ -571,7 +571,8 @@ function generate_input_error_code(ctx::CodeGenContext, machine::Machine) byte_symbol = gensym() vars = ctx.vars return quote - if $(vars.cs) < 0 + if $(vars.cs) != 0 + $(vars.cs) = -abs($(vars.cs)) $byte_symbol = ($(vars.p_eof) > -1 && $(vars.p) > $(vars.p_eof)) ? nothing : $(vars.byte) Automa.throw_input_error($(machine), -$(vars.cs), $byte_symbol, $(vars.mem), $(vars.p)) end diff --git a/test/test18.jl b/test/test18.jl index 1e89e5f6..b771436f 100644 --- a/test/test18.jl +++ b/test/test18.jl @@ -6,16 +6,20 @@ using Test @testset "Test18" begin machine = Automa.compile(re"\0\a\b\t\n\v\r\x00\xff\xFF[\\][^\\]") - for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) - (generator == :goto && checkbounds) && continue - ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) - code = Automa.generate_code(ctx, machine) - validate = @eval function (data) - $(code) - end - @test_throws Exception validate(b"abracadabra") + for goto in (false, true) + println(goto) + @eval $(Automa.generate_validator_function(:validate, machine, goto)) + + # Bad input types + @test_throws Exception validate(18) + @test_throws Exception validate('a') + @test_throws Exception validate(0x01:0x02) + @test validate(b"\0\a\b\t\n\v\r\x00\xff\xFF\\!") === nothing - @test validate(b"\0\a\b\t\n\v\r\x00\xff\xFF\\\\") === nothing + bad_input = b"\0\a\b\t\n\v\r\x00\xff\xFF\\\\\\" + @test validate(bad_input) == lastindex(bad_input) + bad_input = b"\0\a\b\t\n\v\r\x00\xff\xFF\\" + @test validate(bad_input) == lastindex(bad_input) + 1 end end From d3b6adaf844a4d4099822c96a6ab900cd017cdb6 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Mon, 1 Aug 2022 15:31:05 +0200 Subject: [PATCH 18/64] Remove checkbounds option With the :goto generator, checking bounds is not permitted anyway. And the table generator is so slow that it makes no sense to disable checking of bounds - then you might as well use the goto generator --- docs/src/index.md | 2 +- example/fasta.jl | 2 +- src/codegen.jl | 17 ++++------------- test/runtests.jl | 2 +- test/simd.jl | 4 ++-- test/test01.jl | 5 ++--- test/test02.jl | 5 ++--- test/test03.jl | 13 ++++--------- test/test04.jl | 5 ++--- test/test05.jl | 5 ++--- test/test06.jl | 5 ++--- test/test11.jl | 2 +- test/test14.jl | 2 +- test/test15.jl | 5 ++--- test/test16.jl | 2 +- test/test17.jl | 10 ++++------ 16 files changed, 32 insertions(+), 54 deletions(-) diff --git a/docs/src/index.md b/docs/src/index.md index 9578ad02..a8c614c4 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -378,7 +378,7 @@ elseif cs < 0 end ``` -Automa.jl has four kinds of code generators. The first and default one uses two lookup tables to pick up the next state and the actions for the current state and input. The second one expands these lookup tables into a series of if-else branches. The third one is based on `@goto` jumps. The fourth one is identitical to the third one, except uses SIMD operations where applicable. These two code generators are named as `:table`, and `:goto`, respectively. To sepcify a code generator, you can pass the `code=:table|:goto` argument to `Automa.generate_exec_code`. The generated code size and its runtime speed highly depends on the machine and actions. However, as a rule of thumb, `:table` is simpler with smaller code, but is also slower.Also, specifying `checkbounds=false` turns off bounds checking while executing and often improves the runtime performance slightly. +Automa.jl has four kinds of code generators. The first and default one uses two lookup tables to pick up the next state and the actions for the current state and input. The second one expands these lookup tables into a series of if-else branches. The third one is based on `@goto` jumps. The fourth one is identitical to the third one, except uses SIMD operations where applicable. These two code generators are named as `:table`, and `:goto`, respectively. To sepcify a code generator, you can pass the `code=:table|:goto` argument to `Automa.generate_exec_code`. The generated code size and its runtime speed highly depends on the machine and actions. However, as a rule of thumb, `:table` is simpler with smaller code, but is also slower. Note that the `:goto` generator has more requirements than the `:table` generator: * First, `boundscheck=false` must be set diff --git a/example/fasta.jl b/example/fasta.jl index 39be125a..dd950bdf 100644 --- a/example/fasta.jl +++ b/example/fasta.jl @@ -52,7 +52,7 @@ mutable struct FASTARecord end # Generate a parser function from `fasta_machine` and `fasta_actions`. -context = Automa.CodeGenContext(generator=:goto, checkbounds=false) +context = Automa.CodeGenContext(generator=:goto) @eval function parse_fasta(data::Union{String,Vector{UInt8}}) # Initialize variables you use in the action code. records = FASTARecord[] diff --git a/src/codegen.jl b/src/codegen.jl index 531780e0..65cb077f 100644 --- a/src/codegen.jl +++ b/src/codegen.jl @@ -1,4 +1,5 @@ # Code Generator +# Code Generator # ============== """ @@ -31,7 +32,6 @@ end struct CodeGenContext vars::Variables generator::Function - checkbounds::Bool getbyte::Function clean::Bool end @@ -44,7 +44,6 @@ function generate_goto_code end CodeGenContext(; vars=Variables(:p, :p_end, :p_eof, :ts, :te, :cs, :data, :mem, :byte), generator=:table, - checkbounds=true, getbyte=Base.getindex, clean=false ) @@ -56,22 +55,18 @@ Arguments - `vars`: variable names used in generated code - `generator`: code generator (`:table` or `:goto`) -- `checkbounds`: flag of bounds check - `getbyte`: function of byte access (i.e. `getbyte(data, p)`) - `clean`: flag of code cleansing, e.g. removing line comments """ function CodeGenContext(; vars::Variables=Variables(:p, :p_end, :p_eof, :ts, :te, :cs, :data, :mem, :byte), generator::Symbol=:table, - checkbounds::Bool=generator == :table, getbyte::Function=Base.getindex, clean::Bool=false) # special conditions for simd generator if generator == :goto if getbyte != Base.getindex throw(ArgumentError("GOTO generator only support Base.getindex")) - elseif checkbounds - throw(ArgumentError("GOTO generator does not support boundscheck")) end end # check generator @@ -82,7 +77,7 @@ function CodeGenContext(; else throw(ArgumentError("invalid code generator: $(generator)")) end - return CodeGenContext(vars, generator, checkbounds, getbyte, clean) + return CodeGenContext(vars, generator, getbyte, clean) end const DefaultCodeGenContext = CodeGenContext() @@ -393,7 +388,7 @@ function generate_goto_code(ctx::CodeGenContext, machine::Machine, actions::Dict append_code!(block, quote @label $(Symbol("state_case_", s.state)) $(simd_code) - $(generate_getbyte_code(ctx)) + $(ctx.vars.byte) = @inbounds getindex($(ctx.vars.mem), $(ctx.vars.p)) $(dispatch_code) end) push!(blocks, block) @@ -490,11 +485,7 @@ function generate_getbyte_code(ctx::CodeGenContext) end function generate_getbyte_code(ctx::CodeGenContext, varbyte::Symbol, offset::Int) - code = :($(varbyte) = $(ctx.getbyte)($(ctx.vars.mem), $(ctx.vars.p) + $(offset))) - if !ctx.checkbounds - code = :(@inbounds $(code)) - end - return code + :($(varbyte) = $(ctx.getbyte)($(ctx.vars.mem), $(ctx.vars.p) + $(offset))) end function state_condition(ctx::CodeGenContext, s::Int) diff --git a/test/runtests.jl b/test/runtests.jl index 19721499..816f7cac 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -407,7 +407,7 @@ end loopcode = quote found && @goto __return__ end -context = Automa.CodeGenContext(generator=:goto, checkbounds=false) +context = Automa.CodeGenContext(generator=:goto) Automa.Stream.generate_reader( :readrecord!, machine, diff --git a/test/simd.jl b/test/simd.jl index 987f366f..a7d86677 100644 --- a/test/simd.jl +++ b/test/simd.jl @@ -1,7 +1,7 @@ # Test codegencontext @testset "CodeGenContext" begin @test_throws ArgumentError Automa.CodeGenContext(generator=:fdjfhkdj) - @test_throws ArgumentError Automa.CodeGenContext(generator=:goto, checkbounds=false, getbyte=identity) + @test_throws ArgumentError Automa.CodeGenContext(generator=:goto, getbyte=identity) end import Automa @@ -16,7 +16,7 @@ import Automa.RegExp: @re_str Automa.compile(re.opt(rec) * re.rep(re"\n" * rec)) end - context = Automa.CodeGenContext(generator=:goto, checkbounds=false) + context = Automa.CodeGenContext(generator=:goto) eval(Automa.generate_validator_function(:is_valid_fasta, machine, true)) diff --git a/test/test01.jl b/test/test01.jl index 3550ae0f..7884457b 100644 --- a/test/test01.jl +++ b/test/test01.jl @@ -11,9 +11,8 @@ using Test machine = Automa.compile(re) @test occursin(r"^Automa.Machine\(<.*>\)$", repr(machine)) - for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) - (generator == :goto && checkbounds) && continue - ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) + for generator in (:table, :goto), clean in (true, false) + ctx = Automa.CodeGenContext(generator=generator, clean=clean) code = Automa.generate_code(ctx, machine, :debug) validate = @eval function (data) logger = Symbol[] diff --git a/test/test02.jl b/test/test02.jl index 8be6723b..96bea71c 100644 --- a/test/test02.jl +++ b/test/test02.jl @@ -27,9 +27,8 @@ using Test @test last == 0 @test actions == [:enter_re,:enter_a,:final_a,:exit_a,:enter_b,:final_b,:final_re,:exit_b,:exit_re] - for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) - (generator == :goto && checkbounds) && continue - ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) + for generator in (:table, :goto), clean in (true, false) + ctx = Automa.CodeGenContext(generator=generator, clean=clean) code = (Automa.generate_code(ctx, machine, :debug)) validate = @eval function (data) logger = Symbol[] diff --git a/test/test03.jl b/test/test03.jl index f02daabf..a3a1accb 100644 --- a/test/test03.jl +++ b/test/test03.jl @@ -13,15 +13,10 @@ using Test machine = Automa.compile(fasta) - for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) - # Test the default CTX, if none is passed. - # We use the otherwise invalid combinarion :goto && checkbounds to do this - (init_code, exec_code) = if generator == :goto && checkbounds - (Automa.generate_init_code(machine), Automa.generate_exec_code(machine)) - else - ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) - (Automa.generate_init_code(ctx, machine), Automa.generate_exec_code(ctx, machine)) - end + for generator in (:table, :goto), clean in (true, false) + ctx = Automa.CodeGenContext(generator=generator, clean=clean) + init_code = Automa.generate_init_code(ctx, machine) + exec_code = Automa.generate_exec_code(ctx, machine) validate = @eval function (data) $(init_code) $(exec_code) diff --git a/test/test04.jl b/test/test04.jl index 917c46cf..074d8e6d 100644 --- a/test/test04.jl +++ b/test/test04.jl @@ -12,9 +12,8 @@ using Test machine = Automa.compile(beg_a_end_b) - for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) - (generator == :goto && checkbounds) && continue - ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) + for generator in (:table, :goto), clean in (true, false) + ctx = Automa.CodeGenContext(generator=generator, clean=clean) init_code = Automa.generate_init_code(ctx, machine) exec_code = Automa.generate_exec_code(ctx, machine) validate = @eval function (data) diff --git a/test/test05.jl b/test/test05.jl index 9f63cda3..6884ab01 100644 --- a/test/test05.jl +++ b/test/test05.jl @@ -16,9 +16,8 @@ using Test machine = Automa.compile(token) - for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) - (generator == :goto && checkbounds) && continue - ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) + for generator in (:table, :goto), clean in (true, false) + ctx = Automa.CodeGenContext(generator=generator, clean=clean) code = Automa.generate_code(ctx, machine, :debug) validate = @eval function (data) logger = Symbol[] diff --git a/test/test06.jl b/test/test06.jl index 1d0ede2b..76d1f699 100644 --- a/test/test06.jl +++ b/test/test06.jl @@ -23,9 +23,8 @@ using Test end end - for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) - (generator == :goto && checkbounds) && continue - ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) + for generator in (:table, :goto), clean in (true, false) + ctx = Automa.CodeGenContext(generator=generator, clean=clean) run! = @eval function (state, data) ret = [] $(Automa.generate_init_code(machine)) diff --git a/test/test11.jl b/test/test11.jl index b4348cdf..f6a33ce8 100644 --- a/test/test11.jl +++ b/test/test11.jl @@ -23,7 +23,7 @@ using Test @test_throws ErrorException Automa.generate_exec_code(ctx, machine, actions) for clean in (true, false) - ctx = Automa.CodeGenContext(generator=:goto, checkbounds=false, clean=clean) + ctx = Automa.CodeGenContext(generator=:goto, clean=clean) validate = @eval function (data, n) logger = Symbol[] $(Automa.generate_init_code(ctx, machine)) diff --git a/test/test14.jl b/test/test14.jl index 5dd6b675..4523c2ef 100644 --- a/test/test14.jl +++ b/test/test14.jl @@ -16,7 +16,7 @@ using Test return p, cs end - ctx = Automa.CodeGenContext(generator=:goto, checkbounds=false) + ctx = Automa.CodeGenContext(generator=:goto) @eval function validate_goto(data) $(Automa.generate_init_code(ctx, machine)) $(Automa.generate_exec_code(ctx, machine)) diff --git a/test/test15.jl b/test/test15.jl index 449c77d0..f5beff47 100644 --- a/test/test15.jl +++ b/test/test15.jl @@ -22,9 +22,8 @@ using Test @test last == 0 @test actions == [:enter, :all, :final, :exit, :enter, :all, :final, :exit] - for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) - (generator == :goto && checkbounds) && continue - ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) + for generator in (:table, :goto), clean in (true, false) + ctx = Automa.CodeGenContext(generator=generator, clean=clean) validate = @eval function (data) logger = Symbol[] $(Automa.generate_code(ctx, machine, :debug)) diff --git a/test/test16.jl b/test/test16.jl index 5f224b0a..5d8f956d 100644 --- a/test/test16.jl +++ b/test/test16.jl @@ -7,7 +7,7 @@ using Test @testset "Test16" begin re = re"A+(B+C)*(D|E)+" machine = Automa.compile(re) - ctx = Automa.CodeGenContext(generator=:goto, checkbounds=false) + ctx = Automa.CodeGenContext(generator=:goto) code = validate = @eval function (data) $(Automa.generate_init_code(ctx, machine)) diff --git a/test/test17.jl b/test/test17.jl index 1aa7059f..0caabcd0 100644 --- a/test/test17.jl +++ b/test/test17.jl @@ -11,9 +11,8 @@ using Test machine1 = Automa.compile(re1) @test occursin(r"^Automa.Machine\(<.*>\)$", repr(machine1)) - for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) - (generator == :goto && checkbounds) && continue - ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) + for generator in (:table, :goto), clean in (true, false) + ctx = Automa.CodeGenContext(generator=generator, clean=clean) code = Automa.generate_code(ctx, machine1, :debug) validate = @eval function (data) logger = Symbol[] @@ -31,9 +30,8 @@ using Test machine2 = Automa.compile(re2) @test occursin(r"^Automa.Machine\(<.*>\)$", repr(machine2)) - for generator in (:table, :goto), checkbounds in (true, false), clean in (true, false) - (generator == :goto && checkbounds) && continue - ctx = Automa.CodeGenContext(generator=generator, checkbounds=checkbounds, clean=clean) + for generator in (:table, :goto), clean in (true, false) + ctx = Automa.CodeGenContext(generator=generator, clean=clean) code = Automa.generate_code(ctx, machine2, :debug) validate = @eval function (data) logger = Symbol[] From fe52c0a1fc083fcede9a25399d04e54818be5e48 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Mon, 1 Aug 2022 16:20:17 +0200 Subject: [PATCH 19/64] Make clean work --- src/codegen.jl | 32 +++++++------------------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/src/codegen.jl b/src/codegen.jl index 65cb077f..85e06be2 100644 --- a/src/codegen.jl +++ b/src/codegen.jl @@ -130,11 +130,13 @@ function generate_code(ctx::CodeGenContext, machine::Machine, actions=nothing) else quote nothing end end - return quote + code = quote $(generate_init_code(ctx, machine)) $(generate_exec_code(ctx, machine, actions)) $(error_code) end + ctx.clean && Base.remove_linenums!(code) + return code end generate_code(machine::Machine, actions=nothing) = generate_code(DefaultCodeGenContext, machine, actions) @@ -146,13 +148,15 @@ If not passed, the context defaults to `DefaultCodeGenContext` """ function generate_init_code(ctx::CodeGenContext, machine::Machine) vars = ctx.vars - return quote + code = quote $(vars.byte)::UInt8 = 0x00 $(vars.p)::Int = 1 $(vars.p_end)::Int = sizeof($(vars.data)) $(vars.p_eof)::Int = $(vars.p_end) $(vars.cs)::Int = $(machine.start_state) end + ctx.clean && Base.remove_linenums!(code) + return code end generate_init_code(machine::Machine) = generate_init_code(DefaultCodeGenContext, machine) @@ -191,9 +195,7 @@ function generate_exec_code(ctx::CodeGenContext, machine::Machine, actions=nothi # generate code code = ctx.generator(ctx, machine, actions_dict) - if ctx.clean - code = cleanup(code) - end + ctx.clean && Base.remove_linenums!(code) return code end @@ -615,26 +617,6 @@ function isescape(arg) return arg isa Expr && arg.head == :macrocall && arg.args[1] == Symbol("@escape") end -# Clean created code of e.g. Automa source code comments. -# By default not executed, as it's handy for debugging. -function cleanup(ex::Expr) - args = [] - for arg in ex.args - if isa(arg, Expr) - if arg.head == :line - # pass - elseif ex.head == :block && arg.head == :block - append!(args, cleanup(arg).args) - else - push!(args, cleanup(arg)) - end - else - push!(args, arg) - end - end - return Expr(ex.head, args...) -end - function debug_actions(machine::Machine) function log_expr(name) return :(push!(logger, $(QuoteNode(name)))) From 8b50068c4dc7b5fef9da1fac725535aeabdba59f Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Mon, 1 Aug 2022 16:55:07 +0200 Subject: [PATCH 20/64] Make Variables easier to construct --- src/codegen.jl | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/codegen.jl b/src/codegen.jl index 85e06be2..44819506 100644 --- a/src/codegen.jl +++ b/src/codegen.jl @@ -29,6 +29,20 @@ struct Variables byte::Symbol end +function Variables( + ;p=:p, + p_end=:p_end, + p_eof=:p_eof, + ts=:ts, + te=:te, + cs=:cs, + data=:data, + mem=:mem, + byte=:byte +) + Variables(p, p_end, p_eof, ts, te, cs, data, mem, byte) +end + struct CodeGenContext vars::Variables generator::Function From f288142a3057c6ec6d9b08473c03c31995581c23 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Tue, 2 Aug 2022 10:36:28 +0200 Subject: [PATCH 21/64] Add tests for regex set operations --- test/runtests.jl | 2 +- test/test13.jl | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 test/test13.jl diff --git a/test/runtests.jl b/test/runtests.jl index 816f7cac..7abe54a6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -90,7 +90,7 @@ include("test09.jl") include("test10.jl") include("test11.jl") include("test12.jl") -# test13 tested functionality now removed. +include("test13.jl") include("test14.jl") include("test15.jl") include("test16.jl") diff --git a/test/test13.jl b/test/test13.jl new file mode 100644 index 00000000..6e6554d7 --- /dev/null +++ b/test/test13.jl @@ -0,0 +1,26 @@ +module Test13 + +using Automa +using Test + +# Some cases of regex I've seen fail +@testset "Test13" begin + for (regex, good_strings, bad_strings) in [ + (re"[AB]" & re"A", ["A"], ["B", "AA", "AB"]), + (re"(A|B|C|D)" \ re"[A-C]", ["D"], ["AC", "A", "B", "DD"]), + (!re"A[BC]D?E", ["ABCDE", "ABCE"], ["ABDE", "ACE", "ABE"]) + ] + for goto in (false, true) + machine = Automa.compile(regex) + @eval $(Automa.generate_validator_function(:validate, machine, goto)) + for string in good_strings + @test validate(string) === nothing + end + for string in bad_strings + @test validate(string) !== nothing + end + end + end +end + +end # module From af92b38222dbd9f508f698d6f9c3f98b97ec030b Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Wed, 3 Aug 2022 08:32:58 +0200 Subject: [PATCH 22/64] Minor polish --- src/action.jl | 3 +++ src/codegen.jl | 22 +++++++++++++++++----- src/edge.jl | 30 ++++++++++++++---------------- src/machine.jl | 36 ++++++++++++++++++++++++------------ src/nfa.jl | 15 +++++++++++++++ src/tokenizer.jl | 3 ++- 6 files changed, 75 insertions(+), 34 deletions(-) diff --git a/src/action.jl b/src/action.jl index 98fa3be0..1d7080f9 100644 --- a/src/action.jl +++ b/src/action.jl @@ -1,6 +1,9 @@ # Action # ====== +# The name is used by the user to refer to actions in the action_dict. +# The order is the order in which actions assigned to the same transition +# is executed struct Action name::Symbol order::Int diff --git a/src/codegen.jl b/src/codegen.jl index 44819506..85a5f221 100644 --- a/src/codegen.jl +++ b/src/codegen.jl @@ -78,10 +78,8 @@ function CodeGenContext(; getbyte::Function=Base.getindex, clean::Bool=false) # special conditions for simd generator - if generator == :goto - if getbyte != Base.getindex - throw(ArgumentError("GOTO generator only support Base.getindex")) - end + if generator == :goto && getbyte != Base.getindex + throw(ArgumentError("GOTO generator only support Base.getindex")) end # check generator if generator == :table @@ -248,6 +246,8 @@ function generate_table_code(ctx::CodeGenContext, machine::Machine, actions::Dic $(eof_action_code) $(ctx.vars.cs) = 0 elseif $(ctx.vars.cs) < 0 + # If cs < 0, the machine errored. The code above incremented p regardless, + # but on error, we want p to be where the machine errored, so we reset it. $(ctx.vars.p) -= 1 end end # GC.@preserve block @@ -426,11 +426,12 @@ function generate_goto_code(ctx::CodeGenContext, machine::Machine, actions::Dict # Check the final state is an accept state, in an efficient manner final_state_code = generate_final_state_mem_code(ctx, machine) + # This is an overview of the final code structure return quote + $(ctx.vars.mem) = $(SizedMemory)($(ctx.vars.data)) if $(ctx.vars.p) > $(ctx.vars.p_end) @goto exit end - $(ctx.vars.mem) = $(SizedMemory)($(ctx.vars.data)) $(enter_code) $(Expr(:block, blocks...)) @label exit @@ -586,6 +587,14 @@ function generate_input_error_code(ctx::CodeGenContext, machine::Machine) end end +# This is a dummy macro, not actually used in Automa. +# In generated code, Automa may generate this macro, but Automa +# removes it in the `rewrite_special_macros` function before Julia can expand +# the macro. +# I only have this here so if people grep for escape, they find this comment +macro escape() +end + # Used by the :table code generator. function rewrite_special_macros(ctx::CodeGenContext, ex::Expr, eof::Bool) args = [] @@ -631,6 +640,9 @@ function isescape(arg) return arg isa Expr && arg.head == :macrocall && arg.args[1] == Symbol("@escape") end +# Debug actions just pushes the action names into a vector called "logger". +# this exists as a convenience method to allow the user to set actions = :debug +# in generate_exec_code function debug_actions(machine::Machine) function log_expr(name) return :(push!(logger, $(QuoteNode(name)))) diff --git a/src/edge.jl b/src/edge.jl index 078fa89c..731a5a4c 100644 --- a/src/edge.jl +++ b/src/edge.jl @@ -21,25 +21,25 @@ end # Don't override isless, because I don't want to figure out how # to hash correctly. It's fine, we only use this for sorting in order_machine +# The criterion is arbitrary, but ordering must be transitive, +# and this function must deterministically return a Bool when comparing +# two edges from the same node in a Machine function in_sort_order(e1::Edge, e2::Edge) - # First check edges - for (i,j) in zip(e1.labels, e2.labels) - if i < j - return true - elseif j < i - return false - end - end - l1, l2 = length(e1.labels), length(e2.labels) - if l1 < l2 - return true - elseif l2 < l1 - return false + # First check labels + lab1, lab2 = e1.labels, e2.labels + len1, len2 = length(lab1), length(lab2) + len1 < len2 && return true + len2 < len1 && return false + for (i,j) in zip(lab1, lab2) + i < j && return true + j < i && return false end # Then check preconditions p1, p2 = e1.precond, e2.precond lp1, lp2 = length(p1.names), length(p2.names) + lp1 < lp2 && return true + lp2 < lp1 && return false for i in 1:min(lp1, lp2) isless(p1.names[i], p2.names[i]) && return true isless(p2.names[i], p1.names[i]) && return false @@ -47,11 +47,9 @@ function in_sort_order(e1::Edge, e2::Edge) u1 < u2 && return true u2 < u1 && return false end - lp1 < lp2 && return true - lp2 < lp1 && return false # A machine should never have two indistinguishable edges - # so if we reach here, something went wrong + # so if we reach here, something went wrong. error() end diff --git a/src/machine.jl b/src/machine.jl index cc8bc1a8..4821e90f 100644 --- a/src/machine.jl +++ b/src/machine.jl @@ -23,6 +23,16 @@ function findedge(s::Node, b::UInt8) error("$(b) ∈ label not found") end +""" + Machine + +An `Automa.Machine` represents a compiled regular expression in `Automa.jl`. +Its fields are considered internal. Its only use it to use as arguments in +Automa's code generation functions. + +To visualise the DFA represented by a `Machine`, use `Automa.machine2dot` +to construct a DOT file, then visualise it using the `graphviz` software. +""" struct Machine start::Node states::UnitRange{Int} @@ -53,7 +63,12 @@ function machine_names(machine::Machine) end function Base.show(io::IO, machine::Machine) - print(io, summary(machine), "()") + print(io, + summary(machine), + "()" + ) end # Reorder machine states so the states are in a completely deterministic manner. @@ -74,20 +89,16 @@ function reorder_machine(machine::Machine) # Make new nodes complete with edges new_nodes = Dict(i => Node(i) for i in 1:length(old2new)) - oldnodes = collect(traverse(machine.start)) - @assert length(oldnodes) == length(machine.states) for old_node in traverse(machine.start) for (e, t) in old_node.edges + new_node = new_nodes[old2new[old_node.state]] push!( - new_nodes[old2new[old_node.state]].edges, + new_node.edges, (e, new_nodes[old2new[t.state]]) ) - + sort!(new_node.edges; by=first, lt=in_sort_order) end end - for node in values(new_nodes) - sort!(node.edges; by=first, lt=in_sort_order) - end # Rebuild machine and return it Machine( @@ -100,14 +111,15 @@ function reorder_machine(machine::Machine) end """ - compile(re::RegExp; optimize, unambiguous) -> Machine + compile(re::RegExp; optimize::Bool=true, unambiguous::Bool=true) -> Machine -Compile a finite state machine (FSM) from RegExp `re`. If `optimize`, attempt to minimize the number -of states in the FSM. If `unambiguous`, disallow creation of FSM where the actions are not deterministic. +Compile a finite state machine (FSM) from RegExp `re`. +If `optimize`, attempt to minimize the number of states in the FSM. +If `unambiguous`, disallow creation of FSM where the actions are not deterministic. # Examples ``` -machine let +machine = let name = re"[A-Z][a-z]+" first_last = name * re" " * name last_first = name * re", " * name diff --git a/src/nfa.jl b/src/nfa.jl index be35a69f..2dc7e83e 100644 --- a/src/nfa.jl +++ b/src/nfa.jl @@ -13,6 +13,9 @@ function Base.show(io::IO, node::NFANode) print(io, summary(node), "(<", length(node.edges), " edges", '@', objectid(node), ">)") end +# An NFA contains a start and final nodes, which are not the same, as per +# the textbook definition. +# This NFA is an nfa with epsilon transitions. struct NFA start::NFANode final::NFANode @@ -171,7 +174,11 @@ function re2nfa(re::RegExp.RE, predefined_actions::Dict{Symbol,Action}=Dict{Symb return NFA(nfa_start, nfa_final) end +# Removes both dead nodes, i.e. nodes from which there is no path to +# the final node, and also unreachable nodes, i.e. nodes that cannot be +# reached from the start node. function remove_dead_nodes(nfa::NFA) + # Get a dict Node => Set of nodes that point to Node. backrefs = Dict(nfa.start => Set{NFANode}()) for s in traverse(nfa.start), (_, t) in s.edges push!(get!(() -> Set{NFANode}(), backrefs, t), s) @@ -189,6 +196,7 @@ function remove_dead_nodes(nfa::NFA) ) end + # Mark nodes as alive, if the final state can be reached from them. alive = Set{NFANode}() unvisited = [nfa.final] while !isempty(unvisited) @@ -200,13 +208,20 @@ function remove_dead_nodes(nfa::NFA) end end end + + # If this is not true, we threw the big error above. @assert nfa.start ∈ alive @assert nfa.final ∈ alive + # Map from old to new node. newnodes = Dict{NFANode,NFANode}() new(s) = get!(() -> NFANode(), newnodes, s) isvisited(s) = haskey(newnodes, s) unvisited = [nfa.start] + + # Now make a new NFA that only contain nodes marked alive in the previous step. + # since we make this new NFA by traversing from the start node, we also skip + # unreachable nodes while !isempty(unvisited) s = pop!(unvisited) s′ = new(s) diff --git a/src/tokenizer.jl b/src/tokenizer.jl index d7fea0f4..7fd04c12 100644 --- a/src/tokenizer.jl +++ b/src/tokenizer.jl @@ -105,7 +105,8 @@ function generate_table_code(ctx::CodeGenContext, tokenizer::Tokenizer, actions: $(ctx.vars.cs) = -$(ctx.vars.cs) end end - # If in a failed state, reset p (why do we do this?) + # If in a failed state, reset p to where it failed, since it was + # incremented immediately after the state transition if $(ctx.vars.cs) < 0 $(ctx.vars.p) -= 1 end From 2ffe5644975313384e82a34335738946ffa5e97c Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Fri, 12 Aug 2022 08:46:22 +0200 Subject: [PATCH 23/64] Rename p_eof to is_eof p_eof only have two valid values: -1 and p_end. By instead using is_eof::Bool to distinguish these two cases, we make the logic clearer --- docs/src/index.md | 20 +++++++++----------- example/tokenizer.jl | 2 +- src/Stream.jl | 12 +++++------- src/codegen.jl | 20 ++++++++++---------- src/tokenizer.jl | 4 ++-- test/debug.jl | 6 ++++-- test/test09.jl | 2 +- 7 files changed, 32 insertions(+), 34 deletions(-) diff --git a/docs/src/index.md b/docs/src/index.md index a8c614c4..e0440c06 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -217,7 +217,7 @@ the arguments list. Once a pattern is determined, the start and end positions of the token substring can be accessed via `ts` and `te` local variables in the action code. -Other special variables (i.e. `p`, `p_end`, `p_eof` and `cs`) will be explained +Other special variables (i.e. `p`, `p_end`, `is_eof` and `cs`) will be explained in the following section. See example/tokenizer.jl for a complete example. @@ -291,7 +291,7 @@ julia> Automa.generate_init_code(context, machine) quote # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 67: p::Int = 1 # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 68: p_end::Int = sizeof(data) # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 69: - p_eof::Int = p_end # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 70: + is_eof::Bool = true # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 70: cs::Int = 1 end @@ -306,13 +306,11 @@ which depend on `Base.pointer` and `Base.sizeof` methods. So, if `data` is a you want to use your own type, you need to support them. The variable `p` points at the next byte position in `data`. `p_end` points at -the end position of data available in `data`. `p_eof` is similar to `p_end` but -it points at the *actual* end of the input sequence. In the example above, -`p_end` and `p_eof` are soon set to `sizeof(data)` because these two values can -be determined immediately. `p_eof` would be undefined when `data` is too long -to store in memory. In such a case, `p_eof` is set to a negative integer at the -beginning and later set to a suitable position when the end of an input sequence -is seen. The `cs` variable stores the current state of a machine. +the end position of data available in `data`. `is_eof` marks whether `p_end` +points at the *actual* end of the input sequence, instead of the end of a smaller +buffer. In the example above, `p_end` is set to `sizeof(data)`, and `is_eof` is true. +`is_eof` would be `false` to store in memory. +The `cs` variable stores the current state of a machine. The `generate_exec_code` generates code that emulates the FSM execution by updating `cs` (current state) while reading bytes from `data`. You don't need to @@ -336,7 +334,7 @@ quote # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 116: end # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 122: p += 1 end # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 124: - if p > p_eof ≥ 0 && cs ∈ Set([2, 1]) # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 125: + if is_eof && p > p_end && cs ∈ Set([2, 1]) # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 125: if cs == 2 count += 1 else @@ -370,7 +368,7 @@ while p ≤ p_end && cs > 0 p += 1 # increment the position variable end -if p_eof ≥ 0 && p > p_eof && cs ∈ machine.final_states +if is_eof && p > p_end && cs ∈ machine.final_states {{ execute EOF actions if any }} cs = 0 elseif cs < 0 diff --git a/example/tokenizer.jl b/example/tokenizer.jl index dda38ce3..6ba6a238 100644 --- a/example/tokenizer.jl +++ b/example/tokenizer.jl @@ -52,7 +52,7 @@ context = Automa.CodeGenContext() $(Automa.generate_init_code(context, minijulia)) tokens = Tuple{Symbol,String}[] emit(kind) = push!(tokens, (kind, data[ts:te])) - while p ≤ p_eof && cs > 0 + while p ≤ p_end && cs > 0 $(Automa.generate_exec_code(context, minijulia)) end if cs < 0 diff --git a/src/Stream.jl b/src/Stream.jl index d62cae86..3f7f9720 100644 --- a/src/Stream.jl +++ b/src/Stream.jl @@ -80,7 +80,7 @@ function {funcname}(stream::TranscodingStream, {arguments}...) {set up the variables and the data buffer} {execute the machine} {loopcode} - if cs ≤ 0 || p > p_eof ≥ 0 + if cs ≤ 0 || (is_eof && p > p_end) @label __return__ {returncode} end @@ -112,14 +112,12 @@ function generate_reader( $(Automa.generate_init_code(context, machine)) # Overwrite these for Stream, since we don't know EOF or end, # as this is set in the __exec__ part depending on the stream state. - $(context.vars.p_end) = 0 - $(context.vars.p_eof) = -1 + $(vars.p_end) = 0 + $(vars.is_eof) = false $(initcode) @label __exec__ - if $(vars.p_eof) ≥ 0 || eof(stream) - $(vars.p_eof) = buffer.marginpos - 1 - end + $(vars.is_eof) |= eof(stream) $(vars.p) = buffer.bufferpos $(vars.p_end) = buffer.marginpos - 1 $(Automa.generate_exec_code(context, machine, actions)) @@ -127,7 +125,7 @@ function generate_reader( $(loopcode) - if $(vars.cs) ≤ 0 || $(vars.p) > $(vars.p_eof) ≥ 0 + if $(vars.cs) ≤ 0 || ($(vars.is_eof) && $(vars.p) > $(vars.p_end)) @label __return__ $(returncode) end diff --git a/src/codegen.jl b/src/codegen.jl index 85a5f221..132f556e 100644 --- a/src/codegen.jl +++ b/src/codegen.jl @@ -9,7 +9,7 @@ The following variable names may be used in the code. - `p::Int`: current position of data - `p_end::Int`: end position of data -- `p_eof::Int`: end position of file stream +- `is_eof::Bool`: `p_end` marks end of total file stream - `ts::Int`: start position of token (tokenizer only) - `te::Int`: end position of token (tokenizer only) - `cs::Int`: current state @@ -20,7 +20,7 @@ The following variable names may be used in the code. struct Variables p::Symbol p_end::Symbol - p_eof::Symbol + is_eof::Symbol ts::Symbol te::Symbol cs::Symbol @@ -32,7 +32,7 @@ end function Variables( ;p=:p, p_end=:p_end, - p_eof=:p_eof, + is_eof=:is_eof, ts=:ts, te=:te, cs=:cs, @@ -40,7 +40,7 @@ function Variables( mem=:mem, byte=:byte ) - Variables(p, p_end, p_eof, ts, te, cs, data, mem, byte) + Variables(p, p_end, is_eof, ts, te, cs, data, mem, byte) end struct CodeGenContext @@ -56,7 +56,7 @@ function generate_goto_code end """ CodeGenContext(; - vars=Variables(:p, :p_end, :p_eof, :ts, :te, :cs, :data, :mem, :byte), + vars=Variables(:p, :p_end, :is_eof, :ts, :te, :cs, :data, :mem, :byte), generator=:table, getbyte=Base.getindex, clean=false @@ -73,7 +73,7 @@ Arguments - `clean`: flag of code cleansing, e.g. removing line comments """ function CodeGenContext(; - vars::Variables=Variables(:p, :p_end, :p_eof, :ts, :te, :cs, :data, :mem, :byte), + vars::Variables=Variables(:p, :p_end, :is_eof, :ts, :te, :cs, :data, :mem, :byte), generator::Symbol=:table, getbyte::Function=Base.getindex, clean::Bool=false) @@ -164,7 +164,7 @@ function generate_init_code(ctx::CodeGenContext, machine::Machine) $(vars.byte)::UInt8 = 0x00 $(vars.p)::Int = 1 $(vars.p_end)::Int = sizeof($(vars.data)) - $(vars.p_eof)::Int = $(vars.p_end) + $(vars.is_eof)::Bool = true $(vars.cs)::Int = $(machine.start_state) end ctx.clean && Base.remove_linenums!(code) @@ -242,7 +242,7 @@ function generate_table_code(ctx::CodeGenContext, machine::Machine, actions::Dic end # If we're out of bytes and in an accept state, find the correct EOF action # and execute it, then set cs to 0 to signify correct execution - if $(ctx.vars.p) > $(ctx.vars.p_eof) ≥ 0 && $(final_state_code) + if is_eof && $(ctx.vars.p) > $(ctx.vars.p_end) && $(final_state_code) $(eof_action_code) $(ctx.vars.cs) = 0 elseif $(ctx.vars.cs) < 0 @@ -435,7 +435,7 @@ function generate_goto_code(ctx::CodeGenContext, machine::Machine, actions::Dict $(enter_code) $(Expr(:block, blocks...)) @label exit - if $(ctx.vars.p) > $(ctx.vars.p_eof) ≥ 0 && $(final_state_code) + if is_eof && $(ctx.vars.p) > $(ctx.vars.p_end) && $(final_state_code) $(eof_action_code) $(ctx.vars.cs) = 0 end @@ -581,7 +581,7 @@ function generate_input_error_code(ctx::CodeGenContext, machine::Machine) return quote if $(vars.cs) != 0 $(vars.cs) = -abs($(vars.cs)) - $byte_symbol = ($(vars.p_eof) > -1 && $(vars.p) > $(vars.p_eof)) ? nothing : $(vars.byte) + $byte_symbol = $(vars.p) > $(vars.p_end) ? nothing : $(vars.byte) Automa.throw_input_error($(machine), -$(vars.cs), $byte_symbol, $(vars.mem), $(vars.p)) end end diff --git a/src/tokenizer.jl b/src/tokenizer.jl index 7fd04c12..6401f218 100644 --- a/src/tokenizer.jl +++ b/src/tokenizer.jl @@ -50,7 +50,7 @@ function generate_init_code(ctx::CodeGenContext, tokenizer::Tokenizer) quote $(ctx.vars.p)::Int = 1 $(ctx.vars.p_end)::Int = sizeof($(ctx.vars.data)) - $(ctx.vars.p_eof)::Int = $(ctx.vars.p_end) + $(ctx.vars.is_eof)::Bool = true $(ctx.vars.ts)::Int = 0 $(ctx.vars.te)::Int = 0 $(ctx.vars.cs)::Int = $(tokenizer.machine.start_state) @@ -94,7 +94,7 @@ function generate_table_code(ctx::CodeGenContext, tokenizer::Tokenizer, actions: $(action_dispatch_code) $(ctx.vars.p) += 1 end - if $(ctx.vars.p) > p_eof ≥ 0 + if $(ctx.vars.is_eof) && $(ctx.vars.p) > $(ctx.vars.p_end) # If EOF and in accept state, run EOF code and set current state to 0 # meaning accept state if $(generate_final_state_mem_code(ctx, tokenizer.machine)) diff --git a/test/debug.jl b/test/debug.jl index f51f5504..91f59c67 100644 --- a/test/debug.jl +++ b/test/debug.jl @@ -48,7 +48,8 @@ function create_debug_function(machine::Automa.Machine; ascii::Bool=false, quote Tuple{UInt8, Int, Vector{Symbol}}[] end end) $(Automa.generate_init_code(ctx, debugger)) - p_end = p_eof = sizeof($(ctx.vars.data)) + p_end = sizeof($(ctx.vars.data)) + is_eof = true $(Automa.generate_exec_code(ctx, debugger, action_dict)) ($(ctx.vars.cs), $logsym) end @@ -71,7 +72,8 @@ function create_debug_display_function(machine::Automa.Machine; quote function debug_display($(ctx.vars.data)::Union{String, SubString{String}}) $(Automa.generate_init_code(ctx, debugger)) - p_end = p_eof = sizeof($(ctx.vars.data)) + p_end = sizeof($(ctx.vars.data)) + is_eof = true $(Automa.generate_exec_code(ctx, debugger, nothing)) if !iszero($(ctx.vars.cs)) print_error_state($(ctx.vars.data), $(ctx.vars.p), $(ctx.vars.cs)) diff --git a/test/test09.jl b/test/test09.jl index bfe244f7..2b349ac5 100644 --- a/test/test09.jl +++ b/test/test09.jl @@ -17,7 +17,7 @@ using Test $(Automa.generate_init_code(ctx, tokenizer)) tokens = Tuple{Symbol,String}[] emit(kind, range) = push!(tokens, (kind, data[range])) - while p ≤ p_eof && cs > 0 + while p ≤ p_end && cs > 0 $(Automa.generate_exec_code(ctx, tokenizer)) end if cs < 0 From 51fe1bcb5148e753a69b0111a4018b1d6c4b244e Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Wed, 22 Feb 2023 16:48:28 +0100 Subject: [PATCH 24/64] Comment generate_reader better --- src/Stream.jl | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Stream.jl b/src/Stream.jl index 3f7f9720..ce7cdc83 100644 --- a/src/Stream.jl +++ b/src/Stream.jl @@ -91,16 +91,18 @@ end function generate_reader( funcname::Symbol, machine::Automa.Machine; - arguments::Tuple=(), - context::Automa.CodeGenContext=Automa.CodeGenContext(), + arguments=(), + context::Automa.CodeGenContext=Automa.DefaultCodeGenContext, actions::Dict{Symbol,Expr}=Dict{Symbol,Expr}(), initcode::Expr=:(), loopcode::Expr=:(), returncode::Expr=:(return $(context.vars.cs)) ) + # Add a `return` to the return expression if the user forgot it if returncode.head != :return returncode = Expr(:return, returncode) end + # Create the function signature functioncode = :(function $(funcname)(stream::$(TranscodingStream)) end) for arg in arguments push!(functioncode.args[1].args, arg) @@ -116,15 +118,28 @@ function generate_reader( $(vars.is_eof) = false $(initcode) + # Code between __exec__ and the bottom is repeated in a loop, + # in order to continuously read data, filling in new data to the buffer + # once it runs out. + # When the buffer is filled, data in the buffer may shift, which necessitates + # us updating `p` and `p_end`. + # Hence, they need to be redefined here. @label __exec__ + # The eof call here is what refills the buffer, if the buffer is used up, + # eof will try refilling the buffer before returning true $(vars.is_eof) |= eof(stream) $(vars.p) = buffer.bufferpos $(vars.p_end) = buffer.marginpos - 1 $(Automa.generate_exec_code(context, machine, actions)) + + # This function flushes any unused data from the buffer, if it is not marked. + # this way Automa can keep reading data in a smaller buffer Base.skip(stream, $(vars.p) - buffer.bufferpos) $(loopcode) + # If the machine errored, or we're past the end of the stream, actually return. + # Else, keep looping. if $(vars.cs) ≤ 0 || ($(vars.is_eof) && $(vars.p) > $(vars.p_end)) @label __return__ $(returncode) From 82a28364679c9ddfda0ac03099b2a2475a2cdb8b Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Wed, 22 Feb 2023 17:09:47 +0100 Subject: [PATCH 25/64] Small tweaks --- src/Automa.jl | 2 ++ src/Stream.jl | 35 ++++++++++++++++++----------------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/Automa.jl b/src/Automa.jl index 22108267..aee02a3b 100644 --- a/src/Automa.jl +++ b/src/Automa.jl @@ -39,6 +39,7 @@ include("Stream.jl") const RE = Automa.RegExp using .RegExp: @re_str, opt, rep, rep1 +using .Stream: generate_reader # This list of exports lists the API export RE, @@ -51,6 +52,7 @@ export RE, generate_init_code, generate_exec_code, generate_code, + generate_reader, # cat and alt is not exported in favor of * and | opt, diff --git a/src/Stream.jl b/src/Stream.jl index ce7cdc83..56ed01fb 100644 --- a/src/Stream.jl +++ b/src/Stream.jl @@ -18,7 +18,7 @@ Note: `mark(stream)` doesn't work as expected because the reading position is not updated while scanning the stream. """ macro mark() - esc(:(buffer.markpos = p)) + esc(:(__buffer.markpos = p)) end """ @@ -27,7 +27,7 @@ end Get the markerd position. """ macro markpos() - esc(:(buffer.markpos)) + esc(:(__buffer.markpos)) end """ @@ -36,7 +36,7 @@ end Get the relative position of the absolute position `pos`. """ macro relpos(pos) - esc(:(@assert buffer.markpos > 0; $(pos) - buffer.markpos + 1)) + esc(:(@assert __buffer.markpos > 0; $(pos) - __buffer.markpos + 1)) end """ @@ -45,7 +45,7 @@ end Get the absolute position of the relative position `pos`. """ macro abspos(pos) - esc(:(@assert buffer.markpos > 0; $(pos) + buffer.markpos - 1)) + esc(:(@assert __buffer.markpos > 0; $(pos) + __buffer.markpos - 1)) end """ @@ -72,13 +72,15 @@ need to evaluate it in a module in which the generated function is needed. The generated code looks like this: ```julia function {funcname}(stream::TranscodingStream, {arguments}...) - buffer = stream.state.buffer1 - data = buffer.data + __buffer = stream.state.buffer1 + \$(vars.data) = buffer.data {declare variables used by the machine} {initcode} @label __exec__ - {set up the variables and the data buffer} + {fill the buffer if more data is available} + {update p, is_eof and p_end to match buffer} {execute the machine} + {flush used data from the buffer} {loopcode} if cs ≤ 0 || (is_eof && p > p_end) @label __return__ @@ -109,14 +111,13 @@ function generate_reader( end vars = context.vars functioncode.args[2] = quote - buffer = stream.state.buffer1 - data = buffer.data + __buffer = stream.state.buffer1 + $(vars.data) = __buffer.data $(Automa.generate_init_code(context, machine)) - # Overwrite these for Stream, since we don't know EOF or end, - # as this is set in the __exec__ part depending on the stream state. - $(vars.p_end) = 0 - $(vars.is_eof) = false $(initcode) + # Overwrite is_eof for Stream, since we don't know the real EOF + # until after we've actually seen the stream eof + $(vars.is_eof) = false # Code between __exec__ and the bottom is repeated in a loop, # in order to continuously read data, filling in new data to the buffer @@ -127,14 +128,14 @@ function generate_reader( @label __exec__ # The eof call here is what refills the buffer, if the buffer is used up, # eof will try refilling the buffer before returning true - $(vars.is_eof) |= eof(stream) - $(vars.p) = buffer.bufferpos - $(vars.p_end) = buffer.marginpos - 1 + $(vars.is_eof) = eof(stream) + $(vars.p) = __buffer.bufferpos + $(vars.p_end) = __buffer.marginpos - 1 $(Automa.generate_exec_code(context, machine, actions)) # This function flushes any unused data from the buffer, if it is not marked. # this way Automa can keep reading data in a smaller buffer - Base.skip(stream, $(vars.p) - buffer.bufferpos) + $(vars.p) > __buffer.bufferpos && Base.skip(stream, $(vars.p) - __buffer.bufferpos) $(loopcode) From d6351b8341a987e951a47487e35b451031eda271 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Wed, 22 Feb 2023 19:41:29 +0100 Subject: [PATCH 26/64] Add default error in generated reader function --- src/Stream.jl | 9 +++++++-- test/runtests.jl | 12 ++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Stream.jl b/src/Stream.jl index 56ed01fb..26325589 100644 --- a/src/Stream.jl +++ b/src/Stream.jl @@ -98,7 +98,8 @@ function generate_reader( actions::Dict{Symbol,Expr}=Dict{Symbol,Expr}(), initcode::Expr=:(), loopcode::Expr=:(), - returncode::Expr=:(return $(context.vars.cs)) + returncode::Expr=:(return $(context.vars.cs)), + errorcode::Expr=Automa.generate_input_error_code(context, machine) ) # Add a `return` to the return expression if the user forgot it if returncode.head != :return @@ -139,9 +140,13 @@ function generate_reader( $(loopcode) + if $(vars.cs) < 0 + $(errorcode) + end + # If the machine errored, or we're past the end of the stream, actually return. # Else, keep looping. - if $(vars.cs) ≤ 0 || ($(vars.is_eof) && $(vars.p) > $(vars.p_end)) + if $(vars.cs) == 0 || ($(vars.is_eof) && $(vars.p) > $(vars.p_end)) @label __return__ $(returncode) end diff --git a/test/runtests.jl b/test/runtests.jl index 7abe54a6..6a0422ab 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -168,7 +168,7 @@ machine = let newline = re"\r?\n" Automa.compile(line * newline) end -Automa.Stream.generate_reader(:readline, machine) |> eval +Automa.Stream.generate_reader(:readline, machine; errorcode=:(return cs)) |> eval @testset "Scanning a line" begin for (data, state) in [ ("\n", :ok), @@ -205,7 +205,15 @@ actions = Dict( ) initcode = :(start_alphanum = end_alphanum = 0) returncode = :(return cs == 0 ? String(data[@abspos(start_alphanum):@abspos(end_alphanum)]) : "") -Automa.Stream.generate_reader(:stripwhitespace, machine, actions=actions, initcode=initcode, returncode=returncode) |> eval +Automa.Stream.generate_reader( + :stripwhitespace, + machine, + actions=actions, + initcode=initcode, + returncode=returncode, + errorcode=:(return "") +) |> eval + @testset "Stripping whitespace" begin for (data, value) in [ ("x", "x"), From 15b638939a17d916b39de7594b0d3f951caf2eb8 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Thu, 23 Feb 2023 16:26:48 +0100 Subject: [PATCH 27/64] Fix bug in execute_debug --- test/debug.jl | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/test/debug.jl b/test/debug.jl index 91f59c67..13700b63 100644 --- a/test/debug.jl +++ b/test/debug.jl @@ -56,32 +56,6 @@ function create_debug_function(machine::Automa.Machine; ascii::Bool=false, end end -function print_error_state(s::AbstractString, p::Int, cs::Int) - println(s[thisind(s, max(1, p-20)):thisind(s, min(ncodeunits(s), p+20))]) - before = Base.Unicode.textwidth(s[1:thisind(s, p-1)]) - print(' ' ^ before) - println('^') - println(' ' ^ before, "Parser stopped at byte $p") - println(' ' ^ before, "at machine state $(-cs)") -end - -function create_debug_display_function(machine::Automa.Machine; - ctx::Union{Automa.CodeGenContext, Nothing}=nothing -) - ctx = ctx === nothing ? Automa.CodeGenContext() : ctx - quote - function debug_display($(ctx.vars.data)::Union{String, SubString{String}}) - $(Automa.generate_init_code(ctx, debugger)) - p_end = sizeof($(ctx.vars.data)) - is_eof = true - $(Automa.generate_exec_code(ctx, debugger, nothing)) - if !iszero($(ctx.vars.cs)) - print_error_state($(ctx.vars.data), $(ctx.vars.p), $(ctx.vars.cs)) - end - end - end -end - function debug_execute(re::Automa.RegExp.RE, data::Vector{UInt8}; ascii=false) machine = Automa.compile(re, optimize=false) s = machine.start @@ -97,6 +71,7 @@ function debug_execute(re::Automa.RegExp.RE, data::Vector{UInt8}; ascii=false) rethrow() end cs = -cs + break end end if cs ∈ machine.final_states && haskey(machine.eof_actions, s.state) From 48baf893f0c333622bd629407debce1c367b2d7d Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Thu, 23 Feb 2023 16:27:12 +0100 Subject: [PATCH 28/64] Export machine2dot --- src/Automa.jl | 5 ++++- src/dot.jl | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Automa.jl b/src/Automa.jl index aee02a3b..ef6531d7 100644 --- a/src/Automa.jl +++ b/src/Automa.jl @@ -57,6 +57,9 @@ export RE, # cat and alt is not exported in favor of * and | opt, rep, - rep1 + rep1, + + # Debugging functionality + machine2dot end # module diff --git a/src/dot.jl b/src/dot.jl index 32038817..b02343b5 100644 --- a/src/dot.jl +++ b/src/dot.jl @@ -39,6 +39,11 @@ function dfa2dot(dfa::DFA) return String(take!(out)) end +""" + machine2dot(machine::Machine)::String + +Return a String with a flowchart of the machine in Graphviz (dot) format. +""" function machine2dot(machine::Machine) out = IOBuffer() println(out, "digraph {") From cd94e07ab6a790534789372228aef8328b06c532 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Thu, 23 Feb 2023 16:59:20 +0100 Subject: [PATCH 29/64] Make more use of magical macros --- src/Stream.jl | 44 ++++++---------------- src/codegen.jl | 98 ++++++++++++++++++++++++++++++------------------ test/runtests.jl | 2 +- 3 files changed, 74 insertions(+), 70 deletions(-) diff --git a/src/Stream.jl b/src/Stream.jl index 26325589..ed6c5d70 100644 --- a/src/Stream.jl +++ b/src/Stream.jl @@ -9,43 +9,22 @@ module Stream import Automa import TranscodingStreams: TranscodingStream -""" - @mark() - -Mark at the current position. - -Note: `mark(stream)` doesn't work as expected because the reading position is -not updated while scanning the stream. -""" -macro mark() - esc(:(__buffer.markpos = p)) -end - -""" - @markpos() - -Get the markerd position. -""" -macro markpos() - esc(:(__buffer.markpos)) -end - """ @relpos(pos) Get the relative position of the absolute position `pos`. """ macro relpos(pos) - esc(:(@assert __buffer.markpos > 0; $(pos) - __buffer.markpos + 1)) + esc(:(@assert buffer.markpos > 0; $(pos) - buffer.markpos + 1)) end """ @abspos(pos) Get the absolute position of the relative position `pos`. -""" +""" macro abspos(pos) - esc(:(@assert __buffer.markpos > 0; $(pos) + __buffer.markpos - 1)) + esc(:(@assert buffer.markpos > 0; $(pos) + buffer.markpos - 1)) end """ @@ -80,7 +59,7 @@ function {funcname}(stream::TranscodingStream, {arguments}...) {fill the buffer if more data is available} {update p, is_eof and p_end to match buffer} {execute the machine} - {flush used data from the buffer} + {update buffer position to value of p} {loopcode} if cs ≤ 0 || (is_eof && p > p_end) @label __return__ @@ -112,8 +91,8 @@ function generate_reader( end vars = context.vars functioncode.args[2] = quote - __buffer = stream.state.buffer1 - $(vars.data) = __buffer.data + $(vars.buffer) = stream.state.buffer1 + $(vars.data) = $(vars.buffer).data $(Automa.generate_init_code(context, machine)) $(initcode) # Overwrite is_eof for Stream, since we don't know the real EOF @@ -130,13 +109,12 @@ function generate_reader( # The eof call here is what refills the buffer, if the buffer is used up, # eof will try refilling the buffer before returning true $(vars.is_eof) = eof(stream) - $(vars.p) = __buffer.bufferpos - $(vars.p_end) = __buffer.marginpos - 1 + $(vars.p) = $(vars.buffer).bufferpos + $(vars.p_end) = $(vars.buffer).marginpos - 1 $(Automa.generate_exec_code(context, machine, actions)) - - # This function flushes any unused data from the buffer, if it is not marked. - # this way Automa can keep reading data in a smaller buffer - $(vars.p) > __buffer.bufferpos && Base.skip(stream, $(vars.p) - __buffer.bufferpos) + + # Advance the buffer, hence advancing the stream itself + $(vars.buffer).bufferpos = $(vars.p) $(loopcode) diff --git a/src/codegen.jl b/src/codegen.jl index 132f556e..ec46e132 100644 --- a/src/codegen.jl +++ b/src/codegen.jl @@ -16,6 +16,7 @@ The following variable names may be used in the code. - `data::Any`: input data - `mem::SizedMemory`: input data memory - `byte::UInt8`: current data byte +- `buffer::TranscodingStreams.Buffer`: If reading from an IO """ struct Variables p::Symbol @@ -27,6 +28,7 @@ struct Variables data::Symbol mem::Symbol byte::Symbol + buffer::Symbol end function Variables( @@ -38,9 +40,10 @@ function Variables( cs=:cs, data=:data, mem=:mem, - byte=:byte + byte=:byte, + buffer=:buffer ) - Variables(p, p_end, is_eof, ts, te, cs, data, mem, byte) + Variables(p, p_end, is_eof, ts, te, cs, data, mem, byte, buffer) end struct CodeGenContext @@ -73,7 +76,7 @@ Arguments - `clean`: flag of code cleansing, e.g. removing line comments """ function CodeGenContext(; - vars::Variables=Variables(:p, :p_end, :is_eof, :ts, :te, :cs, :data, :mem, :byte), + vars::Variables=Variables(:p, :p_end, :is_eof, :ts, :te, :cs, :data, :mem, :byte, :buffer), generator::Symbol=:table, getbyte::Function=Base.getindex, clean::Bool=false) @@ -310,7 +313,7 @@ function generate_action_dispatch_code(ctx::CodeGenContext, machine::Machine, ac # else if act == 2 (... etc) action_dispatch_code = foldr(default, action_ids) do names_id, els names, id = names_id - action_code = rewrite_special_macros(ctx, generate_action_code(names, actions), false) + action_code = rewrite_special_macros(ctx, generate_action_code(names, actions), false, nothing) return Expr(:if, :($(act) == $(id)), action_code, els) end # Action code is: Get the action int from the state and current input byte @@ -421,7 +424,7 @@ function generate_goto_code(ctx::CodeGenContext, machine::Machine, actions::Dict # When EOF, go through a list of if/else statements: If cs == 1, do this, elseif # cs == 2 do that etc - eof_action_code = rewrite_special_macros(ctx, generate_eof_action_code(ctx, machine, actions), true) + eof_action_code = rewrite_special_macros(ctx, generate_eof_action_code(ctx, machine, actions), true, nothing) # Check the final state is an accept state, in an efficient manner final_state_code = generate_final_state_mem_code(ctx, machine) @@ -484,7 +487,7 @@ end function generate_eof_action_code(ctx::CodeGenContext, machine::Machine, actions::Dict{Symbol,Expr}) return foldr(:(), machine.eof_actions) do s_as, els s, as = s_as - action_code = rewrite_special_macros(ctx, generate_action_code(action_names(as), actions), true) + action_code = rewrite_special_macros(ctx, generate_action_code(action_names(as), actions), true, nothing) Expr(:if, state_condition(ctx, s), action_code, els) end end @@ -587,27 +590,52 @@ function generate_input_error_code(ctx::CodeGenContext, machine::Machine) end end -# This is a dummy macro, not actually used in Automa. -# In generated code, Automa may generate this macro, but Automa +# These are dummy macros +# Users may use this macro, but Automa # removes it in the `rewrite_special_macros` function before Julia can expand # the macro. -# I only have this here so if people grep for escape, they find this comment -macro escape() -end - -# Used by the :table code generator. -function rewrite_special_macros(ctx::CodeGenContext, ex::Expr, eof::Bool) +# I only have this here so if people grep for these macros, they find this comment +# macro escape() +# macro mark() +# macro unmark() +# macro markpos() +# macro get_buffer_pos() +# macro set_buffer_pos() + +# If cs is nothing, we're in the table generator and do not need to set `cs`. Then we use break. +# if it's an int, we set cs and escape using @goto +function rewrite_special_macros(ctx::CodeGenContext, ex::Expr, eof::Bool, cs::Union{Int, Nothing}) args = [] for arg in ex.args - if isescape(arg) + special_macro = special_macro_type(arg) + if special_macro isa Symbol if !eof - push!(args, quote - $(ctx.vars.p) += 1 - break - end) + expr = if special_macro == Symbol("@escape") + if cs === nothing + quote + $(ctx.vars.p) += 1 + break + end + else + quote + $(ctx.vars.cs) = $(cs) + $(ctx.vars.p) += 1 + @goto exit + end + end + elseif special_macro == Symbol("@mark") + quote $(ctx.vars.buffer).markpos = $(ctx.vars.p) end + elseif special_macro == Symbol("@unmark") + quote $(ctx.vars.buffer).markpos = 0 end + elseif special_macro == Symbol("@markpos") + quote $(ctx.vars.buffer).markpos end + elseif special_macro == Symbol("@setp") + quote $(ctx.vars.p) = $(ctx.vars.buffer).markpos end + end + push!(args, expr) end elseif isa(arg, Expr) - push!(args, rewrite_special_macros(ctx, arg, eof)) + push!(args, rewrite_special_macros(ctx, arg, eof, cs)) else push!(args, arg) end @@ -615,25 +643,23 @@ function rewrite_special_macros(ctx::CodeGenContext, ex::Expr, eof::Bool) return Expr(ex.head, args...) end -# Used by the :goto code generator. -function rewrite_special_macros(ctx::CodeGenContext, ex::Expr, eof::Bool, cs::Int) - args = [] - for arg in ex.args - if isescape(arg) - if !eof - push!(args, quote - $(ctx.vars.cs) = $(cs) - $(ctx.vars.p) += 1 - @goto exit - end) +function special_macro_type(arg) + (arg isa Expr && arg.head == :macrocall) || return nothing + sym = arg.args[1] + if sym == Symbol("@escape") || + sym == Symbol("@mark") || + sym == Symbol("@unmark") || + sym == Symbol("@markpos") || + sym == Symbol("@setp") + + if any(view(arg.args, 2:lastindex(arg.args))) do a + !(a isa LineNumberNode) end - elseif isa(arg, Expr) - push!(args, rewrite_special_macros(ctx, arg, eof, cs)) - else - push!(args, arg) + error("Special Automa macro $(sym) used with arguments: Use it like this: `$(sym)()`") end + return sym end - return Expr(ex.head, args...) + return nothing end function isescape(arg) diff --git a/test/runtests.jl b/test/runtests.jl index 6a0422ab..a41718a9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -313,7 +313,7 @@ returncode = quote end Automa.Stream.generate_reader(:readrecord!, machine, arguments=(:(state::Int),), actions=actions, initcode=initcode, loopcode=loopcode, returncode=returncode) |> eval ctx = Automa.CodeGenContext( - vars=Automa.Variables(:pointerindex, :p_ending, :p_fileend, :ts, :te, :current_state, :buffer, gensym(), gensym()), + vars=Automa.Variables(:pointerindex, :p_ending, :p_fileend, :ts, :te, :current_state, :buffer, gensym(), gensym(), :buffer), generator=:goto, ) Automa.Stream.generate_reader(:readrecord2!, machine, context=ctx, arguments=(:(state::Int),), actions=actions, initcode=initcode, loopcode=loopcode, returncode=returncode) |> eval From ba32c780b9d92d2c6f81579d38c24fc0ab84b238 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Thu, 23 Feb 2023 20:10:37 +0100 Subject: [PATCH 30/64] Add generate_io_validator --- src/Automa.jl | 3 +- src/Stream.jl | 120 ++++++++++++++++++++++++++++++++++++++-------- src/codegen.jl | 20 ++++++-- src/re.jl | 35 ++++++++++++++ test/simd.jl | 6 +-- test/test13.jl | 3 +- test/test18.jl | 6 +-- test/validator.jl | 76 ++++++++++++++++++++++++++--- 8 files changed, 228 insertions(+), 41 deletions(-) diff --git a/src/Automa.jl b/src/Automa.jl index ef6531d7..9fffdf11 100644 --- a/src/Automa.jl +++ b/src/Automa.jl @@ -39,7 +39,7 @@ include("Stream.jl") const RE = Automa.RegExp using .RegExp: @re_str, opt, rep, rep1 -using .Stream: generate_reader +using .Stream: generate_reader, generate_io_validator # This list of exports lists the API export RE, @@ -53,6 +53,7 @@ export RE, generate_exec_code, generate_code, generate_reader, + generate_io_validator, # cat and alt is not exported in favor of * and | opt, diff --git a/src/Stream.jl b/src/Stream.jl index ed6c5d70..4e5aebc5 100644 --- a/src/Stream.jl +++ b/src/Stream.jl @@ -7,7 +7,7 @@ deprecations. module Stream import Automa -import TranscodingStreams: TranscodingStream +import TranscodingStreams: TranscodingStream, NoopStream """ @relpos(pos) @@ -47,26 +47,9 @@ need to evaluate it in a module in which the generated function is needed. - `initcode`: Initialization code (default: `:()`). - `loopcode`: Loop code (default: `:()`). - `returncode`: Return code (default: `:(return cs)`). +- `errorcode`: Executed if `cs < 0` after `loopcode` (default error message) -The generated code looks like this: -```julia -function {funcname}(stream::TranscodingStream, {arguments}...) - __buffer = stream.state.buffer1 - \$(vars.data) = buffer.data - {declare variables used by the machine} - {initcode} - @label __exec__ - {fill the buffer if more data is available} - {update p, is_eof and p_end to match buffer} - {execute the machine} - {update buffer position to value of p} - {loopcode} - if cs ≤ 0 || (is_eof && p > p_end) - @label __return__ - {returncode} - end - @goto __exec__ -end +See the source code of this function to see how the generated code looks like ``` """ function generate_reader( @@ -133,4 +116,101 @@ function generate_reader( return functioncode end +""" + generate_io_validator(funcname::Symbol, regex::RE; goto::Bool=false, report_col::Bool=true) + +Create code that, when evaluated, defines a function named `funcname`. +This function takes an `IO`, and checks if the data in the input conforms +to the regex, without executing any actions. +If the input conforms, return `nothing`. +If `report_col` is set, return `(byte, (line, col))`, else return `(byte, line)`, +where `byte` is the first invalid byte, and `(line, col)` the 1-indexed position of that byte. +If the invalid byte is a `\n` byte, `col` is 0 and the line number is incremented. +If the input errors due to unexpected EOF, `byte` is `nothing`, and the line and column +given is the last byte in the file. +If `report_col` is set, the validator may buffer one line of the input. +If the input has very long lines that should not be buffered, set it to `false`. +If `goto`, the function uses the faster but more complicated `:goto` code. +""" +function generate_io_validator( + funcname::Symbol, + regex::RegExp.RE; + goto::Bool=false, + report_col::Bool=true + ) + ctx = if goto + Automa.CodeGenContext(generator=:goto) + else + Automa.DefaultCodeGenContext + end + vars = ctx.vars + returncode = if report_col + quote + return if iszero(cs) + nothing + else + col = $(vars.p) - $(vars.buffer).markpos + # Report position of last byte before EOF + error_byte = if $(vars.p) > $(vars.p_end) + col -= 1 + nothing + else + col -= $(vars.byte) == UInt8('\n') + $(vars.byte) + end + (error_byte, (line_num, col)) + end + end + else + quote + return if iszero(cs) + nothing + else + error_byte = if $(vars.p) > $(vars.p_end) + nothing + else + $(vars.byte) + end + (error_byte, line_num) + end + end + end + machine = compile(RegExp.set_newline_actions(regex)) + actions = if :newline ∈ machine_names(machine) + Dict{Symbol, Expr}(:newline => quote + line_num += 1 + $(report_col ? :(@mark()) : :()) + end + ) + else + Dict{Symbol, Expr}() + end + machine_names(machine) + function_code = generate_reader( + funcname, + machine; + context=ctx, + initcode=:(line_num = 1; @unmark()), + actions=actions, + returncode=returncode, + errorcode=:(@goto __return__), + ) + return quote + """ + $($(funcname))(io::IO) + + Checks if the data in `io` conforms to the given regex specified at function definition time. + If the input conforms, return `nothing`. + Else return `(byte, (line, col))` where `byte` is the first invalid byte, + and `(line, col)` the 1-indexed position of that byte. + If the invalid byte is a `\n` byte, `col` is 0. + If the input errors due to unexpected EOF, `byte` is `nothing`, and the line and column + given is the last byte in the file. + """ + $function_code + + $(funcname)(io::$(IO)) = $(funcname)($(NoopStream)(io)) + end +end + end # module diff --git a/src/codegen.jl b/src/codegen.jl index ec46e132..0d25a79f 100644 --- a/src/codegen.jl +++ b/src/codegen.jl @@ -98,29 +98,37 @@ end const DefaultCodeGenContext = CodeGenContext() """ - generate_validator_function(name::Symbol, machine::Machine, goto=false) + generate_validator_function(name::Symbol, regexp::RE, goto=false) Generate code that, when evaluated, defines a function named `name`, which takes a single argument `data`, interpreted as a sequence of bytes. The function returns `nothing` if `data` matches `Machine`, else the index of the first -invalid byte. If the machine reached unexpected EOF, returns `sizeof(data) + 1`. +invalid byte. If the machine reached unexpected EOF, returns `0`. If `goto`, the function uses the faster but more complicated `:goto` code. """ -function generate_validator_function(name::Symbol, machine::Machine, goto::Bool=false) +function generate_validator_function(name::Symbol, regex::RegExp.RE, goto::Bool=false) ctx = goto ? CodeGenContext(generator=:goto) : DefaultCodeGenContext + machine = compile(RegExp.strip_actions(regex)) return quote """ $($(name))(data)::Union{Int, Nothing} Checks if `data`, interpreted as a bytearray, conforms to the given `Automa.Machine`. Returns `nothing` if it does, else the byte index of the first invalid byte. - If the machine reached unexpected EOF, returns `sizeof(data) + 1`. + If the machine reached unexpected EOF, returns `0`. """ function $(name)(data) $(generate_init_code(ctx, machine)) $(generate_exec_code(ctx, machine)) # By convention, Automa lets cs be 0 if machine executed correctly. - iszero($(ctx.vars.cs)) ? nothing : p + return if iszero($(ctx.vars.cs)) + nothing + # Else if EOF + elseif $(ctx.vars.p) > $(ctx.vars.p_end) + 0 + else + p + end end end end @@ -175,6 +183,8 @@ function generate_init_code(ctx::CodeGenContext, machine::Machine) end generate_init_code(machine::Machine) = generate_init_code(DefaultCodeGenContext, machine) + + """ generate_exec_code([::CodeGenContext], machine::Machine, actions=nothing)::Expr diff --git a/src/re.jl b/src/re.jl index 66d78986..0bda96d0 100644 --- a/src/re.jl +++ b/src/re.jl @@ -360,4 +360,39 @@ function shallow_desugar(re::RE) end end +# Create a deep copy of the regex without any actions +function strip_actions(re::RE) + args = [arg isa RE ? strip_actions(arg) : arg for arg in re.args] + RE(re.head, args, Dict{Symbol, Vector{Symbol}}(), re.when) +end + +# Create a deep copy with the only actions being a :newline action +# on the \n chars +function set_newline_actions(re::RE)::RE + # Normalise the regex first to make it simpler to work with + if re.head ∈ (:rep1, :opt, :neg, :byte, :range, :class, :cclass, :char, :str, :bytes) + re = shallow_desugar(re) + end + # After desugaring, the only type of regex that can directly contain a newline is the :set type + # if it has that, we add a :newline action + if re.head == :set + set = only(re.args)::ByteSet + if UInt8('\n') ∈ set + re1 = RE(:set, [ByteSet(UInt8('\n'))], Dict(:enter => [:newline]), re.when) + if length(set) == 1 + re1 + else + re2 = RE(:set, [setdiff(set, ByteSet(UInt8('\n')))], Dict{Symbol, Vector{Symbol}}(), re.when) + re1 | re2 + end + else + re + end + else + args = [arg isa RE ? set_newline_actions(arg) : arg for arg in re.args] + RE(re.head, args, Dict{Symbol, Vector{Symbol}}(), re.when) + end +end + + end diff --git a/test/simd.jl b/test/simd.jl index a7d86677..8eceb1dd 100644 --- a/test/simd.jl +++ b/test/simd.jl @@ -9,16 +9,16 @@ const re = Automa.RegExp import Automa.RegExp: @re_str @testset "SIMD generator" begin - machine = let + regex = let seq = re"[A-Z]+" name = re"[a-z]+" rec = re">" * name * re"\n" * seq - Automa.compile(re.opt(rec) * re.rep(re"\n" * rec)) + re.opt(rec) * re.rep(re"\n" * rec) end context = Automa.CodeGenContext(generator=:goto) - eval(Automa.generate_validator_function(:is_valid_fasta, machine, true)) + eval(Automa.generate_validator_function(:is_valid_fasta, regex, true)) s1 = ">seq\nTAGGCTA\n>hello\nAJKGMP" s2 = ">seq1\nTAGGC" diff --git a/test/test13.jl b/test/test13.jl index 6e6554d7..1956b460 100644 --- a/test/test13.jl +++ b/test/test13.jl @@ -11,8 +11,7 @@ using Test (!re"A[BC]D?E", ["ABCDE", "ABCE"], ["ABDE", "ACE", "ABE"]) ] for goto in (false, true) - machine = Automa.compile(regex) - @eval $(Automa.generate_validator_function(:validate, machine, goto)) + @eval $(Automa.generate_validator_function(:validate, regex, goto)) for string in good_strings @test validate(string) === nothing end diff --git a/test/test18.jl b/test/test18.jl index b771436f..7fea6d9e 100644 --- a/test/test18.jl +++ b/test/test18.jl @@ -5,10 +5,10 @@ using Automa.RegExp: @re_str using Test @testset "Test18" begin - machine = Automa.compile(re"\0\a\b\t\n\v\r\x00\xff\xFF[\\][^\\]") + regex = re"\0\a\b\t\n\v\r\x00\xff\xFF[\\][^\\]" for goto in (false, true) println(goto) - @eval $(Automa.generate_validator_function(:validate, machine, goto)) + @eval $(Automa.generate_validator_function(:validate, regex, goto)) # Bad input types @test_throws Exception validate(18) @@ -19,7 +19,7 @@ using Test bad_input = b"\0\a\b\t\n\v\r\x00\xff\xFF\\\\\\" @test validate(bad_input) == lastindex(bad_input) bad_input = b"\0\a\b\t\n\v\r\x00\xff\xFF\\" - @test validate(bad_input) == lastindex(bad_input) + 1 + @test validate(bad_input) == 0 end end diff --git a/test/validator.jl b/test/validator.jl index d8ebd691..609bdb03 100644 --- a/test/validator.jl +++ b/test/validator.jl @@ -2,14 +2,16 @@ module Validator import Automa import Automa.RegExp: @re_str +using TranscodingStreams: NoopStream using Test @testset "Validator" begin - machine = let - Automa.compile(re"a(bc)*|(def)|x+" | re"def" | re"x+") - end - eval(Automa.generate_validator_function(:foobar, machine, false)) - eval(Automa.generate_validator_function(:barfoo, machine, true)) + regex = re"a(bc)*|(def)|x+" | re"def" | re"x+" + eval(Automa.generate_validator_function(:foobar, regex, false)) + eval(Automa.generate_validator_function(:barfoo, regex, true)) + + eval(Automa.generate_io_validator(:io_bar, regex; goto=false)) + eval(Automa.generate_io_validator(:io_foo, regex; goto=true)) for good_data in [ "def" @@ -18,7 +20,12 @@ using Test "x" "xxxxxx" ] - @test foobar(good_data) === barfoo(good_data) === nothing + @test foobar(good_data) === + barfoo(good_data) === + io_foo(IOBuffer(good_data)) === + io_bar(IOBuffer(good_data)) === + io_bar(NoopStream(IOBuffer(good_data))) === + nothing end for bad_data in [ @@ -29,7 +36,62 @@ using Test "defdef", "xabc" ] - @test foobar(bad_data) === barfoo(bad_data) !== nothing + @test foobar(bad_data) === + barfoo(bad_data) !== + nothing + + @test io_foo(IOBuffer(bad_data)) == + io_bar(IOBuffer(bad_data)) == + io_bar(NoopStream(IOBuffer(bad_data))) != + nothing + end +end + +@testset "Multiline validator" begin + regex = re"(>[a-z]+\n)+" + eval(Automa.generate_io_validator(:io_bar_2, regex; goto=false)) + eval(Automa.generate_io_validator(:io_foo_2, regex; goto=true)) + + let data = ">abc" + @test io_bar_2(IOBuffer(data)) == io_foo_2(IOBuffer(data)) == (nothing, (1, 4)) + end + + let data = ">abc:a" + @test io_bar_2(IOBuffer(data)) == io_foo_2(IOBuffer(data)) == (UInt8(':'), (1, 5)) + end + + let data = ">" + @test io_bar_2(IOBuffer(data)) == io_foo_2(IOBuffer(data)) == (nothing, (1, 1)) + end + + let data = "" + @test io_bar_2(IOBuffer(data)) == io_foo_2(IOBuffer(data)) == (nothing, (1, 0)) + end + + let data = ">abc\n>def\n>ghi\n>j!" + @test io_bar_2(IOBuffer(data)) == io_foo_2(IOBuffer(data)) == (UInt8('!'), (4, 3)) + end + + let data = ">abc\n;" + @test io_bar_2(IOBuffer(data)) == io_foo_2(IOBuffer(data)) == (UInt8(';'), (2, 1)) + end +end + +@testset "Report column or not" begin + regex = re"[a-z]+" + eval(Automa.generate_io_validator(:io_foo_3, regex; goto=false, report_col=true)) + eval(Automa.generate_io_validator(:io_bar_3, regex; goto=false, report_col=false)) + + let data = "abc;" + @test io_foo_3(IOBuffer(data)) == (UInt8(';'), (1, 4)) + @test io_bar_3(IOBuffer(data)) == (UInt8(';'), 1) + end + + # Test that, if `report_col` is not set, very long lines are not + # buffered (because the mark is not set). + let data = repeat("abcd", 100_000) * ';' + io = NoopStream(IOBuffer(data)) + @test length(io.state.buffer1.data) < 100_000 end end From f078d29e34b06b1fcd4ef4f722e23fa3d6d9018d Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Sat, 25 Feb 2023 16:48:46 +0100 Subject: [PATCH 31/64] Add documentation to pseudomacros Add macros with the same name as pseudomacros. These return a string explaining that the pseudomacros are not to be used in orginary Julia code, and also the macros can be documented. --- src/codegen.jl | 118 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 105 insertions(+), 13 deletions(-) diff --git a/src/codegen.jl b/src/codegen.jl index 0d25a79f..00770f82 100644 --- a/src/codegen.jl +++ b/src/codegen.jl @@ -600,17 +600,106 @@ function generate_input_error_code(ctx::CodeGenContext, machine::Machine) end end -# These are dummy macros -# Users may use this macro, but Automa -# removes it in the `rewrite_special_macros` function before Julia can expand -# the macro. -# I only have this here so if people grep for these macros, they find this comment -# macro escape() -# macro mark() -# macro unmark() -# macro markpos() -# macro get_buffer_pos() -# macro set_buffer_pos() +# Add a warning if users go down a rabbit hole trying to figure out what these macros +# expand to. +# See the function `rewrite_special_macros` below, where the expansion happens +const WARNING_STRING = """This string comes from the expansion of a fake macro in Automa.jl. \ +It is intercepted and expanded by Automa's own compiler, not by the Julia compiler. \ +Search for this string in the Automa source code to learn more.""" + +""" + @escape() + +Pseudomacro. When encountered during `Machine` execution, the machine will stop executing. +This is useful to interrupt the parsing process, for example to emit a record during parsing +of a larger file. +`p` will be advanced as normally, so if `@escape` is hit on `B` during parsing of `"ABC"`, +the next byte will be `C`. +""" +macro escape() + :($WARNING_STRING) +end + +""" + @mark() + +Pseudomacro, to be used with IO-parsing Automa functions. +This macro will "mark" the position of `p` in the current buffer. +The marked position will not be flushed from the buffer after being consumed. +For example, Automa code can call `@mark()` at the beginning of a large string, +then when the string is exited at position `p`, +it is guaranteed that the whole string resides in the buffer at positions `markpos():p-1`. +""" +macro mark() + :($WARNING_STRING) +end + +""" + unmark() + +Removes the mark from the buffer. This allows all previous data to be cleared +from the buffer. + +See also: [`@mark`](@ref), [`@markpos`](@ref) +""" +macro unmark() + :($WARNING_STRING) +end + +""" + markpos() + +Get the position of the mark in the buffer. + +See also: [`@mark`](@ref), [`@markpos`](@ref) +""" +macro markpos() + :($WARNING_STRING) +end + +""" + setp() + +Updates `p` to match the current buffer position. +The buffer position is syncronized with `p` before and after calls to +functions generated by `generate_reader`. +`@setp()` can be used to update `p` after having called another parser. + +# Example +``` +# Inside some Automa action code +@setbuffer() +description = sub_parser(stream) +@setp() +``` + +See also: [`@setbuffer`](@ref) +""" +macro setp() + :($WARNING_STRING) +end + +""" + setbuffer() + +Updates the buffer position to match `p`. +The buffer position is syncronized with `p` before and after calls to +functions generated by `generate_reader`. +`@setbuffer()` can be used to the buffer before calling another parser. + +# Example +``` +# Inside some Automa action code +@setbuffer() +description = sub_parser(stream) +@setp() +``` + +See also: [`@setbuffer`](@ref) +""" +macro setbuffer() + :($WARNING_STRING) +end # If cs is nothing, we're in the table generator and do not need to set `cs`. Then we use break. # if it's an int, we set cs and escape using @goto @@ -640,7 +729,9 @@ function rewrite_special_macros(ctx::CodeGenContext, ex::Expr, eof::Bool, cs::Un elseif special_macro == Symbol("@markpos") quote $(ctx.vars.buffer).markpos end elseif special_macro == Symbol("@setp") - quote $(ctx.vars.p) = $(ctx.vars.buffer).markpos end + quote $(ctx.vars.p) = $(ctx.vars.buffer).bufferpos end + elseif special_macro == Symbol("@setbuffer") + quote $(ctx.vars.buffer).bufferpos = $(ctx.vars.p) end end push!(args, expr) end @@ -660,7 +751,8 @@ function special_macro_type(arg) sym == Symbol("@mark") || sym == Symbol("@unmark") || sym == Symbol("@markpos") || - sym == Symbol("@setp") + sym == Symbol("@setp") || + sym == Symbol("@setbuffer") if any(view(arg.args, 2:lastindex(arg.args))) do a !(a isa LineNumberNode) From 35ad6433f1256212f3f011e3bce494844b34d003 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Fri, 24 Feb 2023 14:49:10 +0100 Subject: [PATCH 32/64] Remove dead eps nodes The `re2nfa` function will create NFAs which include useless eps nodes. Here, I define a useless eps node as a node in the NFA with no outbound labels, actions or preconditions, that are neither the start or final state, and which has exactly one inbound edge. Such a node is trivially useless, and will only clutter up the displayed NFA. Here, I make `remove_dead_nodes(::NFA)` remove these useless eps nodes. --- src/nfa.jl | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/nfa.jl b/src/nfa.jl index 2dc7e83e..92356c34 100644 --- a/src/nfa.jl +++ b/src/nfa.jl @@ -235,5 +235,36 @@ function remove_dead_nodes(nfa::NFA) end end + # The following code will remove any nodes that + # have just a single eps edge with no actions or preconditions. + # TODO: Never create these nodes in the first place + function is_useless_eps(node::NFANode)::Bool + node === new(nfa.start) && return false + node === new(nfa.final) && return false + length(node.edges) == 1 || return false + edge = first(only(node.edges)) + iseps(edge) || return false + isempty(edge.actions.actions) || return false + isempty(edge.precond.names) || return false + return true + end + + unvisited = [new(nfa.start)] + visited = Set{NFANode}() + while !isempty(unvisited) + node = pop!(unvisited) + push!(visited, node) + for (i, (e, child)) in enumerate(node.edges) + original_child = child + while is_useless_eps(child) + child = last(only(child.edges)) + end + in(child, visited) || push!(unvisited, child) + if child !== original_child + node.edges[i] = (e, child) + end + end + end + return NFA(new(nfa.start), new(nfa.final)) end From a1f67fadb167bb2d9254110124c68fb0d21778bf Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Sat, 25 Feb 2023 12:59:37 +0100 Subject: [PATCH 33/64] Remove last Stream non-pseudomacros The existence of these as real macros does not actually work with arbitrary `CodeGenContext`s. Turn them into pseudomacros so that they actually work. Also, error if pseudomacro `@escape` occurs outside machine execution. --- src/Stream.jl | 37 +++++------ src/codegen.jl | 164 +++++++++++++++++++++++++++++++++++++---------- test/runtests.jl | 1 - 3 files changed, 144 insertions(+), 58 deletions(-) diff --git a/src/Stream.jl b/src/Stream.jl index 4e5aebc5..95814901 100644 --- a/src/Stream.jl +++ b/src/Stream.jl @@ -9,24 +9,6 @@ module Stream import Automa import TranscodingStreams: TranscodingStream, NoopStream -""" - @relpos(pos) - -Get the relative position of the absolute position `pos`. -""" -macro relpos(pos) - esc(:(@assert buffer.markpos > 0; $(pos) - buffer.markpos + 1)) -end - -""" - @abspos(pos) - -Get the absolute position of the relative position `pos`. -""" -macro abspos(pos) - esc(:(@assert buffer.markpos > 0; $(pos) + buffer.markpos - 1)) -end - """ generate_reader(funcname::Symbol, machine::Automa.Machine; kwargs...) @@ -72,12 +54,23 @@ function generate_reader( for arg in arguments push!(functioncode.args[1].args, arg) end + # Expands special Automa pseudomacros. When not inside the machine execution, + # at_eof and cs is meaningless, and when both are set to nothing, @escape + # will error at parse time + function rewrite(ex::Expr) + Automa.rewrite_special_macros(; + ctx=context, + ex=ex, + at_eof=nothing, + cs=nothing + ) + end vars = context.vars functioncode.args[2] = quote $(vars.buffer) = stream.state.buffer1 $(vars.data) = $(vars.buffer).data $(Automa.generate_init_code(context, machine)) - $(initcode) + $(rewrite(initcode)) # Overwrite is_eof for Stream, since we don't know the real EOF # until after we've actually seen the stream eof $(vars.is_eof) = false @@ -99,17 +92,17 @@ function generate_reader( # Advance the buffer, hence advancing the stream itself $(vars.buffer).bufferpos = $(vars.p) - $(loopcode) + $(rewrite(loopcode)) if $(vars.cs) < 0 - $(errorcode) + $(rewrite(errorcode)) end # If the machine errored, or we're past the end of the stream, actually return. # Else, keep looping. if $(vars.cs) == 0 || ($(vars.is_eof) && $(vars.p) > $(vars.p_end)) @label __return__ - $(returncode) + $(rewrite(returncode)) end @goto __exec__ end diff --git a/src/codegen.jl b/src/codegen.jl index 00770f82..525ef6ea 100644 --- a/src/codegen.jl +++ b/src/codegen.jl @@ -1,5 +1,4 @@ # Code Generator -# Code Generator # ============== """ @@ -323,7 +322,12 @@ function generate_action_dispatch_code(ctx::CodeGenContext, machine::Machine, ac # else if act == 2 (... etc) action_dispatch_code = foldr(default, action_ids) do names_id, els names, id = names_id - action_code = rewrite_special_macros(ctx, generate_action_code(names, actions), false, nothing) + action_code = rewrite_special_macros(; + ctx=ctx, + ex=generate_action_code(names, actions), + at_eof=false, + cs=nothing + ) return Expr(:if, :($(act) == $(id)), action_code, els) end # Action code is: Get the action int from the state and current input byte @@ -363,7 +367,12 @@ function generate_goto_code(ctx::CodeGenContext, machine::Machine, actions::Dict end append_code!(block, quote @label $(label) - $(rewrite_special_macros(ctx, generate_action_code(names, actions), false, s.state)) + $(rewrite_special_macros(; + ctx=ctx, + ex=generate_action_code(names, actions), + at_eof=false, + cs=s.state) + ) @goto $(Symbol("state_", s.state)) end) end @@ -434,7 +443,12 @@ function generate_goto_code(ctx::CodeGenContext, machine::Machine, actions::Dict # When EOF, go through a list of if/else statements: If cs == 1, do this, elseif # cs == 2 do that etc - eof_action_code = rewrite_special_macros(ctx, generate_eof_action_code(ctx, machine, actions), true, nothing) + eof_action_code = rewrite_special_macros(; + ctx=ctx, + ex=generate_eof_action_code(ctx, machine, actions), + at_eof=true, + cs=nothing + ) # Check the final state is an accept state, in an efficient manner final_state_code = generate_final_state_mem_code(ctx, machine) @@ -497,7 +511,12 @@ end function generate_eof_action_code(ctx::CodeGenContext, machine::Machine, actions::Dict{Symbol,Expr}) return foldr(:(), machine.eof_actions) do s_as, els s, as = s_as - action_code = rewrite_special_macros(ctx, generate_action_code(action_names(as), actions), true, nothing) + action_code = rewrite_special_macros(; + ctx=ctx, + ex=generate_action_code(action_names(as), actions), + at_eof=true, + cs=nothing + ) Expr(:if, state_condition(ctx, s), action_code, els) end end @@ -637,8 +656,8 @@ end """ unmark() -Removes the mark from the buffer. This allows all previous data to be cleared -from the buffer. +Pseudomacro. Removes the mark from the buffer. This allows all previous data to +be cleared from the buffer. See also: [`@mark`](@ref), [`@markpos`](@ref) """ @@ -649,7 +668,7 @@ end """ markpos() -Get the position of the mark in the buffer. +Pseudomacro. Get the position of the mark in the buffer. See also: [`@mark`](@ref), [`@markpos`](@ref) """ @@ -658,24 +677,55 @@ macro markpos() end """ - setp() + bufferpos() -Updates `p` to match the current buffer position. -The buffer position is syncronized with `p` before and after calls to -functions generated by `generate_reader`. -`@setp()` can be used to update `p` after having called another parser. +Pseudomacro. Returns the integer position of the current `TranscodingStreams` +buffer (only used with the `generate_reader` function). # Example ``` # Inside some Automa action code @setbuffer() description = sub_parser(stream) -@setp() +p = @bufferpos() ``` See also: [`@setbuffer`](@ref) """ -macro setp() +macro bufferpos() + :($WARNING_STRING) +end + +""" + relpos(p) + +Automa pseudomacro. Return the position of `p` relative to `@markpos()`. +Equivalent to `p - @markpos() + 1`. +This can be used to mark additional points in the stream when the mark is set, +after which their action position can be retrieved using `abspos(x)` + +# Example usage: +``` +# In one action +identifier_pos = @relpos(p) + +# Later, in a different action +identifier = data[@abspos(identifier_pos):p] +``` + +See also: [`abspos`](@ref) +""" +macro relpos(p) + :($WARNING_STRING) +end + +""" + abspos(p) + +Automa pseudomacro. Used to obtain the actual position of a relative position +obtained from `@relpos`. See `@relpos` for more details. +""" +macro abspos(p) :($WARNING_STRING) end @@ -692,7 +742,7 @@ functions generated by `generate_reader`. # Inside some Automa action code @setbuffer() description = sub_parser(stream) -@setp() +p = @bufferpos() ``` See also: [`@setbuffer`](@ref) @@ -701,20 +751,43 @@ macro setbuffer() :($WARNING_STRING) end -# If cs is nothing, we're in the table generator and do not need to set `cs`. Then we use break. -# if it's an int, we set cs and escape using @goto -function rewrite_special_macros(ctx::CodeGenContext, ex::Expr, eof::Bool, cs::Union{Int, Nothing}) +# The reason we have this function is because we don't want to expose too deep +# internals of TranscodingStream buffers to the final user, but the user needs to +# inject code that operates on buffers into Automa. So, we need to abstract it. +# Maybe they should have been ordinary functions... (TODO) +# @escape is an exception, this does require code generation that touches into +# the internals of machine execution +function rewrite_special_macros(; + ctx::CodeGenContext, + ex::Expr, + # nothing if code is expanded outside machine execution. + at_eof::Union{Bool, Nothing}, + # nothing if table generator, else Int + cs::Union{Int, Nothing} +) args = [] for arg in ex.args special_macro = special_macro_type(arg) if special_macro isa Symbol - if !eof - expr = if special_macro == Symbol("@escape") + expr = if special_macro == Symbol("@escape") + if at_eof === nothing + error( + "Special macro @escape can only be used in `actions`, ", + "and not in e.g. `initcode` or other such user-injected code" + ) + # If we're at eof, machine is already stopped. + elseif at_eof + quote nothing end + else + # If machine is a table generator, cs is already set, and the machine + # execution is a while loop we need to break if cs === nothing quote $(ctx.vars.p) += 1 break end + # If machine is a goto generator, cs is only set when we exit the + # machine, and we must exit it by @goto exit else quote $(ctx.vars.cs) = $(cs) @@ -722,21 +795,33 @@ function rewrite_special_macros(ctx::CodeGenContext, ex::Expr, eof::Bool, cs::Un @goto exit end end - elseif special_macro == Symbol("@mark") - quote $(ctx.vars.buffer).markpos = $(ctx.vars.p) end - elseif special_macro == Symbol("@unmark") - quote $(ctx.vars.buffer).markpos = 0 end - elseif special_macro == Symbol("@markpos") - quote $(ctx.vars.buffer).markpos end - elseif special_macro == Symbol("@setp") - quote $(ctx.vars.p) = $(ctx.vars.buffer).bufferpos end - elseif special_macro == Symbol("@setbuffer") - quote $(ctx.vars.buffer).bufferpos = $(ctx.vars.p) end end - push!(args, expr) + elseif special_macro == Symbol("@mark") + quote $(ctx.vars.buffer).markpos = $(ctx.vars.p) end + elseif special_macro == Symbol("@unmark") + quote $(ctx.vars.buffer).markpos = 0 end + elseif special_macro == Symbol("@markpos") + quote $(ctx.vars.buffer).markpos end + elseif special_macro == Symbol("@bufferpos") + quote $(ctx.vars.buffer).bufferpos end + elseif special_macro == Symbol("@setbuffer") + quote $(ctx.vars.buffer).bufferpos = $(ctx.vars.p) end + else + @assert false end + push!(args, expr) + elseif special_macro isa Tuple{Symbol, Any} + (sym, arg) = special_macro + expr = if sym == Symbol("@relpos") + quote @assert $(ctx.vars.buffer).markpos > 0; $(arg) - $(ctx.vars.buffer).markpos + 1 end + elseif sym == Symbol("@abspos") + quote @assert $(ctx.vars.buffer).markpos > 0; $(arg) + $(ctx.vars.buffer).markpos - 1 end + else + @assert false + end + push!(args, expr) elseif isa(arg, Expr) - push!(args, rewrite_special_macros(ctx, arg, eof, cs)) + push!(args, rewrite_special_macros(;ctx=ctx, ex=arg, at_eof=at_eof, cs=cs)) else push!(args, arg) end @@ -751,15 +836,24 @@ function special_macro_type(arg) sym == Symbol("@mark") || sym == Symbol("@unmark") || sym == Symbol("@markpos") || - sym == Symbol("@setp") || + sym == Symbol("@bufferpos") || sym == Symbol("@setbuffer") if any(view(arg.args, 2:lastindex(arg.args))) do a !(a isa LineNumberNode) end - error("Special Automa macro $(sym) used with arguments: Use it like this: `$(sym)()`") + error("Automa pseudomacro $(sym) used with arguments: Use it like this: `$(sym)()`") end return sym + elseif sym == Symbol("@relpos") || sym == Symbol("@abspos") + non_linenumber_args = filter!(a -> !(a isa LineNumberNode), arg.args[2:end]) + if length(non_linenumber_args) != 1 + error( + "Automa pseudomacro $(sym) should be used with exactly 1 argument, " * + "got $(non_linenumber_args)" + ) + end + return (sym, only(non_linenumber_args)) end return nothing end diff --git a/test/runtests.jl b/test/runtests.jl index a41718a9..33722bf5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -158,7 +158,6 @@ module TestStream import Automa import Automa.RegExp: @re_str -import Automa.Stream: @mark, @markpos, @relpos, @abspos using TranscodingStreams using Test From 56c63e1dd139a3e4b3b7e8bdf6fde6a715648f7c Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Sat, 25 Feb 2023 15:43:15 +0100 Subject: [PATCH 34/64] Remove Stream module This module doesn't make any sense in v1 for two reasons: First, we can not longer consider the module experimental when it's been the driving force for BioJulia parsers for five years at this point. And second, with a clearer separation of internals vs API in v1, users are not meant to access functions through internals like Automa.Stream.generate_reader, but should instead access it as an exported function --- src/Automa.jl | 4 ++-- src/{Stream.jl => stream.jl} | 13 ------------- test/runtests.jl | 14 +++++++------- 3 files changed, 9 insertions(+), 22 deletions(-) rename src/{Stream.jl => stream.jl} (96%) diff --git a/src/Automa.jl b/src/Automa.jl index 9fffdf11..f1fedb29 100644 --- a/src/Automa.jl +++ b/src/Automa.jl @@ -1,5 +1,6 @@ module Automa +using TranscodingStreams: TranscodingStream, NoopStream import ScanByte: ScanByte, ByteSet # Encode a byte set into a sequence of non-empty ranges. @@ -35,11 +36,10 @@ include("dot.jl") include("memory.jl") include("codegen.jl") include("tokenizer.jl") -include("Stream.jl") +include("stream.jl") const RE = Automa.RegExp using .RegExp: @re_str, opt, rep, rep1 -using .Stream: generate_reader, generate_io_validator # This list of exports lists the API export RE, diff --git a/src/Stream.jl b/src/stream.jl similarity index 96% rename from src/Stream.jl rename to src/stream.jl index 95814901..0a6fd2de 100644 --- a/src/Stream.jl +++ b/src/stream.jl @@ -1,14 +1,3 @@ -""" -Streaming Interface of Automa.jl. - -NOTE: This module is still experimental. The behavior may change without -deprecations. -""" -module Stream - -import Automa -import TranscodingStreams: TranscodingStream, NoopStream - """ generate_reader(funcname::Symbol, machine::Automa.Machine; kwargs...) @@ -205,5 +194,3 @@ function generate_io_validator( $(funcname)(io::$(IO)) = $(funcname)($(NoopStream)(io)) end end - -end # module diff --git a/test/runtests.jl b/test/runtests.jl index 33722bf5..2bb0c8d3 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,4 +1,4 @@ -import Automa +using Automa import Automa.RegExp: @re_str using Test @@ -156,7 +156,7 @@ end module TestStream -import Automa +using Automa import Automa.RegExp: @re_str using TranscodingStreams using Test @@ -167,7 +167,7 @@ machine = let newline = re"\r?\n" Automa.compile(line * newline) end -Automa.Stream.generate_reader(:readline, machine; errorcode=:(return cs)) |> eval +generate_reader(:readline, machine; errorcode=:(return cs)) |> eval @testset "Scanning a line" begin for (data, state) in [ ("\n", :ok), @@ -204,7 +204,7 @@ actions = Dict( ) initcode = :(start_alphanum = end_alphanum = 0) returncode = :(return cs == 0 ? String(data[@abspos(start_alphanum):@abspos(end_alphanum)]) : "") -Automa.Stream.generate_reader( +generate_reader( :stripwhitespace, machine, actions=actions, @@ -310,12 +310,12 @@ returncode = quote return ("", -1, 0), cs end end -Automa.Stream.generate_reader(:readrecord!, machine, arguments=(:(state::Int),), actions=actions, initcode=initcode, loopcode=loopcode, returncode=returncode) |> eval +generate_reader(:readrecord!, machine, arguments=(:(state::Int),), actions=actions, initcode=initcode, loopcode=loopcode, returncode=returncode) |> eval ctx = Automa.CodeGenContext( vars=Automa.Variables(:pointerindex, :p_ending, :p_fileend, :ts, :te, :current_state, :buffer, gensym(), gensym(), :buffer), generator=:goto, ) -Automa.Stream.generate_reader(:readrecord2!, machine, context=ctx, arguments=(:(state::Int),), actions=actions, initcode=initcode, loopcode=loopcode, returncode=returncode) |> eval +generate_reader(:readrecord2!, machine, context=ctx, arguments=(:(state::Int),), actions=actions, initcode=initcode, loopcode=loopcode, returncode=returncode) |> eval @testset "Three-column BED (stateful)" begin for reader in (readrecord!, readrecord2!) @@ -415,7 +415,7 @@ loopcode = quote found && @goto __return__ end context = Automa.CodeGenContext(generator=:goto) -Automa.Stream.generate_reader( +generate_reader( :readrecord!, machine, arguments=(:(state::Int), :(record::$(Record)),), From c9d57568e9cdad9c7910c3370a99171bfcaab0a0 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Mon, 27 Feb 2023 11:20:52 +0100 Subject: [PATCH 35/64] Simplify Machine struct layout Previously, `Machine`s contained the fields `start_state::Int` and `states::UnitRange{Int}`. However, since machines always contained the states `1:n_states`, and the start state was always 1, this information is needless. Simplify the layout so it now contains only a `n_states` field. --- docs/src/index.md | 5 ++--- src/codegen.jl | 8 ++++---- src/machine.jl | 11 ++++------- src/tokenizer.jl | 4 ++-- test/debug.jl | 6 ++---- test/runtests.jl | 12 ++++++------ 6 files changed, 20 insertions(+), 26 deletions(-) diff --git a/docs/src/index.md b/docs/src/index.md index e0440c06..9c5e0715 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -159,8 +159,7 @@ After finished defining a regular expression with optional actions you can compi ```julia mutable struct Machine start::Node - states::UnitRange{Int} - start_state::Int + n_states::Int final_states::Set{Int} eof_actions::Dict{Int,Set{Action}} end @@ -169,7 +168,7 @@ end For the purpose of debugging, Automa.jl offers the `execute` function, which emulates the machine execution and returns the last state with the action log. Let's execute a machine of `re"a*b"` with actions used in the previous example. ```jlcon julia> machine = Automa.compile(ab) -Automa.Machine() +Automa.Machine() julia> Automa.execute(machine, "b") (2,Symbol[:enter_a,:exit_a,:enter_b,:final_b,:exit_b]) diff --git a/src/codegen.jl b/src/codegen.jl index 525ef6ea..9e5b2472 100644 --- a/src/codegen.jl +++ b/src/codegen.jl @@ -175,7 +175,7 @@ function generate_init_code(ctx::CodeGenContext, machine::Machine) $(vars.p)::Int = 1 $(vars.p_end)::Int = sizeof($(vars.data)) $(vars.is_eof)::Bool = true - $(vars.cs)::Int = $(machine.start_state) + $(vars.cs)::Int = 1 end ctx.clean && Base.remove_linenums!(code) return code @@ -277,7 +277,7 @@ end # The table is a 256xnstates byte lookup table, such that table[input,cs] will give # the next state. function generate_transition_table(machine::Machine) - nstates = length(machine.states) + nstates = machine.n_states trans_table = Matrix{smallest_int(nstates)}(undef, 256, nstates) for j in 1:size(trans_table, 2) trans_table[:,j] .= -j @@ -298,7 +298,7 @@ end function generate_action_dispatch_code(ctx::CodeGenContext, machine::Machine, actions::Dict{Symbol,Expr}) nactions = length(actions) T = smallest_int(nactions) - action_table = fill(zero(T), (256, length(machine.states))) + action_table = fill(zero(T), (256, machine.n_states)) # Each edge with actions is a Vector{Symbol} with action names. # Enumerate them, by mapping the vector to an integer. # This way, each set of actions is mapped to an integer (call it: action int) @@ -437,7 +437,7 @@ function generate_goto_code(ctx::CodeGenContext, machine::Machine, actions::Dict # the starting state, then directly goto that state. # In cases where the starting state is hardcoded as a constant, (which is quite often!) # hopefully the Julia compiler will optimize this block away. - enter_code = foldr(:(@goto exit), machine.states) do s, els + enter_code = foldr(:(@goto exit), 1:machine.n_states) do s, els return Expr(:if, :($(ctx.vars.cs) == $(s)), :(@goto $(Symbol("state_case_", s))), els) end diff --git a/src/machine.jl b/src/machine.jl index 4821e90f..06b12402 100644 --- a/src/machine.jl +++ b/src/machine.jl @@ -35,8 +35,7 @@ to construct a DOT file, then visualise it using the `graphviz` software. """ struct Machine start::Node - states::UnitRange{Int} - start_state::Int + n_states::Int final_states::Set{Int} eof_actions::Dict{Int,ActionList} end @@ -65,8 +64,7 @@ end function Base.show(io::IO, machine::Machine) print(io, summary(machine), - "()" ) end @@ -103,8 +101,7 @@ function reorder_machine(machine::Machine) # Rebuild machine and return it Machine( new_nodes[1], - machine.states, - 1, + machine.n_states, Set([old2new[i] for i in machine.final_states]), Dict{Int, ActionList}(old2new[i] => act for (i, act) in machine.eof_actions) ) @@ -154,7 +151,7 @@ function dfa2machine(dfa::DFA) end start = new(dfa.start) @assert start.state == 1 - return Machine(start, 1:length(newnodes), 1, final_states, eof_actions) + return Machine(start, length(newnodes), final_states, eof_actions) end function execute(machine::Machine, data::Vector{UInt8}) diff --git a/src/tokenizer.jl b/src/tokenizer.jl index 6401f218..2620f840 100644 --- a/src/tokenizer.jl +++ b/src/tokenizer.jl @@ -53,7 +53,7 @@ function generate_init_code(ctx::CodeGenContext, tokenizer::Tokenizer) $(ctx.vars.is_eof)::Bool = true $(ctx.vars.ts)::Int = 0 $(ctx.vars.te)::Int = 0 - $(ctx.vars.cs)::Int = $(tokenizer.machine.start_state) + $(ctx.vars.cs)::Int = 1 end end @@ -114,7 +114,7 @@ function generate_table_code(ctx::CodeGenContext, tokenizer::Tokenizer, actions: $(token_exit_code) $(ctx.vars.p) = $(ctx.vars.te) + 1 if $(ctx.vars.cs) != 0 - $(ctx.vars.cs) = $(tokenizer.machine.start_state) + $(ctx.vars.cs) = 1 end end end diff --git a/test/debug.jl b/test/debug.jl index 13700b63..53fce211 100644 --- a/test/debug.jl +++ b/test/debug.jl @@ -1,6 +1,5 @@ function debug_machine(machine::Automa.Machine) - @assert machine.start_state == 1 - cloned_nodes = [Automa.Node(i) for i in machine.states] + cloned_nodes = [Automa.Node(i) for i in 1:machine.n_states] for node in Automa.traverse(machine.start) for (edge, child) in node.edges cloned_edge = Automa.Edge( @@ -17,8 +16,7 @@ function debug_machine(machine::Automa.Machine) end Automa.Machine( cloned_nodes[1], - 1:length(cloned_nodes), - 1, + length(cloned_nodes), copy(machine.final_states), copy(machine.eof_actions) ) diff --git a/test/runtests.jl b/test/runtests.jl index 2bb0c8d3..88670746 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -320,14 +320,14 @@ generate_reader(:readrecord2!, machine, context=ctx, arguments=(:(state::Int),), @testset "Three-column BED (stateful)" begin for reader in (readrecord!, readrecord2!) stream = NoopStream(IOBuffer("""chr1\t10\t200\n""")) - state = machine.start_state + state = 1 val, state = readrecord!(stream, state) @test val == ("chr1", 10, 200) val, state = readrecord!(stream, state) @test val == ("", -1, 0) @test state == 0 stream = NoopStream(IOBuffer("""1\t10\t200000\nchr12\t0\t21000\r\nchrM\t123\t12345\n""")) - state = machine.start_state + state = 1 val, state = readrecord!(stream, state) @test val == ("1", 10, 200000) val, state = readrecord!(stream, state) @@ -427,7 +427,7 @@ generate_reader( @testset "Streaming FASTA" begin stream = NoopStream(IOBuffer("")) - state = machine.start_state + state = 1 record = Record() @test readrecord!(stream, state, record) == 0 @@ -436,7 +436,7 @@ generate_reader( ACGT TGCA """), bufsize=10) - state = machine.start_state + state = 1 record = Record() @test readrecord!(stream, state, record) == 0 @test String(record.data[record.identifier]) == "seq1" @@ -450,7 +450,7 @@ generate_reader( -----AAA GGGGG--- """), bufsize=10) - state = machine.start_state + state = 1 record = Record() state = readrecord!(stream, state, record) @test state > 0 @@ -477,7 +477,7 @@ generate_reader( """), bufsize=10) - state = machine.start_state + state = 1 record = Record() state = readrecord!(stream, state, record) @test state > 0 From 9f09df91c6cb2a9d46d21ab83e508ae9c4e78966 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Mon, 27 Feb 2023 14:52:05 +0100 Subject: [PATCH 36/64] Fix warnings when running tests --- src/codegen.jl | 24 +++++++++++++++--------- test/test13.jl | 2 +- test/test18.jl | 3 +-- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/codegen.jl b/src/codegen.jl index 9e5b2472..216b73fa 100644 --- a/src/codegen.jl +++ b/src/codegen.jl @@ -105,17 +105,10 @@ The function returns `nothing` if `data` matches `Machine`, else the index of th invalid byte. If the machine reached unexpected EOF, returns `0`. If `goto`, the function uses the faster but more complicated `:goto` code. """ -function generate_validator_function(name::Symbol, regex::RegExp.RE, goto::Bool=false) +function generate_validator_function(name::Symbol, regex::RegExp.RE, goto::Bool=false; docstring::Bool=true) ctx = goto ? CodeGenContext(generator=:goto) : DefaultCodeGenContext machine = compile(RegExp.strip_actions(regex)) - return quote - """ - $($(name))(data)::Union{Int, Nothing} - - Checks if `data`, interpreted as a bytearray, conforms to the given `Automa.Machine`. - Returns `nothing` if it does, else the byte index of the first invalid byte. - If the machine reached unexpected EOF, returns `0`. - """ + code = quote function $(name)(data) $(generate_init_code(ctx, machine)) $(generate_exec_code(ctx, machine)) @@ -130,6 +123,19 @@ function generate_validator_function(name::Symbol, regex::RegExp.RE, goto::Bool= end end end + if docstring + code = quote + """ + $($(name))(data)::Union{Int, Nothing} + + Checks if `data`, interpreted as a bytearray, conforms to the given `Automa.Machine`. + Returns `nothing` if it does, else the byte index of the first invalid byte. + If the machine reached unexpected EOF, returns `0`. + """ + $code + end + end + code end """ diff --git a/test/test13.jl b/test/test13.jl index 1956b460..e545047a 100644 --- a/test/test13.jl +++ b/test/test13.jl @@ -11,7 +11,7 @@ using Test (!re"A[BC]D?E", ["ABCDE", "ABCE"], ["ABDE", "ACE", "ABE"]) ] for goto in (false, true) - @eval $(Automa.generate_validator_function(:validate, regex, goto)) + @eval $(Automa.generate_validator_function(:validate, regex, goto; docstring=false)) for string in good_strings @test validate(string) === nothing end diff --git a/test/test18.jl b/test/test18.jl index 7fea6d9e..271f0cbd 100644 --- a/test/test18.jl +++ b/test/test18.jl @@ -7,8 +7,7 @@ using Test @testset "Test18" begin regex = re"\0\a\b\t\n\v\r\x00\xff\xFF[\\][^\\]" for goto in (false, true) - println(goto) - @eval $(Automa.generate_validator_function(:validate, regex, goto)) + @eval $(Automa.generate_validator_function(:validate, regex, goto; docstring=false)) # Bad input types @test_throws Exception validate(18) From daf589cdeb6dc0fe4876c30e2d8be53ab3e0c0d6 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Mon, 27 Feb 2023 15:48:41 +0100 Subject: [PATCH 37/64] Disallow direct modification of actions field Instead of doing `re.actions[:exit] = [:foo]`, do `onexit(re, :foo)`. It's cleaner, and avoids messing with internal fields of struct. --- docs/actions.jl | 2 +- docs/src/index.md | 4 ++-- example/fasta.jl | 35 +++++++++++++++------------------ example/numbers.jl | 22 ++++++++++----------- example/tokenizer.jl | 14 ++++++-------- src/Automa.jl | 7 ++++++- src/nfa.jl | 15 +++++++-------- src/re.jl | 37 +++++++++++++++++++++++++---------- src/tokenizer.jl | 4 ++-- test/runtests.jl | 46 ++++++++++++++++++++++---------------------- test/test01.jl | 6 +++--- test/test02.jl | 21 ++++++++++---------- test/test03.jl | 2 +- test/test04.jl | 2 +- test/test05.jl | 6 +++--- test/test06.jl | 4 ++-- test/test07.jl | 2 +- test/test08.jl | 8 ++++---- test/test09.jl | 2 +- test/test10.jl | 2 +- test/test11.jl | 10 +++++----- test/test12.jl | 6 +++--- test/test14.jl | 2 +- test/test15.jl | 18 ++++++++--------- test/test16.jl | 2 +- test/test17.jl | 10 +++++----- test/test19.jl | 36 +++++++++++++++++----------------- test/unicode.jl | 2 +- test/validator.jl | 2 +- 29 files changed, 171 insertions(+), 158 deletions(-) diff --git a/docs/actions.jl b/docs/actions.jl index d4540644..7afd8f9a 100644 --- a/docs/actions.jl +++ b/docs/actions.jl @@ -9,7 +9,7 @@ pattern = re.cat(ab, c) ab.actions[:enter] = [:enter_ab] ab.actions[:exit] = [:exit_ab] ab.actions[:all] = [:all_ab] -ab.actions[:final] = [:final_ab] +onfinal!(ab, :final_ab) c.actions[:enter] = [:enter_c] c.actions[:exit] = [:exit_c] c.actions[:final] = [:final_c] diff --git a/docs/src/index.md b/docs/src/index.md index 9c5e0715..c201c42e 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -121,7 +121,7 @@ pattern = re.cat(ab, c) ab.actions[:enter] = [:enter_ab] ab.actions[:exit] = [:exit_ab] ab.actions[:all] = [:all_ab] -ab.actions[:final] = [:final_ab] +onfinal!(ab, :final_ab) c.actions[:enter] = [:enter_c] c.actions[:exit] = [:exit_c] c.actions[:final] = [:final_c] @@ -233,7 +233,7 @@ const re = Automa.RegExp word = re"[A-Za-z]+" words = re.cat(re.opt(word), re.rep(re" +" * word), re" *") -word.actions[:exit] = [:word] +onexit!(word, :word) machine = Automa.compile(words) diff --git a/example/fasta.jl b/example/fasta.jl index dd950bdf..4f596bb5 100644 --- a/example/fasta.jl +++ b/example/fasta.jl @@ -1,35 +1,32 @@ # A simple and practical FASTA file parser # ======================================== -import Automa -import Automa.RegExp: @re_str -const re = Automa.RegExp +using Automa # Create a machine of FASTA. -fasta_machine = (function () +fasta_machine = let # First, describe FASTA patterns in regular expression. lf = re"\n" newline = re"\r?" * lf identifier = re"[!-~]*" description = re"[!-~][ -~]*" letters = re"[A-Za-z*-]*" - sequence = re.cat(letters, re.rep(newline * letters)) - record = re.cat('>', identifier, re.opt(re" " * description), newline, sequence) - fasta = re.rep(record) + sequence = letters * rep(newline * letters) + record = '>' * identifier * opt(' ' * description) * newline * sequence + fasta = rep(record) # Second, bind action names to each regular expression. - lf.actions[:enter] = [:count_line] - identifier.actions[:enter] = [:mark] - identifier.actions[:exit] = [:identifier] - description.actions[:enter] = [:mark] - description.actions[:exit] = [:description] - letters.actions[:enter] = [:mark] - letters.actions[:exit] = [:letters] - record.actions[:exit] = [:record] + onenter!(identifier, :mark) + onexit!( identifier, :identifier) + onenter!(description, :mark) + onexit!( description, :description) + onenter!(letters, :mark) + onexit!( letters, :letters) + onenter!(record, :record) # Finally, compile the final FASTA pattern into a state machine. - return Automa.compile(fasta) -end)() + compile(fasta) +end # It is useful to visualize the state machine for debugging. # write("fasta.dot", Automa.machine2dot(fasta_machine)) @@ -52,7 +49,7 @@ mutable struct FASTARecord end # Generate a parser function from `fasta_machine` and `fasta_actions`. -context = Automa.CodeGenContext(generator=:goto) +context = CodeGenContext(generator=:goto) @eval function parse_fasta(data::Union{String,Vector{UInt8}}) # Initialize variables you use in the action code. records = FASTARecord[] @@ -62,7 +59,7 @@ context = Automa.CodeGenContext(generator=:goto) buffer = IOBuffer() # Generate code for initialization and main loop - $(Automa.generate_code(context, fasta_machine, fasta_actions)) + $(generate_code(context, fasta_machine, fasta_actions)) # Check the last state the machine reached. if cs != 0 diff --git a/example/numbers.jl b/example/numbers.jl index 070bdd36..50de070b 100644 --- a/example/numbers.jl +++ b/example/numbers.jl @@ -1,28 +1,26 @@ # A tokenizer of octal, decimal, hexadecimal and floating point numbers # ===================================================================== -import Automa -import Automa.RegExp: @re_str -const re = Automa.RegExp +using Automa # Describe patterns in regular expression. oct = re"0o[0-7]+" dec = re"[-+]?[0-9]+" hex = re"0x[0-9A-Fa-f]+" prefloat = re"[-+]?([0-9]+\.[0-9]*|[0-9]*\.[0-9]+)" -float = prefloat | re.cat(prefloat | re"[-+]?[0-9]+", re"[eE][-+]?[0-9]+") +float = prefloat | ((prefloat | re"[-+]?[0-9]+") * re"[eE][-+]?[0-9]+") number = oct | dec | hex | float -numbers = re.cat(re.opt(number), re.rep(re" +" * number), re" *") +numbers = opt(number) * rep(re" +" * number) * re" *" # Register action names to regular expressions. -number.actions[:enter] = [:mark] -oct.actions[:exit] = [:oct] -dec.actions[:exit] = [:dec] -hex.actions[:exit] = [:hex] -float.actions[:exit] = [:float] +onenter!(number, :mark) +onexit!(oct, :oct) +onexit!(dec, :dec) +onexit!(hex, :hex) +onexit!(float, :float) # Compile a finite-state machine. -machine = Automa.compile(numbers) +machine = compile(numbers) # This generates a SVG file to visualize the state machine. # write("numbers.dot", Automa.machine2dot(machine)) @@ -38,7 +36,7 @@ actions = Dict( ) # Generate a tokenizing function from the machine. -context = Automa.CodeGenContext() +context = CodeGenContext() @eval function tokenize(data::String) tokens = Tuple{Symbol,String}[] mark = 0 diff --git a/example/tokenizer.jl b/example/tokenizer.jl index 6ba6a238..418ab0b2 100644 --- a/example/tokenizer.jl +++ b/example/tokenizer.jl @@ -1,18 +1,16 @@ -import Automa -import Automa.RegExp: @re_str -const re = Automa.RegExp +using Automa keyword = re"break|const|continue|else|elseif|end|for|function|if|return|type|using|while" identifier = re"[A-Za-z_][0-9A-Za-z_!]*" operator = re"-|\+|\*|/|%|&|\||^|!|~|>|<|<<|>>|>=|<=|=>|==|===" macrocall = re"@" * re"[A-Za-z_][0-9A-Za-z_!]*" comment = re"#[^\r\n]*" -char = re.cat('\'', re"[ -&(-~]" | re.cat('\\', re"[ -~]"), '\'') -string = re.cat('"', re.rep(re"[ !#-~]" | re.cat("\\\"")), '"') -triplestring = re.cat("\"\"\"", (re"[ -~]*" \ re"\"\"\""), "\"\"\"") +char = '\'' * (re"[ -&(-~]" | ('\\' * re"[ -~]")) * '\'' +string = '"' * rep(re"[ !#-~]" | re"\\\\\"") * '"' +triplestring = "\"\"\"" * (re"[ -~]*" \ re"\"\"\"") * "\"\"\"" newline = re"\r?\n" -const minijulia = Automa.compile( +const minijulia = compile( re"," => :(emit(:comma)), re":" => :(emit(:colon)), re";" => :(emit(:semicolon)), @@ -47,7 +45,7 @@ write("minijulia.dot", Automa.machine2dot(minijulia.machine)) run(`dot -Tsvg -o minijulia.svg minijulia.dot`) =# -context = Automa.CodeGenContext() +context = CodeGenContext() @eval function tokenize(data) $(Automa.generate_init_code(context, minijulia)) tokens = Tuple{Symbol,String}[] diff --git a/src/Automa.jl b/src/Automa.jl index f1fedb29..637f0ef4 100644 --- a/src/Automa.jl +++ b/src/Automa.jl @@ -39,7 +39,7 @@ include("tokenizer.jl") include("stream.jl") const RE = Automa.RegExp -using .RegExp: @re_str, opt, rep, rep1 +using .RegExp: @re_str, opt, rep, rep1, onenter!, onexit!, onall!, onfinal!, precond! # This list of exports lists the API export RE, @@ -59,6 +59,11 @@ export RE, opt, rep, rep1, + onexit!, + onenter!, + onall!, + onfinal!, + precond!, # Debugging functionality machine2dot diff --git a/src/nfa.jl b/src/nfa.jl index 92356c34..6e1ba009 100644 --- a/src/nfa.jl +++ b/src/nfa.jl @@ -58,14 +58,13 @@ function re2nfa(re::RegExp.RE, predefined_actions::Dict{Symbol,Action}=Dict{Symb # Thompson's construction. function rec!(start, re) - # Validate keys - for (k, v) in re.actions - if k ∉ ACCEPTED_KEYS - error("Bad key in RE.actions: \"$k\". Accepted keys are: $(string(ACCEPTED_KEYS))") + if re.actions !== nothing + for k in keys(re.actions) + @assert k ∈ ACCEPTED_KEYS end end - if haskey(re.actions, :enter) + if !isnothing(re.actions) && haskey(re.actions, :enter) start_in = NFANode() push!(start.edges, (Edge(eps, make_action_list(re, re.actions[:enter])), start_in)) else @@ -132,7 +131,7 @@ function re2nfa(re::RegExp.RE, predefined_actions::Dict{Symbol,Action}=Dict{Symb error("unsupported operation: $(head)") end - if haskey(re.actions, :all) + if !isnothing(re.actions) && haskey(re.actions, :all) as = make_action_list(re, re.actions[:all]) for s in traverse(start), (e, _) in s.edges if !iseps(e) @@ -141,7 +140,7 @@ function re2nfa(re::RegExp.RE, predefined_actions::Dict{Symbol,Action}=Dict{Symb end end - if haskey(re.actions, :final) + if !isnothing(re.actions) && haskey(re.actions, :final) as = make_action_list(re, re.actions[:final]) for s in traverse(start), (e, t) in s.edges if !iseps(e) && final_in ∈ epsilon_closure(t) @@ -150,7 +149,7 @@ function re2nfa(re::RegExp.RE, predefined_actions::Dict{Symbol,Action}=Dict{Symb end end - if haskey(re.actions, :exit) + if !isnothing(re.actions) && haskey(re.actions, :exit) final = NFANode() push!(final_in.edges, (Edge(eps, make_action_list(re, re.actions[:exit])), final)) else diff --git a/src/re.jl b/src/re.jl index 0bda96d0..bbe7c52f 100644 --- a/src/re.jl +++ b/src/re.jl @@ -5,34 +5,47 @@ module RegExp import Automa: ByteSet -function gen_empty_names() - return Symbol[] -end - # Head: What kind of regex, like cat, or rep, or opt etc. # args: the content of the regex itself. Maybe should be type stable? # actions: Julia code to be executed when matching the regex. See Automa docs # when: a Precondition that is checked when every byte in the regex is matched. # See comments on Precondition struct + mutable struct RE head::Symbol args::Vector - actions::Dict{Symbol, Vector{Symbol}} + actions::Union{Nothing, Dict{Symbol, Vector{Symbol}}} when::Union{Symbol, Nothing} end function RE(head::Symbol, args::Vector) - return RE(head, args, Dict{Symbol, Vector{Symbol}}(), nothing) + return RE(head, args, nothing, nothing) end +function actions!(re::RE) + if isnothing(re.actions) + re.actions = Dict{Symbol, Vector{Symbol}}() + end + re.actions +end + +onenter!(re::RE, v::Vector{Symbol}) = (actions!(re)[:enter] = v; re) +onenter!(re::RE, s::Symbol) = onenter!(re, [s]) +onexit!(re::RE, v::Vector{Symbol}) = (actions!(re)[:exit] = v; re) +onexit!(re::RE, s::Symbol) = onexit!(re, [s]) +onfinal!(re::RE, v::Vector{Symbol}) = (actions!(re)[:final] = v; re) +onfinal!(re::RE, s::Symbol) = onfinal!(re, [s]) +onall!(re::RE, v::Vector{Symbol}) = (actions!(re)[:all] = v; re) +onall!(re::RE, s::Symbol) = onall!(re, [s]) + +precond!(re::RE, s::Symbol) = re.when = s + const Primitive = Union{RE, ByteSet, UInt8, UnitRange{UInt8}, Char, String, Vector{UInt8}} function primitive(re::RE) return re end -const PRIMITIVE = (:set, :byte, :range, :class, :cclass, :char, :str, :bytes) - function primitive(set::ByteSet) return RE(:set, [set]) end @@ -53,8 +66,8 @@ function primitive(str::String) return RE(:str, [str]) end -function primitive(bs::Vector{UInt8}) - return RE(:bytes, copy(bs)) +function primitive(bs::AbstractVector{UInt8}) + return RE(:bytes, collect(bs)) end function cat(xs::Primitive...) @@ -102,6 +115,8 @@ function space() end Base.:*(re1::RE, re2::RE) = cat(re1, re2) +Base.:*(x::Union{String, Char}, re::RE) = parse(string(x)) * re +Base.:*(re::RE, x::Union{String, Char}) = re * parse(string(x)) Base.:|(re1::RE, re2::RE) = alt(re1, re2) Base.:&(re1::RE, re2::RE) = isec(re1, re2) Base.:\(re1::RE, re2::RE) = diff(re1, re2) @@ -327,6 +342,8 @@ function unescape(str::String, s::Int) end end +# This converts from compound regex to foundational regex. +# For example, rep1(x) is equivalent to x * rep(x). function shallow_desugar(re::RE) head = re.head args = re.args diff --git a/src/tokenizer.jl b/src/tokenizer.jl index 2620f840..80f40a94 100644 --- a/src/tokenizer.jl +++ b/src/tokenizer.jl @@ -25,9 +25,9 @@ function compile(tokens::AbstractVector{Pair{RegExp.RE,Expr}}; optimize::Bool=tr actions_code = Tuple{Symbol,Expr}[] for (i, (re, code)) in enumerate(tokens) re′ = RegExp.shallow_desugar(re) - push!(get!(() -> Symbol[], re′.actions, :enter), :__token_start) + push!(get!(() -> Symbol[], RegExp.actions!(re′), :enter), :__token_start) name = Symbol(:__token, i) - push!(get!(() -> Symbol[], re′.actions, :final), name) + push!(get!(() -> Symbol[], RegExp.actions!(re′), :final), name) nfa = re2nfa(re′, actions) push!(start.edges, (Edge(eps), nfa.start)) push!(nfa.final.edges, (Edge(eps), final)) diff --git a/test/runtests.jl b/test/runtests.jl index 88670746..aad6da12 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -45,8 +45,8 @@ end @testset "DOT" begin re = re"[A-Za-z_][A-Za-z0-9_]*" - re.actions[:enter] = [:enter] - re.actions[:exit] = [:exit] + onenter!(re, :enter) + onexit!(re, :exit) nfa = Automa.re2nfa(re) @test startswith(Automa.nfa2dot(nfa), "digraph") dfa = Automa.nfa2dfa(nfa) @@ -193,8 +193,8 @@ end # Test 2 machine = let alphanum = re"[A-Za-z0-9]+" - alphanum.actions[:enter] = [:start_alphanum] - alphanum.actions[:exit] = [:end_alphanum] + onenter!(alphanum, :start_alphanum) + onexit!(alphanum, :end_alphanum) whitespace = re"[ \t\r\n\f]*" Automa.compile(whitespace * alphanum * whitespace) end @@ -231,12 +231,12 @@ end @testset "Incorrect action names" begin machine = let a = re"abc" - a.actions[:enter] = [:foo, :bar] + onenter!(a, [:foo, :bar]) b = re"bcd" - b.actions[:all] = [:qux] - b.when = :froom + onall!(b, :qux) + precond!(b, :froom) c = re"x*" - c.actions[:exit] = [] + onexit!(c, Symbol[]) Automa.compile(Automa.RegExp.cat(c, a | b)) end ctx = Automa.CodeGenContext(generator=:goto) @@ -263,13 +263,13 @@ end @testset "Invalid RE.actions keys" begin @test_throws Exception let a = re"abc" - a.actions[:badkey] = [:foo] + Automa.RegExp.actions!(a)[:badkey] = [:foo] Automa.compile(a) end @test let a = re"abc" - a.actions[:enter] = [:foo] + Automa.RegExp.actions!(a)[:enter] = [:foo] Automa.compile(a) end isa Any end @@ -279,13 +279,13 @@ cat = Automa.RegExp.cat rep = Automa.RegExp.rep machine = let chrom = re"[^\t]+" - chrom.actions[:exit] = [:chrom] + onexit!(chrom, :chrom) chromstart = re"[0-9]+" - chromstart.actions[:exit] = [:chromstart] + onexit!(chromstart, :chromstart) chromend = re"[0-9]+" - chromend.actions[:exit] = [:chromend] + onexit!(chromend, :chromend) record = cat(chrom, '\t', chromstart, '\t', chromend) - record.actions[:enter] = [:mark] + onenter!(record, :mark) bed = rep(cat(record, re"\r?\n")) Automa.compile(bed) end @@ -362,25 +362,25 @@ machine = let re = Automa.RegExp newline = re"\r?\n" identifier = re"[!-~]*" - identifier.actions[:enter] = [:pos] - identifier.actions[:exit] = [:identifier] + onenter!(identifier, :pos) + onexit!(identifier, :identifier) description = re"[!-~][ -~]*" - description.actions[:enter] = [:pos] - description.actions[:exit] = [:description] + onenter!(description, :pos) + onexit!(description, :description) header = re.cat('>', identifier, re.opt(re" " * description)) - header.actions[:exit] = [:header] + onexit!(header, :header) letters = re"[A-Za-z*-]*" - letters.actions[:enter] = [:mark, :pos] - letters.actions[:exit] = [:letters] + onenter!(letters, [:mark, :pos]) + onexit!(letters, :letters) sequence = re.cat(letters, re.rep(newline * letters)) record = re.cat(header, newline, sequence) - record.actions[:enter] = [:mark] - record.actions[:exit] = [:record] + onenter!(record, :mark) + onexit!(record, :record) fasta = re.rep(record) diff --git a/test/test01.jl b/test/test01.jl index 7884457b..3692d330 100644 --- a/test/test01.jl +++ b/test/test01.jl @@ -1,13 +1,13 @@ module Test01 -import Automa +using Automa import Automa.RegExp: @re_str using Test @testset "Test01" begin re = re"" - re.actions[:enter] = [:enter] - re.actions[:exit] = [:exit] + onenter!(re, :enter) + onexit!(re, :exit) machine = Automa.compile(re) @test occursin(r"^Automa.Machine\(<.*>\)$", repr(machine)) diff --git a/test/test02.jl b/test/test02.jl index 96bea71c..702d60eb 100644 --- a/test/test02.jl +++ b/test/test02.jl @@ -1,7 +1,6 @@ module Test02 -import Automa -import Automa.RegExp: @re_str +using Automa using Test @testset "Test02" begin @@ -11,15 +10,15 @@ using Test b = re.cat('b', re.rep('b')) ab = re.cat(a, b) - a.actions[:enter] = [:enter_a] - a.actions[:exit] = [:exit_a] - a.actions[:final] = [:final_a] - b.actions[:enter] = [:enter_b] - b.actions[:exit] = [:exit_b] - b.actions[:final] = [:final_b] - ab.actions[:enter] = [:enter_re] - ab.actions[:exit] = [:exit_re] - ab.actions[:final] = [:final_re] + onenter!(a, :enter_a) + onexit!(a, :exit_a) + onfinal!(a, :final_a) + onenter!(b, :enter_b) + onexit!(b, :exit_b) + onfinal!(b, :final_b) + onenter!(ab, :enter_re) + onexit!(ab, :exit_re) + onfinal!(ab, :final_re) machine = Automa.compile(ab) diff --git a/test/test03.jl b/test/test03.jl index a3a1accb..0dabf452 100644 --- a/test/test03.jl +++ b/test/test03.jl @@ -1,6 +1,6 @@ module Test03 -import Automa +using Automa import Automa.RegExp: @re_str using Test diff --git a/test/test04.jl b/test/test04.jl index 074d8e6d..5691da40 100644 --- a/test/test04.jl +++ b/test/test04.jl @@ -1,6 +1,6 @@ module Test04 -import Automa +using Automa import Automa.RegExp: @re_str using Test diff --git a/test/test05.jl b/test/test05.jl index 6884ab01..edd89081 100644 --- a/test/test05.jl +++ b/test/test05.jl @@ -1,6 +1,6 @@ module Test05 -import Automa +using Automa import Automa.RegExp: @re_str using Test @@ -11,8 +11,8 @@ using Test ident = re.diff(re"[a-z]+", keyword) token = re.alt(keyword, ident) - keyword.actions[:exit] = [:keyword] - ident.actions[:exit] = [:ident] + onexit!(keyword, :keyword) + onexit!(ident, :ident) machine = Automa.compile(token) diff --git a/test/test06.jl b/test/test06.jl index 76d1f699..395cee38 100644 --- a/test/test06.jl +++ b/test/test06.jl @@ -1,6 +1,6 @@ module Test06 -import Automa +using Automa import Automa.RegExp: @re_str using Test @@ -9,7 +9,7 @@ using Test foo = re.cat("foo") foos = re.rep(re.cat(foo, re" *")) - foo.actions[:exit] = [:foo] + onexit!(foo, :foo) actions = Dict(:foo => :(push!(ret, state.p:p-1); @escape)) machine = Automa.compile(foos) diff --git a/test/test07.jl b/test/test07.jl index 33af2335..71ff28b0 100644 --- a/test/test07.jl +++ b/test/test07.jl @@ -1,6 +1,6 @@ module Test07 -import Automa +using Automa import Automa.RegExp: @re_str using Test diff --git a/test/test08.jl b/test/test08.jl index 7de056be..baa4d30c 100644 --- a/test/test08.jl +++ b/test/test08.jl @@ -1,6 +1,6 @@ module Test08 -import Automa +using Automa import Automa.RegExp: @re_str using Test @@ -13,9 +13,9 @@ using Test spaces = re.rep(re.space()) numbers = re.cat(re.opt(spaces * number), re.rep(re.space() * spaces * number), spaces) - number.actions[:enter] = [:mark] - int.actions[:exit] = [:int] - float.actions[:exit] = [:float] + onenter!(number, :mark) + onexit!(int, :int) + onexit!(float, :float) machine = Automa.compile(numbers) diff --git a/test/test09.jl b/test/test09.jl index 2b349ac5..21468eac 100644 --- a/test/test09.jl +++ b/test/test09.jl @@ -1,6 +1,6 @@ module Test09 -import Automa +using Automa import Automa.RegExp: @re_str const re = Automa.RegExp using Test diff --git a/test/test10.jl b/test/test10.jl index b60cf254..f0e58c69 100644 --- a/test/test10.jl +++ b/test/test10.jl @@ -1,6 +1,6 @@ module Test10 -import Automa +using Automa import Automa.RegExp: @re_str const re = Automa.RegExp using Test diff --git a/test/test11.jl b/test/test11.jl index f6a33ce8..433e1ada 100644 --- a/test/test11.jl +++ b/test/test11.jl @@ -1,17 +1,17 @@ module Test11 -import Automa +using Automa import Automa.RegExp: @re_str const re = Automa.RegExp using Test @testset "Test11" begin a = re"[a-z]+" - a.when = :le - a = re.rep1(a) - a.actions[:exit] = [:one] + precond!(a, :le) + a = rep1(a) + onexit!(a, :one) b = re"[a-z]+[0-9]+" - b.actions[:exit] = [:two] + onexit!(b, :two) machine = Automa.compile(re.cat(a | b, '\n')) actions = Dict( diff --git a/test/test12.jl b/test/test12.jl index 6750dc06..ebe78303 100644 --- a/test/test12.jl +++ b/test/test12.jl @@ -1,14 +1,14 @@ module Test12 -import Automa +using Automa import Automa.RegExp: @re_str const re = Automa.RegExp using Test @testset "Test12" begin a = re"a*" - a.actions[:all] = [:a] - machine = Automa.compile(a) + onall!(a, :a) + machine = compile(a) ctx = Automa.CodeGenContext() @eval function validate(data) diff --git a/test/test14.jl b/test/test14.jl index 4523c2ef..75959732 100644 --- a/test/test14.jl +++ b/test/test14.jl @@ -1,6 +1,6 @@ module Test14 -import Automa +using Automa import Automa.RegExp: @re_str const re = Automa.RegExp using Test diff --git a/test/test15.jl b/test/test15.jl index f5beff47..b19a3903 100644 --- a/test/test15.jl +++ b/test/test15.jl @@ -1,20 +1,20 @@ module Test15 -import Automa +using Automa import Automa.RegExp: @re_str using Test @testset "Test15" begin a = re"a+" - a.actions[:enter] = [:enter] - a.actions[:all] = [:all] - a.actions[:final] = [:final] - a.actions[:exit] = [:exit] + onenter!(a, :enter) + onall!(a, :all) + onfinal!(a, :final) + onexit!(a, :exit) b = re"b+" - b.actions[:enter] = [:enter] - b.actions[:all] = [:all] - b.actions[:final] = [:final] - b.actions[:exit] = [:exit] + onenter!(b, :enter) + onall!(b, :all) + onfinal!(b, :final) + onexit!(b, :exit) ab = Automa.RegExp.cat(a, b) machine = Automa.compile(ab) diff --git a/test/test16.jl b/test/test16.jl index 5d8f956d..ddca4abe 100644 --- a/test/test16.jl +++ b/test/test16.jl @@ -1,6 +1,6 @@ module Test16 -import Automa +using Automa import Automa.RegExp: @re_str using Test diff --git a/test/test17.jl b/test/test17.jl index 0caabcd0..db37fb40 100644 --- a/test/test17.jl +++ b/test/test17.jl @@ -1,13 +1,13 @@ module Test17 -import Automa +using Automa import Automa.RegExp: @re_str using Test @testset "Test17" begin re1 = re"[a\-c]" - re1.actions[:enter] = [:enter] - re1.actions[:exit] = [:exit] + onenter!(re1, :enter) + onexit!(re1, :exit) machine1 = Automa.compile(re1) @test occursin(r"^Automa.Machine\(<.*>\)$", repr(machine1)) @@ -25,8 +25,8 @@ using Test end re2 = re"[a-c]" - re2.actions[:enter] = [:enter] - re2.actions[:exit] = [:exit] + onenter!(re2, :enter) + onexit!(re2, :exit) machine2 = Automa.compile(re2) @test occursin(r"^Automa.Machine\(<.*>\)$", repr(machine2)) diff --git a/test/test19.jl b/test/test19.jl index d658ddca..bd38d5c3 100644 --- a/test/test19.jl +++ b/test/test19.jl @@ -1,6 +1,6 @@ module Test19 -import Automa +using Automa import Automa.RegExp: @re_str const re = Automa.RegExp using Test @@ -9,42 +9,42 @@ using Test # Ambiguous enter statement A = re"XY" B = re"XZ" - A.actions[:enter] = [:enter_A] - @test_throws ErrorException Automa.compile(A | B) - @test Automa.compile(A | B, unambiguous=false) isa Automa.Machine + onenter!(A, :enter_A) + @test_throws ErrorException compile(A | B) + @test compile(A | B, unambiguous=false) isa Automa.Machine # Ambiguous, but no action in ambiguity A = re"XY" - A.actions[:exit] = [:exit_A] - @test Automa.compile(A | B) isa Automa.Machine + onexit!(A, :exit_A) + @test compile(A | B) isa Automa.Machine A = re"aa" B = re"a+" @test Automa.compile(A | B) isa Automa.Machine - A.actions[:enter] = [:enter_A] - @test_throws ErrorException Automa.compile(A | B) - @test Automa.compile(A | B, unambiguous=false) isa Automa.Machine + onenter!(A, :enter_A) + @test_throws ErrorException compile(A | B) + @test compile(A | B, unambiguous=false) isa Automa.Machine A = re"aa" - A.actions[:exit] = [:exit_A] - @test_throws ErrorException Automa.compile(A | B) - @test Automa.compile(A | B, unambiguous=false) isa Automa.Machine + onexit!(A, :exit_A) + @test_throws ErrorException compile(A | B) + @test compile(A | B, unambiguous=false) isa Automa.Machine # Harder test case A = re"AAAAB" B = re"A+C" - A.actions[:exit] = [:exit_A] - B.actions[:exit] = [:exit_B] - @test Automa.compile(A | B) isa Automa.Machine + onexit!(A, :exit_A) + onexit!(B, :exit_B) + @test compile(A | B) isa Automa.Machine # Test that conflicting edges can be known to be distinct # with different conditions. A = re"XY" - A.when = :cond + precond!(A, :cond) B = re"XZ" - A.actions[:enter] = [:enter_A] - @test Automa.compile(A | B, unambiguous=true) isa Automa.Machine + onenter!(A, :enter_A) + @test compile(A | B, unambiguous=true) isa Automa.Machine end end diff --git a/test/unicode.jl b/test/unicode.jl index 61021902..cc637b54 100644 --- a/test/unicode.jl +++ b/test/unicode.jl @@ -1,6 +1,6 @@ module Unicode -import Automa +using Automa import Automa.RegExp: @re_str const re = Automa.RegExp using Test diff --git a/test/validator.jl b/test/validator.jl index 609bdb03..3c451af1 100644 --- a/test/validator.jl +++ b/test/validator.jl @@ -1,6 +1,6 @@ module Validator -import Automa +using Automa import Automa.RegExp: @re_str using TranscodingStreams: NoopStream using Test From ce522bb1f2f356cf5321342a1464a982c163cce6 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Tue, 28 Feb 2023 11:32:18 +0100 Subject: [PATCH 38/64] Use `using` over `import` There is no reason to use `import` over `using`. On the contrary, it muddles the difference between exported and unexported names, and also allows extending foreign methods without warning. --- benchmark/runbenchmarks.jl | 3 +-- src/Automa.jl | 2 +- src/re.jl | 2 +- test/runtests.jl | 2 -- test/simd.jl | 10 ++++------ test/test01.jl | 1 - test/test03.jl | 1 - test/test04.jl | 1 - test/test05.jl | 1 - test/test06.jl | 1 - test/test07.jl | 1 - test/test08.jl | 1 - test/test09.jl | 2 -- test/test10.jl | 23 +++++++++++------------ test/test11.jl | 8 +++----- test/test12.jl | 5 +---- test/test14.jl | 8 +++----- test/test15.jl | 1 - test/test16.jl | 1 - test/test17.jl | 1 - test/test19.jl | 4 +--- test/unicode.jl | 6 ++---- test/validator.jl | 1 - 23 files changed, 28 insertions(+), 58 deletions(-) diff --git a/benchmark/runbenchmarks.jl b/benchmark/runbenchmarks.jl index 208f809c..f072e489 100644 --- a/benchmark/runbenchmarks.jl +++ b/benchmark/runbenchmarks.jl @@ -1,5 +1,4 @@ -import Automa -import Automa.RegExp: @re_str +using Automa using BenchmarkTools using Random: seed! diff --git a/src/Automa.jl b/src/Automa.jl index 637f0ef4..5ded2f27 100644 --- a/src/Automa.jl +++ b/src/Automa.jl @@ -1,7 +1,7 @@ module Automa using TranscodingStreams: TranscodingStream, NoopStream -import ScanByte: ScanByte, ByteSet +using ScanByte: ScanByte, ByteSet # Encode a byte set into a sequence of non-empty ranges. function range_encode(set::ScanByte.ByteSet) diff --git a/src/re.jl b/src/re.jl index bbe7c52f..9ac89d50 100644 --- a/src/re.jl +++ b/src/re.jl @@ -3,7 +3,7 @@ module RegExp -import Automa: ByteSet +using Automa: ByteSet # Head: What kind of regex, like cat, or rep, or opt etc. # args: the content of the regex itself. Maybe should be type stable? diff --git a/test/runtests.jl b/test/runtests.jl index aad6da12..aaa334ca 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,4 @@ using Automa -import Automa.RegExp: @re_str using Test @@ -157,7 +156,6 @@ end module TestStream using Automa -import Automa.RegExp: @re_str using TranscodingStreams using Test diff --git a/test/simd.jl b/test/simd.jl index 8eceb1dd..613be34c 100644 --- a/test/simd.jl +++ b/test/simd.jl @@ -4,21 +4,19 @@ @test_throws ArgumentError Automa.CodeGenContext(generator=:goto, getbyte=identity) end -import Automa -const re = Automa.RegExp -import Automa.RegExp: @re_str +using Automa @testset "SIMD generator" begin regex = let seq = re"[A-Z]+" name = re"[a-z]+" rec = re">" * name * re"\n" * seq - re.opt(rec) * re.rep(re"\n" * rec) + opt(rec) * rep(re"\n" * rec) end - context = Automa.CodeGenContext(generator=:goto) + context = CodeGenContext(generator=:goto) - eval(Automa.generate_validator_function(:is_valid_fasta, regex, true)) + eval(generate_validator_function(:is_valid_fasta, regex, true)) s1 = ">seq\nTAGGCTA\n>hello\nAJKGMP" s2 = ">seq1\nTAGGC" diff --git a/test/test01.jl b/test/test01.jl index 3692d330..be5efd2d 100644 --- a/test/test01.jl +++ b/test/test01.jl @@ -1,7 +1,6 @@ module Test01 using Automa -import Automa.RegExp: @re_str using Test @testset "Test01" begin diff --git a/test/test03.jl b/test/test03.jl index 0dabf452..97e78557 100644 --- a/test/test03.jl +++ b/test/test03.jl @@ -1,7 +1,6 @@ module Test03 using Automa -import Automa.RegExp: @re_str using Test @testset "Test03" begin diff --git a/test/test04.jl b/test/test04.jl index 5691da40..6671bec1 100644 --- a/test/test04.jl +++ b/test/test04.jl @@ -1,7 +1,6 @@ module Test04 using Automa -import Automa.RegExp: @re_str using Test @testset "Test04" begin diff --git a/test/test05.jl b/test/test05.jl index edd89081..100907d3 100644 --- a/test/test05.jl +++ b/test/test05.jl @@ -1,7 +1,6 @@ module Test05 using Automa -import Automa.RegExp: @re_str using Test @testset "Test05" begin diff --git a/test/test06.jl b/test/test06.jl index 395cee38..c74928ed 100644 --- a/test/test06.jl +++ b/test/test06.jl @@ -1,7 +1,6 @@ module Test06 using Automa -import Automa.RegExp: @re_str using Test @testset "Test06" begin diff --git a/test/test07.jl b/test/test07.jl index 71ff28b0..19433d69 100644 --- a/test/test07.jl +++ b/test/test07.jl @@ -1,7 +1,6 @@ module Test07 using Automa -import Automa.RegExp: @re_str using Test @testset "Test07" begin diff --git a/test/test08.jl b/test/test08.jl index baa4d30c..ae8171d5 100644 --- a/test/test08.jl +++ b/test/test08.jl @@ -1,7 +1,6 @@ module Test08 using Automa -import Automa.RegExp: @re_str using Test @testset "Test08" begin diff --git a/test/test09.jl b/test/test09.jl index 21468eac..3cbdb8ec 100644 --- a/test/test09.jl +++ b/test/test09.jl @@ -1,8 +1,6 @@ module Test09 using Automa -import Automa.RegExp: @re_str -const re = Automa.RegExp using Test @testset "Test09" begin diff --git a/test/test10.jl b/test/test10.jl index f0e58c69..51008318 100644 --- a/test/test10.jl +++ b/test/test10.jl @@ -1,55 +1,54 @@ module Test10 using Automa -import Automa.RegExp: @re_str const re = Automa.RegExp using Test @testset "Test10" begin - machine = Automa.compile(re.primitive(0x61)) + machine = compile(re.primitive(0x61)) @test Automa.execute(machine, "a")[1] == 0 @test Automa.execute(machine, "b")[1] < 0 - machine = Automa.compile(re.primitive(0x61:0x62)) + machine = compile(re.primitive(0x61:0x62)) @test Automa.execute(machine, "a")[1] == 0 @test Automa.execute(machine, "b")[1] == 0 @test Automa.execute(machine, "c")[1] < 0 - machine = Automa.compile(re.primitive('a')) + machine = compile(re.primitive('a')) @test Automa.execute(machine, "a")[1] == 0 @test Automa.execute(machine, "b")[1] < 0 - machine = Automa.compile(re.primitive('樹')) + machine = compile(re.primitive('樹')) @test Automa.execute(machine, "樹")[1] == 0 @test Automa.execute(machine, "儒")[1] < 0 - machine = Automa.compile(re.primitive("ジュリア")) + machine = compile(re.primitive("ジュリア")) @test Automa.execute(machine, "ジュリア")[1] == 0 @test Automa.execute(machine, "パイソン")[1] < 0 - machine = Automa.compile(re.primitive([0x61, 0x62, 0x72])) + machine = compile(re.primitive([0x61, 0x62, 0x72])) @test Automa.execute(machine, "abr")[1] == 0 @test Automa.execute(machine, "acr")[1] < 0 - machine = Automa.compile(re"[^A-Z]") + machine = compile(re"[^A-Z]") @test Automa.execute(machine, "1")[1] == 0 @test Automa.execute(machine, "A")[1] < 0 @test Automa.execute(machine, "a")[1] == 0 - machine = Automa.compile(re"[A-Z]+" & re"FOO?") + machine = compile(re"[A-Z]+" & re"FOO?") @test Automa.execute(machine, "FO")[1] == 0 @test Automa.execute(machine, "FOO")[1] == 0 @test Automa.execute(machine, "foo")[1] < 0 - machine = Automa.compile(re"[A-Z]+" \ re"foo") + machine = compile(re"[A-Z]+" \ re"foo") @test Automa.execute(machine, "FOO")[1] == 0 @test Automa.execute(machine, "foo")[1] < 0 - machine = Automa.compile(!re"foo") + machine = compile(!re"foo") @test Automa.execute(machine, "bar")[1] == 0 @test Automa.execute(machine, "foo")[1] < 0 - machine = Automa.compile(re.rep("a")) + machine = compile(re.rep("a")) @test Automa.execute(machine, "aaa")[1] == 0 @test Automa.execute(machine, "aYa")[1] < 0 end diff --git a/test/test11.jl b/test/test11.jl index 433e1ada..288f2e72 100644 --- a/test/test11.jl +++ b/test/test11.jl @@ -1,8 +1,6 @@ module Test11 using Automa -import Automa.RegExp: @re_str -const re = Automa.RegExp using Test @testset "Test11" begin @@ -13,17 +11,17 @@ using Test b = re"[a-z]+[0-9]+" onexit!(b, :two) - machine = Automa.compile(re.cat(a | b, '\n')) + machine = compile((a | b) * '\n') actions = Dict( :one => :(push!(logger, :one)), :two => :(push!(logger, :two)), :le => :(p ≤ n)) - ctx = Automa.CodeGenContext(generator=:table) + ctx = CodeGenContext(generator=:table) @test_throws ErrorException Automa.generate_exec_code(ctx, machine, actions) for clean in (true, false) - ctx = Automa.CodeGenContext(generator=:goto, clean=clean) + ctx = CodeGenContext(generator=:goto, clean=clean) validate = @eval function (data, n) logger = Symbol[] $(Automa.generate_init_code(ctx, machine)) diff --git a/test/test12.jl b/test/test12.jl index ebe78303..0b869fb3 100644 --- a/test/test12.jl +++ b/test/test12.jl @@ -1,8 +1,6 @@ module Test12 using Automa -import Automa.RegExp: @re_str -const re = Automa.RegExp using Test @testset "Test12" begin @@ -10,10 +8,9 @@ using Test onall!(a, :a) machine = compile(a) - ctx = Automa.CodeGenContext() @eval function validate(data) logger = Symbol[] - $(Automa.generate_code(ctx, machine, :debug)) + $(generate_code(CodeGenContext(), machine, :debug)) return logger, cs == 0 ? :ok : cs < 0 ? :error : :incomplete end @test validate(b"") == ([], :ok) diff --git a/test/test14.jl b/test/test14.jl index 75959732..4a3decb6 100644 --- a/test/test14.jl +++ b/test/test14.jl @@ -1,22 +1,20 @@ module Test14 using Automa -import Automa.RegExp: @re_str -const re = Automa.RegExp using Test @testset "Test14" begin a = re"a*" - machine = Automa.compile(a) + machine = compile(a) - ctx = Automa.CodeGenContext(generator=:table) + ctx = CodeGenContext(generator=:table) @eval function validate_table(data) $(Automa.generate_init_code(ctx, machine)) $(Automa.generate_exec_code(ctx, machine)) return p, cs end - ctx = Automa.CodeGenContext(generator=:goto) + ctx = CodeGenContext(generator=:goto) @eval function validate_goto(data) $(Automa.generate_init_code(ctx, machine)) $(Automa.generate_exec_code(ctx, machine)) diff --git a/test/test15.jl b/test/test15.jl index b19a3903..c78b8eb4 100644 --- a/test/test15.jl +++ b/test/test15.jl @@ -1,7 +1,6 @@ module Test15 using Automa -import Automa.RegExp: @re_str using Test @testset "Test15" begin diff --git a/test/test16.jl b/test/test16.jl index ddca4abe..80a3ba7a 100644 --- a/test/test16.jl +++ b/test/test16.jl @@ -1,7 +1,6 @@ module Test16 using Automa -import Automa.RegExp: @re_str using Test @testset "Test16" begin diff --git a/test/test17.jl b/test/test17.jl index db37fb40..585bff05 100644 --- a/test/test17.jl +++ b/test/test17.jl @@ -1,7 +1,6 @@ module Test17 using Automa -import Automa.RegExp: @re_str using Test @testset "Test17" begin diff --git a/test/test19.jl b/test/test19.jl index bd38d5c3..8a82841f 100644 --- a/test/test19.jl +++ b/test/test19.jl @@ -1,8 +1,6 @@ module Test19 using Automa -import Automa.RegExp: @re_str -const re = Automa.RegExp using Test @testset "Test19" begin @@ -20,7 +18,7 @@ using Test A = re"aa" B = re"a+" - @test Automa.compile(A | B) isa Automa.Machine + @test compile(A | B) isa Automa.Machine onenter!(A, :enter_A) @test_throws ErrorException compile(A | B) diff --git a/test/unicode.jl b/test/unicode.jl index cc637b54..8a0bd185 100644 --- a/test/unicode.jl +++ b/test/unicode.jl @@ -1,8 +1,6 @@ module Unicode using Automa -import Automa.RegExp: @re_str -const re = Automa.RegExp using Test @testset "Unicode" begin @@ -14,14 +12,14 @@ using Test @test_throws Exception re.parse("[ø]") # Unicode chars work like concatenated - machine = Automa.compile(re"øæ") + machine = compile(re"øæ") @test !passes(machine, "æ") @test !passes(machine, "ø") @test passes(machine, "øæ") @test passes(machine, collect(codeunits("øæ"))) # Byte ranges - machine = Automa.compile(re"[\x1a-\x29\xaa-\xf0]+") + machine = compile(re"[\x1a-\x29\xaa-\xf0]+") @test passes(machine, "\xd0\xe9") @test !passes(machine, "") @test passes(machine, "\x20\xaa\xf0") diff --git a/test/validator.jl b/test/validator.jl index 3c451af1..6206ecfe 100644 --- a/test/validator.jl +++ b/test/validator.jl @@ -1,7 +1,6 @@ module Validator using Automa -import Automa.RegExp: @re_str using TranscodingStreams: NoopStream using Test From d6b5699dc59c34fd0a6b6e991631f464d2b9cce5 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Tue, 28 Feb 2023 14:59:25 +0100 Subject: [PATCH 39/64] Update FASTA example --- example/fasta.jl | 65 ++++++++++++++++++++++++++++++------------------ test/runtests.jl | 4 +-- 2 files changed, 43 insertions(+), 26 deletions(-) diff --git a/example/fasta.jl b/example/fasta.jl index 4f596bb5..3996a0fa 100644 --- a/example/fasta.jl +++ b/example/fasta.jl @@ -6,14 +6,13 @@ using Automa # Create a machine of FASTA. fasta_machine = let # First, describe FASTA patterns in regular expression. - lf = re"\n" - newline = re"\r?" * lf + newline = re"\r?\n" identifier = re"[!-~]*" description = re"[!-~][ -~]*" - letters = re"[A-Za-z*-]*" + letters = re"[A-Za-z*-]+" sequence = letters * rep(newline * letters) record = '>' * identifier * opt(' ' * description) * newline * sequence - fasta = rep(record) + fasta = opt(record) * rep(newline * record) * rep(newline) # Second, bind action names to each regular expression. onenter!(identifier, :mark) @@ -34,45 +33,53 @@ end # Bind Julia code to each action name (see the `parse_fasta` function defined below). fasta_actions = Dict( - :count_line => :(linenum += 1), :mark => :(mark = p), - :identifier => :(identifier = mark == 0 ? "" : String(data[mark:p-1]); mark = 0), - :description => :(description = mark == 0 ? "" : String(data[mark:p-1]); mark = 0), - :letters => :(mark > 0 && unsafe_write(buffer, pointer(data, mark), p - mark); mark = 0), - :record => :(push!(records, FASTARecord(identifier, description, take!(buffer))))) + :identifier => :(identifier = String(data[mark:p-1]); mark = 0), + :description => :(description = iszero(mark) ? nothing : String(data[mark:p-1])), + :letters => quote + linelen = p - mark + length(buffer) < seqlen + linelen && resize!(buffer, seqlen + linelen) + GC.@preserve data buffer unsafe_copyto!(pointer(buffer) + seqlen, pointer(data, mark), linelen) + seqlen += linelen + end, + :record => quote + record_seen && push!(records, FASTARecord(identifier, description, String(buffer[1:seqlen]))) + seqlen = 0 + record_seen = true + end +) # Define a type to store a FASTA record. -mutable struct FASTARecord +struct FASTARecord identifier::String - description::String - sequence::Vector{UInt8} + description::Union{Nothing, String} + sequence::String end # Generate a parser function from `fasta_machine` and `fasta_actions`. context = CodeGenContext(generator=:goto) -@eval function parse_fasta(data::Union{String,Vector{UInt8}}) +@eval function parse_fasta(data::AbstractVector{UInt8}) # Initialize variables you use in the action code. records = FASTARecord[] mark = 0 - linenum = 1 - identifier = description = "" - buffer = IOBuffer() + seqlen = 0 + record_seen = false + identifier = "" + description = nothing + buffer = UInt8[] # Generate code for initialization and main loop $(generate_code(context, fasta_machine, fasta_actions)) - - # Check the last state the machine reached. - if cs != 0 - error("failed to parse on line ", linenum) - end + record_seen && push!(records, FASTARecord(identifier, description, String(buffer[1:seqlen]))) # Finally, return records accumulated in the action code. return records end +parse_fasta(s::Union{String, SubString{String}}) = parse_fasta(codeunits(s)) +parse_fasta(io::IO) = parse_fasta(read(io)) # Run the FASTA parser. -records = parse_fasta(""" ->NP_003172.1 brachyury protein isoform 1 [Homo sapiens] +data = """>NP_003172.1 brachyury protein isoform 1 [Homo sapiens] MSSPGTESAGKSLQYRVDHLLSAVENELQAGSEKGDPTERELRVGLEESELWLRFKELTNEMIVTKNGRR MFPVLKVNVSGLDPNAMYSFLLDFVAADNHRWKYVNGEWVPGGKPEPQAPSCVYIHPDSPNFGAHWMKAP VSFSKVKLTNKLNGGGQIMLNSLHKYEPRIHIVRVGGPQRMITSHCFPETQFIAVTAYQNEEITALKIKY @@ -80,4 +87,14 @@ NPFAKAFLDAKERSDHKEMMEEPGDSQQPGYSQWGWLLPGTSTLCPPANPHPQFGGALSLPSTHSCDRYP TLRSHRSSPYPSPYAHRNNSPTYSDNSPACLSMLQSHDNWSSLGMPAHPSMLPVSHNASPPTSSSQYPSL WSVSNGAVTPGSQAAAVSNGLGAQFFRGSPAHYTPLTHPVSAPSSSGSPLYEGAAAATDIVDSQYDAAAQ GRLIASWTPVSPPSM -""") +>sp|P01308|INS_HUMAN Insulin OS=Homo sapiens OX=9606 GN=INS PE=1 SV=1 +MALWMRLLPLLALLALWGPDPAAAFVNQHLCGSHLVEALYLVCGERGFFYTPKTRREAED +LQVGQVELGGGPGAGSLQPLALEGSLQKRGIVEQCCTSICSLYQLENYCN +""" +records = parse_fasta(data) +let + data2 = repeat(data, 10_000) + seconds = (@timed parse_fasta(data2)).time + MBs = (sizeof(data2) / 1e6) / seconds + println("Parsed FASTA at $(round(MBs; digits=2)) MB/s") +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index aaa334ca..a72f95e3 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -106,8 +106,8 @@ using Test include("../example/fasta.jl") @test records[1].identifier == "NP_003172.1" @test records[1].description == "brachyury protein isoform 1 [Homo sapiens]" - @test records[1].sequence[1:5] == b"MSSPG" - @test records[1].sequence[end-4:end] == b"SPPSM" + @test records[1].sequence[1:5] == "MSSPG" + @test records[1].sequence[end-4:end] == "SPPSM" end end From d4cb6ac78a41de0d5cf52fa9d2305711aad69ee0 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Wed, 1 Mar 2023 08:55:27 +0100 Subject: [PATCH 40/64] Disallow final actions in looping regex Currently, Automa allows you to add a :final action to a regex like "a*", where it is not possible to determine when reading a byte whether it is the final byte matching the regex. Current behaviour is to execute the action for every byte that could concievably be the final byte whether or not it is. Disallow this behaviour by throwing an error in re2nfa for :final actions in looping regex. --- src/nfa.jl | 11 +++++++++++ test/runtests.jl | 28 ++++++++++++++++++++++++++++ test/test02.jl | 32 +++++++++++++++----------------- test/test15.jl | 6 ++---- 4 files changed, 56 insertions(+), 21 deletions(-) diff --git a/src/nfa.jl b/src/nfa.jl index 6e1ba009..63fd4d25 100644 --- a/src/nfa.jl +++ b/src/nfa.jl @@ -144,6 +144,17 @@ function re2nfa(re::RegExp.RE, predefined_actions::Dict{Symbol,Action}=Dict{Symb as = make_action_list(re, re.actions[:final]) for s in traverse(start), (e, t) in s.edges if !iseps(e) && final_in ∈ epsilon_closure(t) + # Ugly hack: The tokenizer ATM relies on adding actions to the final edge + # of tokens. It's fine that they are repeated in that particular case. + # Therefore it can't error for token actions. + # It should probably be fixed in tokenizer.jl, by emitting the token on the + # :exit edge, and changing the codegen for tokenizer to compensate. + if any(action -> !startswith(String(action.name), "__token"), as) && any(i -> i === s, traverse(t)) + error( + "Regex has final action(s): [", join([repr(i.name) for i in as], ", "), + "], but regex is looping (e.g. `re\"a+\"`), so has no final input." + ) + end union!(e.actions, as) end end diff --git a/test/runtests.jl b/test/runtests.jl index a72f95e3..8682675c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -77,6 +77,34 @@ end end end +# Can't have final actions in looping regex +@testset "Looping regex" begin + nonlooping_regex = [ + re"abc", + re"a+b", + re"(a|b)*c", + re"a|b|c", + ] + + looping_regex = [ + re"a+" + re"abc*" + re"ab(c*)" + re"(a|b|c)+" + re"(abc)+" + ] + + for regex in nonlooping_regex + onfinal!(regex, :a) + @test Automa.re2nfa(regex) isa Automa.NFA + end + + for regex in looping_regex + onfinal!(regex, :a) + @test_throws Exception Automa.re2nfa(regex) + end +end + include("test01.jl") include("test02.jl") include("test03.jl") diff --git a/test/test02.jl b/test/test02.jl index 702d60eb..d3daab89 100644 --- a/test/test02.jl +++ b/test/test02.jl @@ -4,27 +4,24 @@ using Automa using Test @testset "Test02" begin - re = Automa.RegExp - - a = re.rep('a') - b = re.cat('b', re.rep('b')) - ab = re.cat(a, b) + a = rep('a') + b = 'b' * rep('b') + ab = a * b + abc = ab * 'c' onenter!(a, :enter_a) onexit!(a, :exit_a) - onfinal!(a, :final_a) onenter!(b, :enter_b) onexit!(b, :exit_b) - onfinal!(b, :final_b) - onenter!(ab, :enter_re) - onexit!(ab, :exit_re) - onfinal!(ab, :final_re) + onenter!(ab, :enter_ab) + onexit!(ab, :exit_ab) + onfinal!(abc, :final_abc) - machine = Automa.compile(ab) + machine = Automa.compile(ab | abc) - last, actions = Automa.execute(machine, "ab") + last, actions = Automa.execute(machine, "abc") @test last == 0 - @test actions == [:enter_re,:enter_a,:final_a,:exit_a,:enter_b,:final_b,:final_re,:exit_b,:exit_re] + @test actions == [:enter_ab,:enter_a,:exit_a,:enter_b,:exit_b,:exit_ab, :final_abc] for generator in (:table, :goto), clean in (true, false) ctx = Automa.CodeGenContext(generator=generator, clean=clean) @@ -34,10 +31,11 @@ using Test $(code) return cs == 0, logger end - @test validate(b"b") == (true, [:enter_re,:enter_a,:exit_a,:enter_b,:final_b,:final_re,:exit_b,:exit_re]) - @test validate(b"a") == (false, [:enter_re,:enter_a,:final_a]) - @test validate(b"ab") == (true, [:enter_re,:enter_a,:final_a,:exit_a,:enter_b,:final_b,:final_re,:exit_b,:exit_re]) - @test validate(b"abb") == (true, [:enter_re,:enter_a,:final_a,:exit_a,:enter_b,:final_b,:final_re,:final_b,:final_re,:exit_b,:exit_re]) + @test validate(b"b") == (true, [:enter_ab,:enter_a,:exit_a,:enter_b,:exit_b,:exit_ab]) + @test validate(b"a") == (false, [:enter_ab,:enter_a]) + @test validate(b"ab") == (true, [:enter_ab,:enter_a,:exit_a,:enter_b,:exit_b,:exit_ab]) + @test validate(b"abb") == (true, [:enter_ab,:enter_a,:exit_a,:enter_b,:exit_b,:exit_ab]) + @test validate(b"aabc") == (true, [:enter_ab, :enter_a, :exit_a, :enter_b, :exit_b, :exit_ab, :final_abc]) end end diff --git a/test/test15.jl b/test/test15.jl index c78b8eb4..d0ecdace 100644 --- a/test/test15.jl +++ b/test/test15.jl @@ -7,19 +7,17 @@ using Test a = re"a+" onenter!(a, :enter) onall!(a, :all) - onfinal!(a, :final) onexit!(a, :exit) b = re"b+" onenter!(b, :enter) onall!(b, :all) - onfinal!(b, :final) onexit!(b, :exit) ab = Automa.RegExp.cat(a, b) machine = Automa.compile(ab) last, actions = Automa.execute(machine, "ab") @test last == 0 - @test actions == [:enter, :all, :final, :exit, :enter, :all, :final, :exit] + @test actions == [:enter, :all, :exit, :enter, :all, :exit] for generator in (:table, :goto), clean in (true, false) ctx = Automa.CodeGenContext(generator=generator, clean=clean) @@ -28,7 +26,7 @@ using Test $(Automa.generate_code(ctx, machine, :debug)) return logger, cs == 0 ? :ok : cs < 0 ? :error : :incomplete end - @test validate(b"ab") == ([:enter, :all, :final, :exit, :enter, :all, :final, :exit], :ok) + @test validate(b"ab") == ([:enter, :all, :exit, :enter, :all, :exit], :ok) end end From afb2e4749a36c32b46fe3b42c5e497373d7b5dc6 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Wed, 1 Mar 2023 11:12:06 +0100 Subject: [PATCH 41/64] Make TranscodingStreams an optional dependency Many potential users of Automa are not interested in parsing from IOs, but only buffers. For those users, the IO-parsing functionality of Automa is not needed, and so there is no need for dependency on TranscodingStreams. --- Project.toml | 9 ++++++++- src/stream.jl => ext/AutomaStream.jl | 20 +++++++++++++------- src/Automa.jl | 9 +++++++-- 3 files changed, 28 insertions(+), 10 deletions(-) rename src/stream.jl => ext/AutomaStream.jl (94%) diff --git a/Project.toml b/Project.toml index dafd2a94..92478bb2 100644 --- a/Project.toml +++ b/Project.toml @@ -3,10 +3,16 @@ uuid = "67c07d97-cdcb-5c2c-af73-a7f9c32a568b" authors = ["Kenta Sato ", "Jakob Nybo Nissen quote line_num += 1 $(report_col ? :(@mark()) : :()) @@ -167,8 +172,7 @@ function generate_io_validator( else Dict{Symbol, Expr}() end - machine_names(machine) - function_code = generate_reader( + function_code = Automa.generate_reader( funcname, machine; context=ctx, @@ -194,3 +198,5 @@ function generate_io_validator( $(funcname)(io::$(IO)) = $(funcname)($(NoopStream)(io)) end end + +end # module diff --git a/src/Automa.jl b/src/Automa.jl index 5ded2f27..64f7a448 100644 --- a/src/Automa.jl +++ b/src/Automa.jl @@ -1,6 +1,5 @@ module Automa -using TranscodingStreams: TranscodingStream, NoopStream using ScanByte: ScanByte, ByteSet # Encode a byte set into a sequence of non-empty ranges. @@ -24,6 +23,9 @@ function range_encode(set::ScanByte.ByteSet) return result end +function generate_reader end +function generate_io_validator end + include("re.jl") include("precond.jl") include("action.jl") @@ -36,7 +38,10 @@ include("dot.jl") include("memory.jl") include("codegen.jl") include("tokenizer.jl") -include("stream.jl") + +if !isdefined(Base, :get_extension) + include("../ext/AutomaStream.jl") +end const RE = Automa.RegExp using .RegExp: @re_str, opt, rep, rep1, onenter!, onexit!, onall!, onfinal!, precond! From 6ad6a99bcb0c2abe15476b21a841abb2bc5d8608 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Wed, 1 Mar 2023 19:29:42 +0100 Subject: [PATCH 42/64] Error with shortest known ambiguity NFAs with ambiguities often contain multiple ambiguities. Displaying the simplest ambiguity when erroring makes debugging easier - especially compared to when the shown ambiguity can never happen due to another ambiguity. --- src/dfa.jl | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/dfa.jl b/src/dfa.jl index 70b78d55..8dd81112 100644 --- a/src/dfa.jl +++ b/src/dfa.jl @@ -186,7 +186,8 @@ end function validate_paths( paths::Vector{Tuple{Union{Nothing, Edge}, NFANode, Vector{Symbol}}}, dfanode::DFANode, - start::DFANode + start::DFANode, + strings_to::Dict{DFANode, String} ) # If they have the same actions, there is no ambiguity all(actions == paths[1][2] for (n, actions) in paths) && return nothing @@ -214,7 +215,7 @@ function validate_paths( # an informative error act1 = isempty(actions1) ? "nothing" : string(actions1) act2 = isempty(actions2) ? "nothing" : string(actions2) - input_until_now = repr(shortest_input(start)[dfanode]) + input_until_now = repr(strings_to[dfanode]) final_input = if eof "EOF" else @@ -232,7 +233,11 @@ function validate_nfanodes( newnodes::Dict{Set{NFANode}, DFANode}, start::DFANode ) - for (nfanodes, dfanode) in newnodes + # Sort all DFA nodes by how short a string can be used to reach it, in order + # to display the shortest possible conflict if any is found. + strings_to = shortest_input(start) + pairs = sort!(collect(newnodes); by=i -> ncodeunits(strings_to[i[2]])) + for (nfanodes, dfanode) in pairs # First get "tops", that's the starting epsilon nodes that cannot be # reached by another epsilon node. All paths lead from those tops = gettop(nfanodes) @@ -246,7 +251,7 @@ function validate_nfanodes( # If any two paths have different actions, and can be reached with the same # byte, the DFA's actions cannot be resolved, and we raise an error - validate_paths(paths, dfanode, start) + validate_paths(paths, dfanode, start, strings_to) end end From d37f8043e1782b48efc89930a97227869a427036 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Wed, 1 Mar 2023 19:48:36 +0100 Subject: [PATCH 43/64] Also check ambiguities for final and all actions An oversight in the ambiguity check meant that actions placed on non-epsilon edges were accidentally not included in the paths for validation. MWE: `compile(onfinal!(re"a", :a) | onfinal!(re"a", :b))` This breaks tokenizers, so we manually skip ambiguity check in tokenizers. In the case of conflicting actions in tokenizers, this will cause the longest matching token to be emitted. --- src/dfa.jl | 3 ++- src/tokenizer.jl | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/dfa.jl b/src/dfa.jl index 8dd81112..ffd5d55f 100644 --- a/src/dfa.jl +++ b/src/dfa.jl @@ -150,6 +150,7 @@ function get_epsilon_paths(tops::Set{NFANode}) push!(heads, (child, append!(copy(actions), [a.name for a in edge.actions]))) end else + append!(actions, [a.name for a in edge.actions]) push!(paths, (edge, node, actions)) end end @@ -190,7 +191,7 @@ function validate_paths( strings_to::Dict{DFANode, String} ) # If they have the same actions, there is no ambiguity - all(actions == paths[1][2] for (n, actions) in paths) && return nothing + all(actions == paths[1][3] for (e, n, actions) in paths) && return nothing for i in 1:length(paths) - 1 edge1, node1, actions1 = paths[i] for j in i+1:length(paths) diff --git a/src/tokenizer.jl b/src/tokenizer.jl index 80f40a94..5c3e2aa8 100644 --- a/src/tokenizer.jl +++ b/src/tokenizer.jl @@ -34,7 +34,7 @@ function compile(tokens::AbstractVector{Pair{RegExp.RE,Expr}}; optimize::Bool=tr push!(actions_code, (name, code)) end nfa = NFA(start, final) - dfa = nfa2dfa(remove_dead_nodes(nfa)) + dfa = nfa2dfa(remove_dead_nodes(nfa), false) if optimize dfa = remove_dead_nodes(reduce_nodes(dfa)) end From decd39f2109523d7066986894e9b7246eac7bec6 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Wed, 1 Mar 2023 15:04:57 +0100 Subject: [PATCH 44/64] Rewrite tokenizer The tokenizer has a completely new design and API. * It's now much easier to use * It's now lazy by default * It's much faster, although not completely optimised. Its API is amenable to further optimisation * It handles errors automatically See issue #116 --- example/fasta.jl | 14 +- example/tokenizer.jl | 107 ++++++------- src/Automa.jl | 3 + src/codegen.jl | 14 +- src/machine.jl | 6 +- src/tokenizer.jl | 350 +++++++++++++++++++++++++++++-------------- test/runtests.jl | 42 +++--- test/test09.jl | 49 +++--- test/tokenizer.jl | 36 +++++ 9 files changed, 385 insertions(+), 236 deletions(-) create mode 100644 test/tokenizer.jl diff --git a/example/fasta.jl b/example/fasta.jl index 3996a0fa..82b6736b 100644 --- a/example/fasta.jl +++ b/example/fasta.jl @@ -92,9 +92,11 @@ MALWMRLLPLLALLALWGPDPAAAFVNQHLCGSHLVEALYLVCGERGFFYTPKTRREAED LQVGQVELGGGPGAGSLQPLALEGSLQKRGIVEQCCTSICSLYQLENYCN """ records = parse_fasta(data) -let - data2 = repeat(data, 10_000) - seconds = (@timed parse_fasta(data2)).time - MBs = (sizeof(data2) / 1e6) / seconds - println("Parsed FASTA at $(round(MBs; digits=2)) MB/s") -end \ No newline at end of file + +# Uncomment to benchmark +# let +# data2 = repeat(data, 10_000) +# seconds = (@timed parse_fasta(data2)).time +# MBs = (sizeof(data2) / 1e6) / seconds +# println("Parsed FASTA at $(round(MBs; digits=2)) MB/s") +# end diff --git a/example/tokenizer.jl b/example/tokenizer.jl index 418ab0b2..dc677907 100644 --- a/example/tokenizer.jl +++ b/example/tokenizer.jl @@ -1,65 +1,50 @@ using Automa -keyword = re"break|const|continue|else|elseif|end|for|function|if|return|type|using|while" -identifier = re"[A-Za-z_][0-9A-Za-z_!]*" -operator = re"-|\+|\*|/|%|&|\||^|!|~|>|<|<<|>>|>=|<=|=>|==|===" -macrocall = re"@" * re"[A-Za-z_][0-9A-Za-z_!]*" -comment = re"#[^\r\n]*" -char = '\'' * (re"[ -&(-~]" | ('\\' * re"[ -~]")) * '\'' -string = '"' * rep(re"[ !#-~]" | re"\\\\\"") * '"' -triplestring = "\"\"\"" * (re"[ -~]*" \ re"\"\"\"") * "\"\"\"" -newline = re"\r?\n" +# Create an enum to store the tokens in. I define the enum in its own module +# in order to not clutter the Main namespace with all the variants. +module Tokens + using Automa + minijulia = [ + :identifier => re"[A-Za-z_][0-9A-Za-z_!]*", + :comma => re",", + :colon => re":", + :semicolon => re";", + :dot => re"\.", + :question => re"\?", + :equal => re"=", + :lparen => re"\(", + :rparen => re"\)", + :lbracket => re"\[", + :rbracket => re"]", + :lbrace => re"{", + :rbrace => re"}", + :dollar => re"$", + :and => re"&&", + :or => re"\|\|", + :typeannot => re"::", + :keyword => re"break|const|continue|else|elseif|end|for|function|if|return|type|using|while", + :operator => re"-|\+|\*|/|%|&|\||^|!|~|>|<|<<|>>|>=|<=|=>|==|===", + :macrocall => re"@" * re"[A-Za-z_][0-9A-Za-z_!]*", + :integer => re"[0-9]+", + :comment => re"#[^\r\n]*", + :char => '\'' * (re"[ -&(-~]" | ('\\' * re"[ -~]")) * '\'', + :string => '"' * rep(re"[ !#-~]" | re"\\\\\"") * '"', + :triplestring => "\"\"\"" * (re"[ -~]*" \ re"\"\"\"") * "\"\"\"", + :newline => re"\r?\n", + :spaces => re"[\t ]+", + ] + @eval @enum Token error $(first.(minijulia)...) + export Token +end -const minijulia = compile( - re"," => :(emit(:comma)), - re":" => :(emit(:colon)), - re";" => :(emit(:semicolon)), - re"\." => :(emit(:dot)), - re"\?" => :(emit(:question)), - re"=" => :(emit(:equal)), - re"\(" => :(emit(:lparen)), - re"\)" => :(emit(:rparen)), - re"\[" => :(emit(:lbracket)), - re"]" => :(emit(:rbracket)), - re"{" => :(emit(:lbrace)), - re"}" => :(emit(:rbrace)), - re"$" => :(emit(:dollar)), - re"&&" => :(emit(:and)), - re"\|\|" => :(emit(:or)), - re"::" => :(emit(:typeannot)), - keyword => :(emit(:keyword)), - identifier => :(emit(:identifier)), - operator => :(emit(:operator)), - macrocall => :(emit(:macrocall)), - re"[0-9]+" => :(emit(:integer)), - comment => :(emit(:comment)), - char => :(emit(:char)), - string => :(emit(:string)), - triplestring => :(emit(:triplestring)), - newline => :(emit(:newline)), - re"[\t ]+" => :(emit(:spaces)), -) +using .Tokens: Token -#= -write("minijulia.dot", Automa.machine2dot(minijulia.machine)) -run(`dot -Tsvg -o minijulia.svg minijulia.dot`) -=# +make_tokenizer(( + Tokens.error, + [Tokens.Token(i) => regex for (i, regex) in enumerate(last.(Tokens.minijulia))] +)) |> eval -context = CodeGenContext() -@eval function tokenize(data) - $(Automa.generate_init_code(context, minijulia)) - tokens = Tuple{Symbol,String}[] - emit(kind) = push!(tokens, (kind, data[ts:te])) - while p ≤ p_end && cs > 0 - $(Automa.generate_exec_code(context, minijulia)) - end - if cs < 0 - error("failed to tokenize") - end - return tokens -end - -tokens = tokenize(""" +code = """ quicksort(xs) = quicksort!(copy(xs)) quicksort!(xs) = quicksort!(xs, 1, length(xs)) @@ -86,4 +71,10 @@ function partition(xs, lo, hi) xs[j], xs[hi] = xs[hi], xs[j] return j end -""") +""" + +# For convenience, let's convert it to (string, token) tuples +# even though it's inefficient to store them as individual strings +tokens = map(tokenize(Token, code)) do (start, len, token) + (code[start:start+len-1], token) +end diff --git a/src/Automa.jl b/src/Automa.jl index 64f7a448..2f2b2bff 100644 --- a/src/Automa.jl +++ b/src/Automa.jl @@ -50,6 +50,8 @@ using .RegExp: @re_str, opt, rep, rep1, onenter!, onexit!, onall!, onfinal!, pre export RE, @re_str, CodeGenContext, + Tokenizer, + tokenize, compile, # user-facing generator functions @@ -59,6 +61,7 @@ export RE, generate_code, generate_reader, generate_io_validator, + make_tokenizer, # cat and alt is not exported in favor of * and | opt, diff --git a/src/codegen.jl b/src/codegen.jl index 216b73fa..8335a1e1 100644 --- a/src/codegen.jl +++ b/src/codegen.jl @@ -8,9 +8,7 @@ The following variable names may be used in the code. - `p::Int`: current position of data - `p_end::Int`: end position of data -- `is_eof::Bool`: `p_end` marks end of total file stream -- `ts::Int`: start position of token (tokenizer only) -- `te::Int`: end position of token (tokenizer only) +- `is_eof::Bool`: Whether `p_end` marks end file stream - `cs::Int`: current state - `data::Any`: input data - `mem::SizedMemory`: input data memory @@ -21,8 +19,6 @@ struct Variables p::Symbol p_end::Symbol is_eof::Symbol - ts::Symbol - te::Symbol cs::Symbol data::Symbol mem::Symbol @@ -34,15 +30,13 @@ function Variables( ;p=:p, p_end=:p_end, is_eof=:is_eof, - ts=:ts, - te=:te, cs=:cs, data=:data, mem=:mem, byte=:byte, buffer=:buffer ) - Variables(p, p_end, is_eof, ts, te, cs, data, mem, byte, buffer) + Variables(p, p_end, is_eof, cs, data, mem, byte, buffer) end struct CodeGenContext @@ -58,7 +52,7 @@ function generate_goto_code end """ CodeGenContext(; - vars=Variables(:p, :p_end, :is_eof, :ts, :te, :cs, :data, :mem, :byte), + vars=Variables(:p, :p_end, :is_eof, :cs, :data, :mem, :byte, :buffer), generator=:table, getbyte=Base.getindex, clean=false @@ -75,7 +69,7 @@ Arguments - `clean`: flag of code cleansing, e.g. removing line comments """ function CodeGenContext(; - vars::Variables=Variables(:p, :p_end, :is_eof, :ts, :te, :cs, :data, :mem, :byte, :buffer), + vars::Variables=Variables(:p, :p_end, :is_eof, :cs, :data, :mem, :byte, :buffer), generator::Symbol=:table, getbyte::Function=Base.getindex, clean::Bool=false) diff --git a/src/machine.jl b/src/machine.jl index 06b12402..dd161184 100644 --- a/src/machine.jl +++ b/src/machine.jl @@ -125,7 +125,11 @@ end ``` """ function compile(re::RegExp.RE; optimize::Bool=true, unambiguous::Bool=true) - dfa = nfa2dfa(remove_dead_nodes(re2nfa(re)), unambiguous) + nfa2machine(re2nfa(re); optimize=optimize, unambiguous=unambiguous) +end + +function nfa2machine(nfa::NFA; optimize::Bool=true, unambiguous::Bool=true) + dfa = nfa2dfa(remove_dead_nodes(nfa), unambiguous) if optimize dfa = remove_dead_nodes(reduce_nodes(dfa)) end diff --git a/src/tokenizer.jl b/src/tokenizer.jl index 5c3e2aa8..c5735a12 100644 --- a/src/tokenizer.jl +++ b/src/tokenizer.jl @@ -1,131 +1,259 @@ -# Tokenizer -# ========= +""" + Tokenizer{E, D, C} -struct Tokenizer - machine::Machine - actions_code::Vector{Tuple{Symbol,Expr}} -end +Lazy iterator of tokens of type `E` over data of type `D`. -# For backwards compatibility. This function needlessly specializes -# on the number of tokens. -# TODO: Deprecate this -function compile(tokens::Pair{RegExp.RE,Expr}...; optimize::Bool=true) - compile(collect(tokens), optimize=optimize) -end +`Tokenizer` works on any buffer-like object that defines `pointer` and `sizeof`. +When iterated, it will return a 3-tuple of integers: + * The first is the 1-based starting index of the token in the buffer + * The second is the length of the token in bytes + * The third is the token kind: The index in the input list `tokens`. -function compile(tokens::AbstractVector{Pair{RegExp.RE,Expr}}; optimize::Bool=true) - start = NFANode() - final = NFANode() - actions = Dict{Symbol,Action}() - for i in 1:lastindex(tokens) - # HACK: place token exit actions after any other actions - action = Action(Symbol(:__token, i), 10000 - i) - actions[action.name] = action - end - actions_code = Tuple{Symbol,Expr}[] - for (i, (re, code)) in enumerate(tokens) - re′ = RegExp.shallow_desugar(re) - push!(get!(() -> Symbol[], RegExp.actions!(re′), :enter), :__token_start) - name = Symbol(:__token, i) - push!(get!(() -> Symbol[], RegExp.actions!(re′), :final), name) - nfa = re2nfa(re′, actions) - push!(start.edges, (Edge(eps), nfa.start)) - push!(nfa.final.edges, (Edge(eps), final)) - push!(actions_code, (name, code)) - end - nfa = NFA(start, final) - dfa = nfa2dfa(remove_dead_nodes(nfa), false) - if optimize - dfa = remove_dead_nodes(reduce_nodes(dfa)) - end - return Tokenizer(dfa2machine(dfa), actions_code) -end +Un-tokenizable data will be emitted as the "error token" with index zero. + +The `Int` `C` parameter allows multiple tokenizers to be created with +the otherwise same type parameters. -function generate_init_code(tokenizer::Tokenizer) - # TODO: deprecate this? - return generate_init_code(CodeGenContext(), tokenizer) +See also: [`make_tokenizer`](@ref) +""" +struct Tokenizer{E, D, C} + data::D end -function generate_init_code(ctx::CodeGenContext, tokenizer::Tokenizer) - quote - $(ctx.vars.p)::Int = 1 - $(ctx.vars.p_end)::Int = sizeof($(ctx.vars.data)) - $(ctx.vars.is_eof)::Bool = true - $(ctx.vars.ts)::Int = 0 - $(ctx.vars.te)::Int = 0 - $(ctx.vars.cs)::Int = 1 - end +# By default, the counter C is 1 +Tokenizer{E}(data) where E = Tokenizer{E, typeof(data), 1}(data) +Base.IteratorSize(::Type{<:Tokenizer}) = Base.SizeUnknown() +Base.eltype(::Type{<:Tokenizer{E}}) where E = Tuple{Int, Int32, E} + +""" + tokenize(::Type{E}, data, version=1) + +Create a `Tokenizer{E, typeof(data), version}`, iterating tokens of type `E` +over `data`. + +See also: [`Tokenizer`](@ref), [`make_tokenizer`](@ref), [`compile`](@ref) +""" +tokenize(::Type{E}, data, version=1) where E = Tokenizer{E}(data) + +""" + TokenizerMachine + +Struct representing a `Machine` created for tokenization. +Machines used for tokenization contain distinct actions and are not to be used for notmal Automa codegen. +`TokenizerMachine`s contain two public fields: `machine::Machine` and `n_tokens::Int`. +""" +struct TokenizerMachine + machine::Machine + n_tokens::Int end -function generate_exec_code(ctx::CodeGenContext, tokenizer::Tokenizer, actions=nothing) - if actions === nothing - actions = Dict{Symbol,Expr}() - elseif actions == :debug - actions = debug_actions(tokenizer.machine) - elseif isa(actions, AbstractDict{Symbol,Expr}) - actions = copy(actions) +# Currently, actions are added to final byte. This usually inhibits SIMD, +# because the end position must be updated every byte. +# It would be faster to add actions to :exit, but then the action will not actually +# trigger when a regex is exited with an "invalid byte" - the beginning of a new regex. +# I'm not quite sure how to handle this. +""" + make_tokenizer( + machine::TokenizerMachine; + tokens::Tuple{E, AbstractVector{E}}= [ integers ], + goto=true, version=1 + ) where E + +Create code which when evaluated, defines `Base.iterate(::Tokenizer{E, D, \$version})`. +`tokens` is a tuple of a vector of non-error tokens of length `machine.n_tokens`, and the error token, +which will be emitted for data that cannot be tokenized. + +# Example usage +``` +julia> machine = compile([re"a", re"b"]); + +julia> make_tokenizer(machine; tokens=(0x00, [0x01, 0x02])) |> eval + +julia> iter = tokenize(UInt8, "abxxxba"); typeof(iter) +Tokenizer{UInt8, String, 1} + +julia> collect(iter) +5-element Vector{Tuple{Int64, Int32, UInt8}}: + (1, 1, 0x01) + (2, 1, 0x02) + (3, 3, 0x00) + (6, 1, 0x02) + (7, 1, 0x01) +``` + +Any actions inside the input regexes will be ignored. +If `goto` (default), use the faster, but more complex goto code generator. +The `version` number will set the last parameter of the `Tokenizer`, +which allows you to create different tokenizers for the same element type. + +See also: [`Tokenizer`](@ref), [`tokenize`](@ref), [`compile`](@ref) +""" +function make_tokenizer( + machine::TokenizerMachine; + tokens::Tuple{E, AbstractVector{E}}=(UInt32(1):UInt32(machine.n_tokens), UInt32(0)), + goto::Bool=true, + version::Int=1 +) where E + (error_token, nonerror_tokens) = tokens + # Check that tokens are unique + if length(nonerror_tokens) != machine.n_tokens + error("Tokenizer has $(machine.n_actions) actions, but only got $(length(nonerror_tokens)) tokens") + end + if !allunique(push!(Set(nonerror_tokens), error_token)) + error("Tokens and nonerror tokens must be unique") + end + ctx = if goto + Automa.CodeGenContext(generator=:goto) else - throw(ArgumentError("invalid actions argument")) + Automa.DefaultCodeGenContext end - actions[:__token_start] = :($(ctx.vars.ts) = $(ctx.vars.p)) - for (i, (name, _)) in enumerate(tokenizer.actions_code) - actions[name] = :(t = $(i); $(ctx.vars.te) = $(ctx.vars.p)) + vars = ctx.vars + # In these actions, store enter token and exit token. + actions = Dict{Symbol, Expr}() + for action_name in action_names(machine.machine) + # The action for every token's final byte is to say: "This is where the token ends, and this is + # what kind it is" + m = match(r"^__token_(\d+)$", String(action_name)) + m === nothing && error( + "Action $action_name found in Machine passed to `make_tokenizer`. ", + "`make_tokenizer` only supports machines generated by `compile(::Vector{RE})`." + ) + actions[action_name] = quote + stop = $(vars.p) + token = $(nonerror_tokens[parse(Int, only(m.captures))]) + end end - return generate_table_code(ctx, tokenizer, actions) -end - -function generate_table_code(ctx::CodeGenContext, tokenizer::Tokenizer, actions::AbstractDict{Symbol,Expr}) - action_dispatch_code, set_act_code = generate_action_dispatch_code(ctx, tokenizer.machine, actions) - trans_table = generate_transition_table(tokenizer.machine) - getbyte_code = generate_getbyte_code(ctx) - cs_code = :($(ctx.vars.cs) = $(trans_table)[($(ctx.vars.cs) - 1) << 8 + $(ctx.vars.byte) + 1]) - eof_action_code = generate_eof_action_code(ctx, tokenizer.machine, actions) - token_exit_code = generate_token_exit_code(tokenizer) return quote - $(ctx.vars.mem) = $(SizedMemory)($(ctx.vars.data)) - # Initialize token and token start to 0 - no token seen yet - t = 0 - $(ctx.vars.ts) = 0 - # In a loop: Get input byte, set action, update current state, execute action - while $(ctx.vars.p) ≤ p_end && $(ctx.vars.cs) > 0 - $(getbyte_code) - $(set_act_code) - $(cs_code) - $(action_dispatch_code) - $(ctx.vars.p) += 1 - end - if $(ctx.vars.is_eof) && $(ctx.vars.p) > $(ctx.vars.p_end) - # If EOF and in accept state, run EOF code and set current state to 0 - # meaning accept state - if $(generate_final_state_mem_code(ctx, tokenizer.machine)) - $(eof_action_code) - $(ctx.vars.cs) = 0 - # Else, if we're not already in a failed state (cs < 0), then set cs to failed state - elseif $(ctx.vars.cs) > 0 - $(ctx.vars.cs) = -$(ctx.vars.cs) + function Base.iterate(tokenizer::$(Tokenizer){$E, D, $version}, state=(1, Int32(1), $error_token)) where D + data = tokenizer.data + (start, len, token) = state + start > sizeof(data) && return nothing + if token !== $error_token + return (state, (start + len, Int32(0), $error_token)) end - end - # If in a failed state, reset p to where it failed, since it was - # incremented immediately after the state transition - if $(ctx.vars.cs) < 0 - $(ctx.vars.p) -= 1 - end - if t > 0 && ($(ctx.vars.cs) ≤ 0 || $(ctx.vars.p) > p_end ≥ 0) - $(token_exit_code) - $(ctx.vars.p) = $(ctx.vars.te) + 1 - if $(ctx.vars.cs) != 0 - $(ctx.vars.cs) = 1 + $(generate_init_code(ctx, machine.machine)) + token_start = start + stop = 0 + token = $error_token + while true + $(vars.p) = token_start + # Every time we try to find a token, we reset the machine's state to 1, i.e. we carry + # no memory between each token. + $(vars.cs) = 1 + $(generate_exec_code(ctx, machine.machine, actions)) + $(vars.cs) = 1 + + # There are only a few possibilities for why it stopped execution, we handle + # each of them here. + # If a token was found: + if token !== $error_token + found_token = (token_start, (stop-token_start+1)%Int32, token) + # If a token was found, but there are some error data, we emit the error data first, + # then set the state to be nonzero so the token is emitted next iteration + if start < token_start + error_state = (start, (token_start-start)%Int32, $error_token) + return (error_state, found_token) + # If no error data, simply emit the token with a zero state + else + return (found_token, (stop+1, Int32(0), $error_token)) + end + else + # If no token was found and EOF, emit an error token for the rest of the data + if $(vars.p) > $(vars.p_end) + error_state = (start, ($(vars.p) - start)%Int32, $error_token) + return (error_state, ($(vars.p_end)+1, Int32(0), $error_token)) + # If no token, and also not EOF, we continue, looking at next byte + else + token_start += 1 + end + end end end end end -function generate_token_exit_code(tokenizer::Tokenizer) - i = 0 - default = :() - return foldr(default, reverse(tokenizer.actions_code)) do name_code, els - _, code = name_code - i += 1 - Expr(:if, :(t == $(i)), code, els) +""" + make_tokenizer( + tokens::Union{ + AbstractVector{RE}, + Tuple{E, AbstractVector{Pair{E, RE}}} + }; + goto::Bool=true, + version::Int=1, + unambiguous=false + ) where E + +Convenience function for both compiling a tokenizer, then running `make_tokenizer` on it. +If `tokens` is an abstract vector, create an iterator of integer tokens with the error token being zero and the non-error tokens being the index in the vector. +Else, `tokens` is the error token followed by `token => regex` pairs. +See the relevant other methods of `make_tokenizer`, and `compile`. + +# Example +```julia +julia> make_tokenizer([re"abc", re"def") |> eval + +julia> collect(tokenize(Int, "abcxyzdef123")) +4-element Vector{Tuple{Int64, Int32, UInt32}}: + (1, 3, 0x00000001) + (4, 3, 0x00000003) + (7, 3, 0x00000002) + (10, 3, 0x00000003) +``` +""" +function make_tokenizer( + tokens::Union{ + <:AbstractVector{RegExp.RE}, + <:Tuple{E, AbstractVector{Pair{E, RegExp.RE}}} + }; + goto::Bool=true, + version::Int=1, + unambiguous=false +) where E + (regex, _tokens) = if tokens isa Vector + (tokens, (UInt32(0), UInt32(1):UInt32(length(tokens)))) + else + (map(last, last(tokens)), (first(tokens), map(first, last(tokens)))) + end + make_tokenizer( + compile(regex; unambiguous=unambiguous); + tokens=_tokens, + goto=goto, + version=version + ) +end + +""" + compile(tokens::Vector{RE}; unambiguous=false)::TokenizerMachine + +Compile the regex `tokens` to a tokenizer machine. +The machine can be passed to `make_tokenizer`. + +The keyword `unambiguous` decides which of multiple matching tokens is emitted: +If `false` (default), the longest token is emitted. If multiple tokens have the +same length, the one with the highest index is returned. +If `true`, `make_tokenizer` will error if any possible input text can be broken +ambiguously down into tokens. + +See also: [`Tokenizer`](@ref), [`make_tokenizer`](@ref), [`tokenize`](@ref) +""" +function compile( + tokens::Vector{RegExp.RE}; + unambiguous=false +)::TokenizerMachine + tokens = map(enumerate(tokens)) do (i, regex) + onfinal!(RegExp.strip_actions(regex), Symbol(:__token_, i)) + end + # We need the predefined actions here simply because it allows us to add priority to the actions. + # This is necessary to guarantee that tokens are disambiguated in the correct order. + predefined_actions = Dict{Symbol, Action}() + for i in eachindex(tokens) + predefined_actions[Symbol(:__token_, i)] = Action(Symbol(:__token_, i), 1000 + i) end + # We intentionally set unambiguous=true. With the current construction of + # this tokenizer, this will cause the longest token to be matched, i.e. for + # the regex "ab" and "a", the text "ab" will emit only the "ab" regex. + # Here, the NFA (i.e. the final regex we match) is a giant alternation statement between each of the tokens, + # i.e. input is token1 or token2 or .... + nfa = re2nfa(RegExp.RE(:alt, tokens), predefined_actions) + TokenizerMachine(nfa2machine(nfa; unambiguous=unambiguous), length(tokens)) end diff --git a/test/runtests.jl b/test/runtests.jl index 8682675c..18da6969 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -157,27 +157,27 @@ using Test @testset "MiniJulia" begin include("../example/tokenizer.jl") @test tokens[1:14] == [ - (:identifier,"quicksort"), - (:lparen,"("), - (:identifier,"xs"), - (:rparen,")"), - (:spaces," "), - (:equal,"="), - (:spaces," "), - (:identifier,"quicksort!"), - (:lparen,"("), - (:identifier,"copy"), - (:lparen,"("), - (:identifier,"xs"), - (:rparen,")"), - (:rparen,")")] + ("quicksort", Tokens.identifier), + ("(", Tokens.lparen), + ("xs", Tokens.identifier), + (")", Tokens.rparen), + (" ", Tokens.spaces), + ("=", Tokens.equal), + (" ", Tokens.spaces), + ("quicksort!", Tokens.identifier), + ("(", Tokens.lparen), + ("copy", Tokens.identifier), + ("(", Tokens.lparen), + ("xs", Tokens.identifier), + (")", Tokens.rparen), + (")", Tokens.rparen)] @test tokens[end-5:end] == [ - (:keyword,"return"), - (:spaces," "), - (:identifier,"j"), - (:newline,"\n"), - (:keyword,"end"), - (:newline,"\n")] + ("return", Tokens.keyword), + (" ", Tokens.spaces), + ("j", Tokens.identifier), + ("\n", Tokens.newline), + ("end", Tokens.keyword), + ("\n", Tokens.newline)] end end @@ -338,7 +338,7 @@ returncode = quote end generate_reader(:readrecord!, machine, arguments=(:(state::Int),), actions=actions, initcode=initcode, loopcode=loopcode, returncode=returncode) |> eval ctx = Automa.CodeGenContext( - vars=Automa.Variables(:pointerindex, :p_ending, :p_fileend, :ts, :te, :current_state, :buffer, gensym(), gensym(), :buffer), + vars=Automa.Variables(:pointerindex, :p_ending, :p_fileend, :current_state, :buffer, gensym(), gensym(), :ts_buffer), generator=:goto, ) generate_reader(:readrecord2!, machine, context=ctx, arguments=(:(state::Int),), actions=actions, initcode=initcode, loopcode=loopcode, returncode=returncode) |> eval diff --git a/test/test09.jl b/test/test09.jl index 3cbdb8ec..0dfedaf3 100644 --- a/test/test09.jl +++ b/test/test09.jl @@ -4,38 +4,29 @@ using Automa using Test @testset "Test09" begin - tokenizer = Automa.compile( - re"a" => :(emit(:a, ts:te)), - re"a*b" => :(emit(:ab, ts:te)), - re"cd" => :(emit(:cd, ts:te)), - ) - ctx = Automa.CodeGenContext() + eval(make_tokenizer([ + re"a", + re"a*b", + re"cd" + ])) - @eval function tokenize(data) - $(Automa.generate_init_code(ctx, tokenizer)) - tokens = Tuple{Symbol,String}[] - emit(kind, range) = push!(tokens, (kind, data[range])) - while p ≤ p_end && cs > 0 - $(Automa.generate_exec_code(ctx, tokenizer)) - end - if cs < 0 - error() - end - return tokens - end + tokenize(x) = collect(Automa.tokenize(UInt32, x)) @test tokenize("") == [] - @test tokenize("a") == [(:a, "a")] - @test tokenize("b") == [(:ab, "b")] - @test tokenize("aa") == [(:a, "a"), (:a, "a")] - @test tokenize("ab") == [(:ab, "ab")] - @test tokenize("aaa") == [(:a, "a"), (:a, "a"), (:a, "a")] - @test tokenize("aab") == [(:ab, "aab")] - @test tokenize("abaabba") == [(:ab, "ab"), (:ab, "aab"), (:ab, "b"), (:a, "a")] - @test_throws ErrorException tokenize("c") - @test_throws ErrorException tokenize("ac") - @test_throws ErrorException tokenize("abc") - @test_throws ErrorException tokenize("acb") + @test tokenize("a") == [(1, 1, 1)] + @test tokenize("b") == [(1, 1, 2)] + + @test tokenize("aa") == [(1,1,1), (2,1,1)] + @test tokenize("ab") == [(1,2,2)] + @test tokenize("aaa") == [(1,1,1), (2,1,1), (3,1,1)] + @test tokenize("aab") == [(1,3,2)] + @test tokenize("abaabba") == [(1,2,2), (3,3,2), (6,1,2), (7,1,1)] + + @test tokenize("c") == [(1, 1, 0)] + @test tokenize("ac") == [(1, 1, 1), (2, 1, 0)] + @test tokenize("abc") == [(1, 2, 2), (3, 1, 0)] + @test tokenize("acb") == [(1, 1, 1), (2, 1, 0), (3, 1, 2)] + @test tokenize("cdc") == [(1, 2, 3), (3, 1, 0)] end end diff --git a/test/tokenizer.jl b/test/tokenizer.jl new file mode 100644 index 00000000..5bdefc69 --- /dev/null +++ b/test/tokenizer.jl @@ -0,0 +1,36 @@ +# There are other tokenizer tests, e.g. test09 and in runtests +module TestTokenizer + +using Automa +using Test + +@testset "Tokenizer" begin + for goto in (false, true) + make_tokenizer(:token_iter, compile([ + re"ADF", + re"[A-Z]+", + re"[abcde]+", + re"abc", + re"ab[a-z]" + ]); goto=goto) |> eval + tokenize(x) = collect(token_iter(x)) + + # Empty + @test tokenize("") == [] + + # Only error + @test tokenize("!"^11) == [(1, 11, 0)] + + # Longest token wins + @test tokenize("abca") == [(1, 4, 3)] + @test tokenize("ADFADF") == [(1, 6, 2)] + @test tokenize("AD") == [(1, 2, 2)] + + # Ties are broken with last token + @test tokenize("ADF") == [(1, 3, 2)] + @test tokenize("abc") == [(1, 3, 5)] + @test tokenize("abe") == [(1, 3, 5)] + end +end + +end # module \ No newline at end of file From a2681b5020cba8a2e02d0dac9fece61b87eccf00 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Tue, 7 Mar 2023 08:45:34 +0100 Subject: [PATCH 45/64] Rename generate_validator_function --- src/Automa.jl | 2 +- src/codegen.jl | 4 ++-- test/simd.jl | 2 +- test/test13.jl | 2 +- test/test18.jl | 2 +- test/validator.jl | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Automa.jl b/src/Automa.jl index 2f2b2bff..6d9158ab 100644 --- a/src/Automa.jl +++ b/src/Automa.jl @@ -55,7 +55,7 @@ export RE, compile, # user-facing generator functions - generate_validator_function, + generate_buffer_validator, generate_init_code, generate_exec_code, generate_code, diff --git a/src/codegen.jl b/src/codegen.jl index 8335a1e1..169c90c8 100644 --- a/src/codegen.jl +++ b/src/codegen.jl @@ -91,7 +91,7 @@ end const DefaultCodeGenContext = CodeGenContext() """ - generate_validator_function(name::Symbol, regexp::RE, goto=false) + generate_buffer_validator(name::Symbol, regexp::RE, goto=false) Generate code that, when evaluated, defines a function named `name`, which takes a single argument `data`, interpreted as a sequence of bytes. @@ -99,7 +99,7 @@ The function returns `nothing` if `data` matches `Machine`, else the index of th invalid byte. If the machine reached unexpected EOF, returns `0`. If `goto`, the function uses the faster but more complicated `:goto` code. """ -function generate_validator_function(name::Symbol, regex::RegExp.RE, goto::Bool=false; docstring::Bool=true) +function generate_buffer_validator(name::Symbol, regex::RegExp.RE, goto::Bool=false; docstring::Bool=true) ctx = goto ? CodeGenContext(generator=:goto) : DefaultCodeGenContext machine = compile(RegExp.strip_actions(regex)) code = quote diff --git a/test/simd.jl b/test/simd.jl index 613be34c..b8958df0 100644 --- a/test/simd.jl +++ b/test/simd.jl @@ -16,7 +16,7 @@ using Automa context = CodeGenContext(generator=:goto) - eval(generate_validator_function(:is_valid_fasta, regex, true)) + eval(generate_buffer_validator(:is_valid_fasta, regex, true)) s1 = ">seq\nTAGGCTA\n>hello\nAJKGMP" s2 = ">seq1\nTAGGC" diff --git a/test/test13.jl b/test/test13.jl index e545047a..38b1c418 100644 --- a/test/test13.jl +++ b/test/test13.jl @@ -11,7 +11,7 @@ using Test (!re"A[BC]D?E", ["ABCDE", "ABCE"], ["ABDE", "ACE", "ABE"]) ] for goto in (false, true) - @eval $(Automa.generate_validator_function(:validate, regex, goto; docstring=false)) + @eval $(Automa.generate_buffer_validator(:validate, regex, goto; docstring=false)) for string in good_strings @test validate(string) === nothing end diff --git a/test/test18.jl b/test/test18.jl index 271f0cbd..3bc8076b 100644 --- a/test/test18.jl +++ b/test/test18.jl @@ -7,7 +7,7 @@ using Test @testset "Test18" begin regex = re"\0\a\b\t\n\v\r\x00\xff\xFF[\\][^\\]" for goto in (false, true) - @eval $(Automa.generate_validator_function(:validate, regex, goto; docstring=false)) + @eval $(Automa.generate_buffer_validator(:validate, regex, goto; docstring=false)) # Bad input types @test_throws Exception validate(18) diff --git a/test/validator.jl b/test/validator.jl index 6206ecfe..8b5f3930 100644 --- a/test/validator.jl +++ b/test/validator.jl @@ -6,8 +6,8 @@ using Test @testset "Validator" begin regex = re"a(bc)*|(def)|x+" | re"def" | re"x+" - eval(Automa.generate_validator_function(:foobar, regex, false)) - eval(Automa.generate_validator_function(:barfoo, regex, true)) + eval(Automa.generate_buffer_validator(:foobar, regex, false)) + eval(Automa.generate_buffer_validator(:barfoo, regex, true)) eval(Automa.generate_io_validator(:io_bar, regex; goto=false)) eval(Automa.generate_io_validator(:io_foo, regex; goto=true)) From 23f47e20c38af7913237f69e669b82231eee709b Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Tue, 7 Mar 2023 08:47:37 +0100 Subject: [PATCH 46/64] Export regex struct instead of module Users should not have access to the module directly. Instead, export the RE struct, and also allow users to construct regex with `RE(str)`. --- src/Automa.jl | 3 +-- src/re.jl | 5 ++++- test/runtests.jl | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Automa.jl b/src/Automa.jl index 6d9158ab..95b02334 100644 --- a/src/Automa.jl +++ b/src/Automa.jl @@ -43,8 +43,7 @@ if !isdefined(Base, :get_extension) include("../ext/AutomaStream.jl") end -const RE = Automa.RegExp -using .RegExp: @re_str, opt, rep, rep1, onenter!, onexit!, onall!, onfinal!, precond! +using .RegExp: RE, @re_str, opt, rep, rep1, onenter!, onexit!, onall!, onfinal!, precond! # This list of exports lists the API export RE, diff --git a/src/re.jl b/src/re.jl index 9ac89d50..7f267e2f 100644 --- a/src/re.jl +++ b/src/re.jl @@ -22,6 +22,8 @@ function RE(head::Symbol, args::Vector) return RE(head, args, nothing, nothing) end +RE(s::AbstractString) = parse(s) + function actions!(re::RE) if isnothing(re.actions) re.actions = Dict{Symbol, Vector{Symbol}}() @@ -129,7 +131,8 @@ end const METACHAR = raw".*+?()[]\|-^" # Parse a regular expression string using the shunting-yard algorithm. -function parse(str::String) +function parse(str_::AbstractString) + str = String(str_) # stacks operands = RE[] operators = Symbol[] diff --git a/test/runtests.jl b/test/runtests.jl index 18da6969..50ddacfd 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -286,7 +286,7 @@ end @test Automa.generate_exec_code(ctx, machine, :debug) isa Any end -@testset "Invalid RE.actions keys" begin +@testset "Invalid actions keys" begin @test_throws Exception let a = re"abc" Automa.RegExp.actions!(a)[:badkey] = [:foo] From fb98649013d5f43e805cca3f88d0f683c816d8d1 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Tue, 7 Mar 2023 16:15:02 +0100 Subject: [PATCH 47/64] Tweak: Allow | and & ops b/w chars/str and RE --- src/re.jl | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/re.jl b/src/re.jl index 7f267e2f..2383c84a 100644 --- a/src/re.jl +++ b/src/re.jl @@ -10,7 +10,33 @@ using Automa: ByteSet # actions: Julia code to be executed when matching the regex. See Automa docs # when: a Precondition that is checked when every byte in the regex is matched. # See comments on Precondition struct +""" + RE(s::AbstractString) +Automa regular expression (regex) that is used to match a sequence of input bytes. +Regex should preferentially be constructed using the `@re_str` macro: `re"ab+c?"`. +Regex can be combined with other regex, strings or chars with `*`, `|`, `&` and `\\`: +* `a * b` matches inputs that matches first `a`, then `b` +* `a | b` matches inputs that matches `a` or `b` +* `a & b` matches inputs that matches `a` and `b` +* `a \\ b` matches input that mathes `a` but not `b` +* `!a` matches all inputs that does not match `a`. + +# Example +```julia +julia> regex = (re"a*b?" | opt('c')) * re"[a-z]+"; + +julia> regex = rep1((regex \\ "aba") & !re"ca"); + +julia> regex isa RE +true + +julia> compile(regex) isa Automa.Machine +true +``` + +See also: `[@re_str](@ref)`, `[@compile](@ref)` +""" mutable struct RE head::Symbol args::Vector @@ -117,11 +143,15 @@ function space() end Base.:*(re1::RE, re2::RE) = cat(re1, re2) -Base.:*(x::Union{String, Char}, re::RE) = parse(string(x)) * re -Base.:*(re::RE, x::Union{String, Char}) = re * parse(string(x)) Base.:|(re1::RE, re2::RE) = alt(re1, re2) Base.:&(re1::RE, re2::RE) = isec(re1, re2) Base.:\(re1::RE, re2::RE) = diff(re1, re2) + +for f in (:*, :|, :&, :\) + @eval Base.$(f)(x::Union{AbstractString, AbstractChar}, re::RE) = $(f)(RE(string(x)), re) + @eval Base.$(f)(re::RE, x::Union{AbstractString, AbstractChar}) = $(f)(re, RE(string(x))) +end + Base.:!(re::RE) = neg(re) macro re_str(str::String) From 35d578b4d1eabaaca7c74f20a3cde3c82fb0aea6 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Wed, 8 Mar 2023 12:01:59 +0100 Subject: [PATCH 48/64] Remove report_col kwarg Instead of buffering an entire line, simply keep track of the number of columns cleared from the buffer. This reaches some more into TranscodingStreams privates, but it's well tested. --- ext/AutomaStream.jl | 78 +++++++++++++++++++++++++-------------------- test/validator.jl | 38 ++++++++++++++-------- 2 files changed, 69 insertions(+), 47 deletions(-) diff --git a/ext/AutomaStream.jl b/ext/AutomaStream.jl index 661f1ff4..fddefdea 100644 --- a/ext/AutomaStream.jl +++ b/ext/AutomaStream.jl @@ -104,26 +104,23 @@ function Automa.generate_reader( end """ - generate_io_validator(funcname::Symbol, regex::RE; goto::Bool=false, report_col::Bool=true) + generate_io_validator(funcname::Symbol, regex::RE; goto::Bool=false) Create code that, when evaluated, defines a function named `funcname`. This function takes an `IO`, and checks if the data in the input conforms to the regex, without executing any actions. If the input conforms, return `nothing`. -If `report_col` is set, return `(byte, (line, col))`, else return `(byte, line)`, -where `byte` is the first invalid byte, and `(line, col)` the 1-indexed position of that byte. +Else, return `(byte, (line, col))`, where `byte` is the first invalid byte, +and `(line, col)` the 1-indexed position of that byte. If the invalid byte is a `\n` byte, `col` is 0 and the line number is incremented. If the input errors due to unexpected EOF, `byte` is `nothing`, and the line and column given is the last byte in the file. -If `report_col` is set, the validator may buffer one line of the input. -If the input has very long lines that should not be buffered, set it to `false`. If `goto`, the function uses the faster but more complicated `:goto` code. """ function Automa.generate_io_validator( funcname::Symbol, regex::Automa.RegExp.RE; - goto::Bool=false, - report_col::Bool=true + goto::Bool=false ) ctx = if goto Automa.CodeGenContext(generator=:goto) @@ -131,42 +128,54 @@ function Automa.generate_io_validator( Automa.DefaultCodeGenContext end vars = ctx.vars - returncode = if report_col - quote - return if iszero(cs) + returncode = quote + return if iszero(cs) + nothing + else + # The column must be cleared cols. If we're EOF, all bytes have + # already been cleared by the buffer when attempting to get more bytes. + # If not, we add the bytes still in the buffer to the column + col = cleared_cols + col += ($(vars.p) - p_newline) * !$(vars.is_eof) + # Report position of last byte before EOF if EOF. + error_byte = if $(vars.p) > $(vars.p_end) nothing else - col = $(vars.p) - $(vars.buffer).markpos - # Report position of last byte before EOF - error_byte = if $(vars.p) > $(vars.p_end) - col -= 1 - nothing - else - col -= $(vars.byte) == UInt8('\n') - $(vars.byte) - end - (error_byte, (line_num, col)) + $(vars.byte) end + # If errant byte was a newline, instead of counting it as last + # byte on a line (which would be inconsistent), count it as first + # byte on a new line + line_num += error_byte == UInt8('\n') + col -= error_byte == UInt8('\n') + (error_byte, (line_num, col)) end - else - quote - return if iszero(cs) - nothing - else - error_byte = if $(vars.p) > $(vars.p_end) - nothing - else - $(vars.byte) - end - (error_byte, line_num) - end + end + initcode = quote + # Unmark buffer in case it's been marked before hand + @unmark() + line_num = 1 + # Keep track of how many columns since newline that has + # been cleared from the buffer + cleared_cols = 0 + # p value of last newline _in the current buffer_. + p_newline = 0 + end + loopcode = quote + # If we're about to clear the buffer (ran out of buffer, did not error), + # then update cleared_cols, and since the buffer is about to be cleared, + # remove p_newline + if $(vars.cs) > 0 && $(vars.p) > $(vars.p_end) && !$(vars.is_eof) + cleared_cols += $(vars.p) - p_newline - 1 + p_newline = 0 end end machine = Automa.compile(Automa.RegExp.set_newline_actions(regex)) actions = if :newline ∈ Automa.machine_names(machine) Dict{Symbol, Expr}(:newline => quote line_num += 1 - $(report_col ? :(@mark()) : :()) + cleared_cols = 0 + p_newline = $(vars.p) end ) else @@ -176,7 +185,8 @@ function Automa.generate_io_validator( funcname, machine; context=ctx, - initcode=:(line_num = 1; @unmark()), + initcode=initcode, + loopcode=loopcode, actions=actions, returncode=returncode, errorcode=:(@goto __return__), diff --git a/test/validator.jl b/test/validator.jl index 8b5f3930..3fd0edf0 100644 --- a/test/validator.jl +++ b/test/validator.jl @@ -76,21 +76,33 @@ end end end -@testset "Report column or not" begin - regex = re"[a-z]+" - eval(Automa.generate_io_validator(:io_foo_3, regex; goto=false, report_col=true)) - eval(Automa.generate_io_validator(:io_bar_3, regex; goto=false, report_col=false)) - - let data = "abc;" - @test io_foo_3(IOBuffer(data)) == (UInt8(';'), (1, 4)) - @test io_bar_3(IOBuffer(data)) == (UInt8(';'), 1) +@testset "Reported column" begin + regex = re"([a-z][a-z]+)(\n[a-z][a-z]+)*" + eval(Automa.generate_io_validator(:io_foo_3, regex; goto=false)) + + function test_reported_pos(data) + # Test with a small buffer size + io = NoopStream(IOBuffer(data); bufsize=8) + y = io_foo_3(io) + y === nothing ? nothing : last(y) end - # Test that, if `report_col` is not set, very long lines are not - # buffered (because the mark is not set). - let data = repeat("abcd", 100_000) * ';' - io = NoopStream(IOBuffer(data)) - @test length(io.state.buffer1.data) < 100_000 + for (data, result) in [ + ("abcd", nothing), + ('a'^10 * '!', (1, 11)), + ('a'^10 * "\nabc!", (2, 4)), + ("abcdef\n\n", (3, 0)), + ('a'^8 * '!', (1, 9)), + ('a'^8 * "\n" * 'a'^20 * '!', (2, 21)), + ('a'^7 * '!', (1, 8)), + ('a'^8, nothing), + ("abc!", (1, 4)), + ("", (1, 0)), + ("a", (1, 1)), + ("ab\na", (2, 1)), + ("ab\naa\n\n", (4, 0)) + ] + @test test_reported_pos(data) == result end end From 2d16183cf032ff6531800a3b583888c1819f269b Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Wed, 8 Mar 2023 16:25:26 +0100 Subject: [PATCH 49/64] Add SnoopPrecompile --- Project.toml | 12 ++++-------- src/Automa.jl | 2 ++ src/tokenizer.jl | 4 ++-- src/workload.jl | 50 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 10 deletions(-) create mode 100644 src/workload.jl diff --git a/Project.toml b/Project.toml index 92478bb2..cd4bc89b 100644 --- a/Project.toml +++ b/Project.toml @@ -1,22 +1,18 @@ name = "Automa" uuid = "67c07d97-cdcb-5c2c-af73-a7f9c32a568b" authors = ["Kenta Sato ", "Jakob Nybo Nissen :(pos = p), + :name => :(push!(headers, String(data[pos:p-1]))), + :field => quote + n_fields += 1 + push!(fields, String(data[pos:p-1])) + end, + :record => quote + n_fields == length(headers) || error("Malformed TSV") + n_fields = 0 + end + ) + + generate_code(goto_ctx, machine, actions) + generate_code(table_ctx, machine, actions) + + # Create a tokenizer + tokens = [ + re"[A-Za-z_][0-9A-Za-z_!]*!", + re"\(", + re"\)", + re",", + re"abc", + re"\"", + re"[\t\f ]+", + ]; + make_tokenizer(tokens; goto=true) + make_tokenizer(tokens; goto=false) +end +end From 937bebb4ddbe08f89427086fb5c8c6311c509c02 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Wed, 22 Feb 2023 16:48:38 +0100 Subject: [PATCH 50/64] Rewrite documentation --- .gitignore | 2 +- docs/Project.toml | 6 +- docs/create_pngs.jl | 77 ++++++ docs/make.jl | 20 +- docs/src/custom.md | 208 ++++++++++++++ docs/src/debugging.md | 184 +++++++++++++ docs/src/figure/alt.png | Bin 0 -> 12743 bytes docs/src/figure/cat.png | Bin 0 -> 4603 bytes docs/src/figure/kleenestar.png | Bin 0 -> 12368 bytes docs/src/figure/large_dfa.png | Bin 0 -> 19790 bytes docs/src/figure/large_machine.png | Bin 0 -> 10743 bytes docs/src/figure/larger.png | Bin 0 -> 56495 bytes docs/src/figure/numbers.png | Bin 49540 -> 0 bytes docs/src/figure/simple.png | Bin 0 -> 6574 bytes docs/src/index.md | 444 ++++++------------------------ docs/src/io.md | 309 +++++++++++++++++++++ docs/src/parser.md | 210 ++++++++++++++ docs/src/reader.md | 123 +++++++++ docs/src/references.md | 22 -- docs/src/regex.md | 72 +++++ docs/src/theory.md | 142 ++++++++++ docs/src/tokenizer.md | 145 ++++++++++ docs/src/validators.md | 76 +++++ ext/AutomaStream.jl | 39 --- src/Automa.jl | 51 +++- src/codegen.jl | 116 ++++++-- src/dfa.jl | 2 +- src/dot.jl | 11 + src/machine.jl | 6 +- src/re.jl | 116 +++++++- todo.md | 9 + 31 files changed, 1931 insertions(+), 459 deletions(-) create mode 100644 docs/create_pngs.jl create mode 100644 docs/src/custom.md create mode 100644 docs/src/debugging.md create mode 100644 docs/src/figure/alt.png create mode 100644 docs/src/figure/cat.png create mode 100644 docs/src/figure/kleenestar.png create mode 100644 docs/src/figure/large_dfa.png create mode 100644 docs/src/figure/large_machine.png create mode 100644 docs/src/figure/larger.png delete mode 100644 docs/src/figure/numbers.png create mode 100644 docs/src/figure/simple.png create mode 100644 docs/src/io.md create mode 100644 docs/src/parser.md create mode 100644 docs/src/reader.md delete mode 100644 docs/src/references.md create mode 100644 docs/src/regex.md create mode 100644 docs/src/theory.md create mode 100644 docs/src/tokenizer.md create mode 100644 docs/src/validators.md create mode 100644 todo.md diff --git a/.gitignore b/.gitignore index 3d83c49d..32972416 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ docs/*.dot docs/build/ docs/site/ .Rproj.user -/Manifest.toml +Manifest.toml diff --git a/docs/Project.toml b/docs/Project.toml index 9a7dacb1..4f7707c7 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,7 +1,9 @@ [deps] -Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" Automa = "67c07d97-cdcb-5c2c-af73-a7f9c32a568b" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +TranscodingStreams = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" [compat] +Automa = "1" +TranscodingStreams = "0.9" Documenter = "0.24 - 0.26" -Automa = "0.8 - 0.9" \ No newline at end of file diff --git a/docs/create_pngs.jl b/docs/create_pngs.jl new file mode 100644 index 00000000..1e45b383 --- /dev/null +++ b/docs/create_pngs.jl @@ -0,0 +1,77 @@ +using Automa + +DIR = joinpath(dirname(dirname(Base.pathof(Automa))), "docs/src/figure") +ispath(DIR) || mkdir(DIR) + +function regex_png(regex, path) + open("/tmp/re.dot", "w") do io + println(io, Automa.nfa2dot(Automa.re2nfa(regex))) + end + run(pipeline(`dot -Tpng /tmp/re.dot`, stdout=path)) +end + +function dot_png(dot, path) + open("/tmp/re.dot", "w") do io + println(io, dot) + end + run(pipeline(`dot -Tpng /tmp/re.dot`, stdout=path)) +end + +regex_png(re"a", "$DIR/simple.png") +regex_png(re"(\+|-)?(0|1)*", "$DIR/larger.png") + +dot = """ +digraph { + graph [ rankdir = LR ]; + A [ shape = circle ]; + A -> B [ label = "ϵ" ]; + B [ shape = doublecircle ]; +} +""" + +dot_png(dot, "$DIR/cat.png") + +dot = """ +digraph { + graph [ rankdir = LR ]; + A [ shape = circle ]; + B [ shape = circle ]; + 1 [ shape = circle ]; + 2 [ shape = doublecircle ]; + 1 -> A [ label = "ϵ" ]; + 1 -> B [ label = "ϵ" ]; + A -> 2 [ label = "ϵ" ]; + B -> 2 [ label = "ϵ" ]; +} +""" + +dot_png(dot, "$DIR/alt.png") + +dot = """ +digraph { + graph [ rankdir = LR ]; + A [ shape = circle ]; + 1 [ shape = circle ]; + 2 [ shape = doublecircle ]; + 1 -> A [ label = "ϵ" ]; + 1 -> 2 [ label = "ϵ" ]; + A -> 2 [ label = "ϵ" ]; + A -> A [ label = "ϵ" ]; +} +""" + +dot_png(dot, "$DIR/kleenestar.png") + +open("/tmp/re.dot", "w") do io + nfa = Automa.remove_dead_nodes(Automa.re2nfa(re"(\+|-)?(0|1)*")) + #dfa = Automa.remove_dead_nodes(Automa.reduce_nodes(Automa.nfa2dfa(nfa))) + dfa = Automa.remove_dead_nodes(Automa.nfa2dfa(nfa)) + println(io, Automa.dfa2dot(dfa)) +end +run(pipeline(`dot -Tpng /tmp/re.dot`, stdout="$DIR/large_dfa.png")) + +open("/tmp/re.dot", "w") do io + machine = compile(re"(\+|-)?(0|1)*") + println(io, Automa.machine2dot(machine)) +end +run(pipeline(`dot -Tpng /tmp/re.dot`, stdout="$DIR/large_machine.png")) diff --git a/docs/make.jl b/docs/make.jl index ce56e9d6..981bd0c3 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,18 +1,30 @@ using Documenter +using TranscodingStreams # to load extension using Automa -# run(`julia actions.jl`) -# run(`julia preconditions.jl`) +DocMeta.setdocmeta!(Automa, :DocTestSetup, :(using Automa); recursive=true) + +#include("create_pngs.jl") makedocs( sitename = "Automa.jl", modules = [Automa], pages = [ "Home" => "index.md", - "References" => "references.md" + "Theory" => "theory.md", + "Regex" => "regex.md", + "Validators" => "validators.md", + "Tokenizers" => "tokenizer.md", + "Parsing buffers" => "parser.md", + "Customizing codegen" => "custom.md", + "Parsing IOs" => "io.md", + "Creating readers" => "reader.md", + "Debugging Automa" => "debugging.md", ], format = Documenter.HTML( - prettyurls = get(ENV, "CI", nothing) == "true") + prettyurls = get(ENV, "CI", nothing) == "true" + ), + checkdocs = :exports ) deploydocs( diff --git a/docs/src/custom.md b/docs/src/custom.md new file mode 100644 index 00000000..cee621fc --- /dev/null +++ b/docs/src/custom.md @@ -0,0 +1,208 @@ +```@meta +CurrentModule = Automa +DocTestSetup = quote + using TranscodingStreams + using Automa +end +``` + +# Customizing Automa's code generation +Automa offers a few ways of customising the created code. +Note that the precise code generated by automa is considered an implementation detail, +and as such is subject to change without warning. +Only the overall behavior, i.e. the "DFA simulation" can be considered stable. + +Nonetheless, it is instructive to look at the code generated for the machine in the "parsing from a buffer" section. +I present it here cleaned up and with comments for human inspection. + +```julia +# Initialize variables used in the code below +byte::UInt8 = 0x00 +p::Int = 1 +p_end::Int = sizeof(data) +p_eof::Int = p_end +cs::Int = 1 + +# Turn the input buffer into SizedMemory, to load data from pointer +GC.@preserve data begin +mem::Automa.SizedMemory = (Automa.SizedMemory)(data) + +# For every input byte: +while p ≤ p_end && cs > 0 + # Load byte + byte = mem[p] + + # Load the action, to execute, if any, by looking up in a table + # using the current state (cs) and byte + @inbounds var"##292" = Int((Int8[0 0 … 0 0; 0 0 … 0 0; … ; 0 0 … 0 0; 0 0 … 0 0])[(cs - 1) << 8 + byte + 1]) + + # Look up next state. If invalid input, next state is negative current state + @inbounds cs = Int((Int8[-1 -2 … -5 -6; -1 -2 … -5 -6; … ; -1 -2 … -5 -6; -1 -2 … -5 -6])[(cs - 1) << 8 + byte + 1]) + + # Check each possible action looked up above, and execute it + # if it is not zero + if var"##292" == 1 + pos = p + elseif var"##292" == 2 + header = String(data[pos:p - 1]) + elseif if var"##292" == 3 + append!(buffer, data[pos:p - 1]) + elseif var"##292" == 4 + seq = Seq(header, String(buffer)) + push!(seqs, seq) + end + + # Increment position by 1 + p += 1 + + # If we're at end of input, and the current state in in an accept state: + if p > p_eof ≥ 0 && cs > 0 && (cs < 65) & isodd(0x0000000000000021 >>> ((cs - 1) & 63)) + # What follows is a list of all possible EOF actions. + + # If state is state 6, execute the appropriate action + # tied to reaching end of input at this state + if cs == 6 + seq = Seq(header, String(buffer)) + push!(seqs, seq) + cs = 0 + + # Else, if the state is < 0, we have taken a bad input (see where cs was updated) + # move position back by one to leave it stuck where it found bad input + elseif cs < 0 + p -= 1 + end + + # If cs is not 0, the machine is in an error state. + # Gather some information about machine state, then throw an error + if cs != 0 + cs = -(abs(cs)) + var"##291" = if p_eof > -1 && p > p_eof + nothing + else + byte + end + Automa.throw_input_error($machine, -cs, var"##291", mem, p) + end +end +end # GC.@preserve +``` + +## Using `CodeGenContext` +The `CodeGenContext` (or ctx, for short) struct is a collection of settings used to customize code creation. +If not passed to the code generator functions, a default `CodeGenContext` is used. + +### Variable names +One obvious place to customize is variable names. +In the code above, for example, the input bytes are named `byte`. +What if you have another variable with that name? + +The ctx contains a `.vars` field with a `Variables` object, which is just a collection of names used in generated code. +For example, to rename `byte` to `u8` in the generated code, you first create the appropriate ctx, +then use the ctx to make the code. + +```julia +ctx = CodeGenContext(vars=Automa.Variables(byte=:u8)) +code = generate_code(ctx, machine, actions) +``` + +### Other options +* The `clean` option strips most linenumber information from the generated code, if set to true. +* `getbyte` is a function that is called like this `getbyte(data, p)` to obtain `byte` in the main loop. + This is usually just `Base.getindex`, but can be customised to be an arbitrary function. + +### Code generator +The code showed at the top of this page is code made with the table code generator. +Automa also supports creating code using the goto code generator instead of the default table generator. +The goto generator creates code with the following properties: +* It is much harder to read than table code +* The code is much larger +* It does not use boundschecking +* It does not allow customizing `getbyte` +* It is much faster than the table generator + +Normally, the table generator is good enough, but for performance sensitive applications, +the goto generator can be used. + +## Optimising the previous example +Let's try optimising the previous FASTA parsing example. +My original code did 300 MB/s. + +To recap, the `Machine` was: + +```jldoctest custom1; output = false +machine = let + header = onexit!(onenter!(re"[a-z]+", :mark_pos), :header) + seqline = onexit!(onenter!(re"[ACGT]+", :mark_pos), :seqline) + record = onexit!(re">" * header * '\n' * rep1(seqline * '\n'), :record) + compile(rep(record)) +end +@assert machine isa Automa.Machine + +# output + +``` + +The first improvement is to the algorithm itself: Instead of of parsing to a vector of `Seq`, +I'm simply going to index the input data, filling up an existing vector of: + +```jldoctest custom1; output = false +struct SeqPos + offset::Int + hlen::Int32 + slen::Int32 +end + +# output + +``` + +The idea here is to remove as many allocations as possible. +This will more accurately show the speed of the DFA simulation, which is now the bottleneck. +The actions will therefore be + +```jldoctest custom1; output = false +actions = Dict( + :mark_pos => :(pos = p), + :header => :(hlen = p - pos), + :seqline => :(slen += p - pos), + :record => quote + seqpos = SeqPos(offset, hlen, slen) + nseqs += 1 + seqs[nseqs] = seqpos + offset += hlen + slen + slen = 0 + end +); + +@assert actions isa Dict + +# output + +``` + +With the new variables such as `slen`, we need to update the function code as well: +```jldoctest custom1; output = false +@eval function parse_fasta(data) + pos = slen = hlen = offset = nseqs = 0 + seqs = Vector{SeqPos}(undef, 400000) + $(generate_code(machine, actions)) + return seqs +end + +# output +parse_fasta (generic function with 1 method) +``` + +This parses a 45 MB file in about 100 ms in my laptop, that's 450 MB/s. +Now let's try the exact same, except with the code being generated by: + +`$(generate_code(CodeGenContext(generator=:goto), machine, actions))` + +Now the code parses the same 45 MB FASTA file in 11.14 miliseconds, parsing at about 4 GB/s. + +## Reference + +```@docs +Automa.CodeGenContext +Automa.Variables +``` \ No newline at end of file diff --git a/docs/src/debugging.md b/docs/src/debugging.md new file mode 100644 index 00000000..9970f5b6 --- /dev/null +++ b/docs/src/debugging.md @@ -0,0 +1,184 @@ +```@meta +CurrentModule = Automa +DocTestSetup = quote + using TranscodingStreams + using Automa +end +``` + +# Debugging Automa + +!!! danger + All Automa's debugging tools are NOT part of the API and are subject to change without warning. + You can use them during development, but do NOT rely on their behaviour in your final code. + +Automa is a complicated package, and the process of indirectly designing parsers by first designing a machine can be error prone. +Therefore, it's crucial to have good debugging tooling. + +## Revise +Revise is not able to update Automa-generated functions. +To make your feedback loop faster, you can manually re-run the code that defines the Automa functions - usually this is much faster than modifying the package and reloading it. + +## Ambiguity check +It is easy to accidentally create a machine where it is undecidable what actions should be taken. +For example: + +```jldoctest +machine = let + alphabet = re"BC" + band = onenter!(re"BBA", :cool_band) + compile(re"XYZ A" * (alphabet | band)) +end + +# output +ERROR: Ambiguous NFA. +[...] +``` + +Consider what the machine should do once it observes the two first bytes `AB` of the input: +Is the `B` part of `alphabet` (in which case it should do nothing), or is it part of `band` (in which case it should do the action `:cool_band`)? It's impossible to tell. + +Automa will not compile this, and will raise the error: +``` +ERROR: Ambiguous NFA. +``` + +Note the error shows an example input which will trigger the ambiguity: `XYZ A`, then `B`. +By simply running the input through in your head, you may discover yourself how the error happens. + +In the example above, the error was obvious, but consider this example: + +```jldoctest +fasta_machine = let + header = re"[a-z]+" + seq_line = re"[ACGT]+" + sequence = seq_line * rep('\n' * seq_line) + record = onexit!('>' * header * '\n' * sequence, :emit_record) + compile(rep(record * '\n') * opt(record)) +end + +# output +ERROR: Ambiguous NFA. +[...] +``` + +It's the same problem: After a sequence line you observe `\n`: Is this the end of the sequence, or just a newline before another sequence line? + +To work around it, consider when you know _for sure_ you are out of the sequence: It's not before you see a new `>`, or end-of-file. +In a sense, the trailing `\n` really IS part of the sequence. +So, really, your machine should regex similar to this + +```jldoctest debug1; output = false +fasta_machine = let + header = re"[a-z]+" + seq_line = re"[ACGT]+" + sequence = rep1(seq_line * '\n') + record = onexit!('>' * header * '\n' * sequence, :emit_record) + + # A special record that can avoid a trailing newline, but ONLY if it's the last record + record_eof = '>' * header * '\n' * seq_line * rep('\n' * seq_line) * opt('\n') + compile(rep(record * '\n') * opt(record_eof)) +end +@assert fasta_machine isa Automa.Machine + +# output +``` + +When all else fails, you can also pass `unambiguous=false` to the `compile` function - but beware! +Ambiguous machines has undefined behaviour if you get into an ambiguous situation. + +## Create `Machine` flowchart +The function `machine2dot(::Machine)` will return a string with a Graphviz `.dot` formatted flowchart of the machine. +Graphviz can then convert the dot file to an SVG function. + +On my computer (with Graphviz and Firefox installed), I can use the following Julia code to display a flowchart of a machine. +Note that `dot` is the command-line name of Graphviz. + +```julia +function display_machine(m::Machine) + open("/tmp/machine.dot", "w") do io + println(io, Automa.machine2dot(m)) + end + run(pipeline(`dot -Tsvg /tmp/machine.dot`, stdout="/tmp/machine.svg")) + run(`firefox /tmp/machine.svg`) +end +``` + +The following function are Automa internals, but they might help with more advanced debugging: +* `re2nfa` - create an NFA from an Automa regex +* `nfa2dot` - create a dot-formatted string from an nfa +* `nfa2dfa` - create a DFA from an NFA +* `dfa2dot` - create a dot-formatted string from a DFA + +## Running machines in debug mode +The function `generate_code` takes an argument `actions`. If this is `:debug`, then all actions in the given `Machine` will be replaced by `:(push!(logger, action_name))`. +Hence, given a FASTA machine, you could create a debugger function: + +```jldoctest debug1; output = false + @eval function debug(data) + logger = [] + $(generate_code(fasta_machine, :debug)) + logger +end + +# output +debug (generic function with 1 method) +``` + +Then see all the actions executed in order, by doing: + +```julia +julia> debug(">abc\nTAG") +4-element Vector{Any}: + :mark + :header + :mark + :seqline + :record +``` + +Note that if your machine relies on its actions to work correctly, for example by actions modifying `p`, +this kind of debugger will not work, as it replaces all actions. + +## More advanced debuggning +The file `test/debug.jl` contains extra debugging functionality and may be `include`d. +In particular it defines the functions `debug_execute` and `create_debug_function`. + +The function of `create_debug_function(::Machine; ascii=false)` is best demonstrated: + +```julia +machine = let + letters = onenter!(re"[a-z]+", :enter_letters) + compile(onexit!(letters * re",[0-9]," * letters, :exiting_regex)) +end +eval(create_debug_function(machine; ascii=true)) +(end_state, transitions) = debug_compile("abc,5,d!") +@show end_state +transitions +``` + +Will create the following output: +``` +end state = -6 +7-element Vector{Tuple{Char, Int64, Vector{Symbol}}}: + ('a', 2, [:enter_letters]) + ('b', 2, []) + ('c', 2, []) + (',', 3, []) + ('5', 4, []) + (',', 5, []) + ('d', 6, [:enter_letters]) +``` + +Where each 3-tuple in the input corresponds to the input byte (displayed as a `Char` if `ascii` is set to `true`), the Automa state reached on reading the letter, and the actions executed. + +The `debug_execute` function works the same as the `debug_compile`, but does not need to be generated first, and can be run directly on an Automa regex: + +```julia +julia> debug_execute(re"[A-z]+", "abc1def"; ascii=true) +(-3, Tuple{Union{Nothing, Char}, Int64, Vector{Symbol}}[('a', 2, []), ('b', 3, []), ('c', 3, [])]) +``` + +```@docs +machine2dot +``` diff --git a/docs/src/figure/alt.png b/docs/src/figure/alt.png new file mode 100644 index 0000000000000000000000000000000000000000..36f936091e212fbb07f0f97a37e92ca6d87eb667 GIT binary patch literal 12743 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 literal 0 HcmV?d00001 diff --git a/docs/src/figure/large_dfa.png b/docs/src/figure/large_dfa.png new file mode 100644 index 0000000000000000000000000000000000000000..59dc492c6480eded454128658cefb7c140ba3531 GIT binary patch literal 19790 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 bPo!%66#ifllCi=~PLz_|19Y*>qZj`Jc*E%i literal 0 HcmV?d00001 diff --git a/docs/src/figure/larger.png b/docs/src/figure/larger.png new file mode 100644 index 0000000000000000000000000000000000000000..02e4aeff39e2166d01edbdba18988f3be28b4115 GIT binary patch literal 56495 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

qGr24yBwneO2^4GCjv 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∾B{&#LZ5<*GoMw~r3OkZLTU03xhU=}2IJ)Z8EJj{C zlrLsH!CO<|nbxqLa>7_Kbv448)6*5)aJvirdfAXX!pw!C=k#h*B9K198cWYNC_Ozu z8vC57B6>{~(#2)Fay`!IA6e>?eyhKFYw@bL*HqtYkqU3? z&4nRd zosTN6b}&a@@}&rbz1V)z2BBZmiay88t}!T3_l1`tOwmLyb=Gg(ZUcnxwPF+g&R+gd z{gc?Dttnmk%pW87_@l8Pb;m()OouU6w)4R>%T2J*z~Ns%RS@?(Ed2{&<>~He%P}XG z%D59o6^aT-ZxBn%iR+0}+=xu=n6_Y%KjZ_g)Cc`P9L*O$cW8E(yfFTI?j#yG5>bIcRi#DO7=Q;X zt9Za3tCBKZvje^~8TFOu{On5(Z89l~f)$v;>7Kgsxfp9@#ErNs$i%0LPXGSnt=n36 zQ^bu!3woz97VCu4N``xSX<; zFQ=}XUSF*C`B$-K!#O-ub=y621g&0)M8U_ueGCcV0~%NqnKKrQ2 z?6D8V7Tx@xpRT{5qcIszXcUxJZTCgN&X(b_37?wyU-F37-}9KxhIIEBO(-=z(V3w; zWYVu)J4yVbni$l@v0*VOSJrV0JvB0N*_AKQj*iI<*#0`LS*sbbFf2r&MHj6@859$} zh`KF#zPIJUfGqB6uybrbPCQc9i(qp#gpoKWSVm!zlp}PpP1BBZZNP>v?-8dAW zG~dYoGHFYCS~f5DL7V6e=@LM7f9hR6~yg=@f#k6YQc~N07t;%MW ze-x9`fAXBJjGhx=vojIPMNJt(eu+anmTCy+cZ02E;P9LKiV51Dx#A%gT56LF8d+I- zymPF8>{b|8Ofj{qqAo3e|^#U18vxmz%FRGeP0;HBpbS zn7!_-=VGywSG^bS_*|#!$mBOX%%5Y5k$IAf`zZwm%hNFJOJ=Gz$Z${jW5fJ;db3qu zRK}uL)WzJq`HIRG@v*Pd`#TpKXuwhO5ty}3K<=oxJWJzEUs%1ipCM$~0HHZ6>_QlgXXaT+B2^1H7pJUmf)oyRrn&y9Rb~9NRriw zN&yXgf+?u=y8A>Nhc?+0)*t#~dyG>XHY{wdjxrmtl`3q^^+!9bJX* zB&7@(#iZ73aB60uDglo?j+?c5tY$FS99w8N)C-8ki%oc7yD`zIveo2cl(?^sgJa|H zU;FpWB`2kWRz8Zm&O*u}DZDo<qV^Ar;CP;-HT0)oO&|8SOY#jNW~c zpX+QFM3uEZuqv|Vk_N9w!yp&+k90i-8IE2DL5gUQ>~kmhVXyqA&^iIz@x9Iz^elFc zlxELmslR}xvJD^^V_Cs&Z9V#R}#VEr>-BSsui+4$_IS* zjDTj^<6Ldq7frfC!K(ENLktX31O!K)zIB@$7z|)2^$xB&I z;dWHi*{S!=XV`3Nzgl#!J?#oFUD@HJ`raGMYo#-M9|qE}F<@4Ql%F#lHhoX^j!zK= zN6oC6Y}HCnu20;PWWEG{V%TLl=o&1~sB|cYo_vvFSjTr#1Vp}oq12$uQJ|AYlS(F- zxZIRxDB^seaTNb^K-Nn>?}`IsFq|%Gr{9*Ym3IwZ2bz%It3*_-A?{y(^oTP|L(wX` z$`VS|G!1~_VY?Xa+X*nu5KwG0vS+Ewb2xm(0ckn1US^fC{VAp4sDkNlCrsmalZ2%7 zJy&v(cub$If*=Up0ov}ZLT?_JUFvXZD3e1sUQATA23tml!V^d#aT^ zvFKd`+*C7FH$q6<{|fplTi@EM5j2lVCy-T@l}@)S$d;`2{82o~Ms>*RG9U_im)zjd zj<`Of9LLpB0VJJ|dg)EDJ9j*#s_u%W??1VSegxeLMhOW3G&+azcy1O1ydu1(3R;9J z{DpY-DBJ)#OgY~YBuVNd_q(ALZoQ${wclrJN=6y-_A&jnk9O1k zF{)Q(r>fQ?J%ba+4XZl5AH=9J4T?hF%J@)BF|rNufMw(=!hrpVR*>4vU6&)RFB|yV z@&rjrY^>Ce$*a{$;5K|ksfACCk=pBKd{dN~4+uJKvrrAoB=aVEx$shgY}yr|DJ3qZ zkfIz|;DWy6C(a>U`9kP0_w(m}X5;OsrVnNuqC+#m&QI59klgV;1sjpWexitKdER#H z0Qq}Fobob1OzXod`ik&+uX)?GjATOO1tL2+qGLuQg1rxVfOgNpt+K^^h&b*=Kufdh zB{ZD8ub^MJ(5A0* zL~v}iE0gP@&{$U6Nl2Flks5wo7pZeF=*(^;=k(h9v@{aLgN#TL^bHdW-#M%~2+0y@8QI|O z6EnII{w3U5qvs|(q8A%AcCn;HGyBl@vn-{}qZkBNzh7oAr>^-C!OfyA?yS@{lnIl1 zPbBx;+wf{~QOasw#C)F;evaz}7x5A=(IOi0$MaIdly#BTw)dokL_kt#&=&@NOphrs z!<))-T9LtP`K39{pvMg>>R|YSVac>g`5ts&3M+n#wzVlnXD2#pKRk4vK(~VcrrYh+23u z=~uUa-Eylo7Gh7>qAzk!1rC)J|5b)_^!e&qib4%32LqFc*uHih3rjsZWe0JJFE-d% zlHbe7!nr52zv-I2JuXoDzVUZuUjRo*bQYN+IFpn9M!+rfv(I)`8^D#{^gI;*tViuT zY-X~XYK2U8OjOZZ@(tA@O>ex!P!qUb38;_9cZ*B=0v5M7gyJ5!Sh$|gnYu=%GzBQv z3{3-{miW34t1h+7+wej4IJx{Lm@v3tRC$UI@@!Ow(qJyAhLM}J=OxDw#Y=sZ>NBrU z;L$DVwd#L^Dt%!DY^Dc{12ezqV^Ee*T&JjkT~!(zEbWY?>j4rTPG4g-ifm&CH z3h)Y**J)7u(C{7>J(MGD;;#gzbFkE9lTyyRp zY0Xci38`iRAcj6iyvesRtmgosAR#_umO1|Vh$S(DT$zm_Nwa%jtpoxS|IG$G2%yY1k? zWJ6>EcD>MzJiXAD!3CruSaD${%=!Ckh;4Jptjl!g&o`;l(+%n%*Z%Q9jhny!jNWvz z>qcs{O+}d!-LAAU8J7Gh08*4N+i-q%O-PwGKN7%Eq2AOCWM~Z+b@fDfOJ-dWmLr(U zA3wh_d1&haN?$7n^n_jx9To`T<%BxPw8w}3N#pUjeqrVUEu0G-v$QiUjl{1O zOujof2QQ=OpmnM=#z8E(482y=7V6V^9bf?(r(er7H2zV`%-*kbF z{f4L>q+o;BkntHTMpRZ(dafqk+7d~%V`)CI`?%71 zBiT-V$7tSSW+`Eii{v=4=3__3m))TV_DcM%j=<$7P>+KceP( ztT`I}x%{ftW#}iw^qjgi220moLf2bAoc^b9;4UQkEVsOcNGKVNjjpX_9)WUwZ}IVE z>y|@%_a|6Ag}C8-$lB|Yy%zpwhj!RaR9M}>R6tweW(~p9Pd4uoGl0{jf-0m<^boi% z(ql8bftCU0@@Lpmp7p8FzI=C5;)s<{I7;MpCKAoX2{vO~YFUjfT;tH92;it+DLO>kyteyE(W@?ezaY z{>5$xoAXq~2}HNaf5>R+?aVGxRX}XiNv@3)o$-1Zy)P+qZd~qh3*zE(v2QqaF$#@P zluftkqTW0)l1@FqAw=P99E9a9j8Y{nvW^>wrS7oa=meGl27dktR{XQMy25yq1Liv7 z3V{FJM%ZpKKqf?gW7`H4Cg*r?eZ`~#k-MpAAYzcp1-+}HDD1;PKA~YU6D)~YnMKD@ON-z-1VB%=58 zd0sNqatAgQzs0H_sfyiL6DpW0?`%M!11}iW)#gt{)A>Ueswh{|iMz0|W{w9Th zV|H@M9_OTiij7RxwB}OAc~Cn51lvU=9mJ<$A4G;^1F48)9dhAm{!W!n^N{h?5N`i% zYx$LJ@(hD^nrmFgnVd?TB>jF&@D|Q|dBy^mg$%{1AXLE-S9_w(#$|VE^ z2N(7lM54u<5gsX*eJN5a8?_u9PEKSVT~sj|l4BmgMqTAdUF*1$IiDHETSjj1*M~X| zcEmk$5iN8cDTZbTA<<2?P8DJIrM(P}AHURu?My>r_&!j0)u?OrP2eUXe<`~vvAQxY zK$=&u`Ob`#7OklpJJaU)AC^dvlB|%Urf=P4?#dh1OJIs z$XpZ&+q9Ul>LrJLJvNeT>$ouDuxQ}Q$}G1x_TEn^Icg^3O8WX9ifzM$^!&zwFU($i z^wd%W$u|bf{Z!cl^QpxK8mYEn?bY_?&mUh+FGbtm(?INKHAi5&LKTTiwL*HCBvubB zmEniCw@Cr6XzO%p+Yg^CER2ootzrU_vU1KR$4%R`i}(YGYWZJl%YfVs<3c4r0==Wo00U=n&*faCzmYhxYI^01Gs-kgV4IPW(|%=nY# zHffUUlOSaWSz#2v5+My?A-2cM<7852fGxY4?R`ZhZmWT2T#hUt9@XskBp7#qRv|xS zxjOVdxkzClD1er@pU&?e;vb;rlY~)M5>OapzViuB43CTa#b&V+I*L(PJ8CQbC}dD{ z%qML&yCXKp=0au0?b7*Lf$~qUam36E-R9l$!hOn(cH`O*7#35KgYTe7S<%~whMAzY z&)*#YMD}aI*3X2S<`%{^ej@I;mIP^j!VdckANbuy>Gcn>Pp38)tDar3g*)_Apb%*bSGs^M&YP^@@lISq9r zcZjxMP;}*vHB4Tr9;!?QB4(>1dls;g;I%c4|B`{63CTc*U;soO!YdS)i{|mkvkqDy zj71g+=Se?6yog)UdH5bkYg~l-loL|v*KtDT509klCEA%ij&%C>60@pAM%@0r&kWZS7Sc+Z z53B&smmdCj8tX)im@gvKuRzi`-oXbxs{fh$Wzxyi<655hQ?-wbsVDsYmhVvBAmF!( z{tu5}p$9a`2x$SZ1!g;$#kARbL9thA4UU?|2Zby+ObOOW14xFVkO^C1&3&aV;PYt=Aynq)s35%KBx(9hH+) z2lTU-ZFy9&*r2?yiMsPKO_%Q&jD6F?nO{yHWxp_WwYllRU#(r=;kkaz8qvXzK6{px)g#JOaTZ^3YkN@MR*$r(oe`una zvTEJ$D>8Lb?DMY`(se{O_)cLx^k(7(b?%%hJP+7F^e$%-fnsGH*OP~V=DagDbhQ5C`N36EldElgoP&B>*x zvD5%?TT^*(oL-8HYUH5oNrEMjU~BP%VVJu2<8*a&7G{c=ZK_v|cmc}qV5W@D72m(t z{?%t6agSbwC!A-oO{!^uwf-pC=87k>D;aEfH$g&Hsa|Yjr0io9W~l4k{9^e={?_&Z zY^K_#zM-pQA7`bF+plB&lxByN&WA=DyzZDUjl%udi{pO{cfbCcn7&hFqdm94`}rh{ zuO$=$mc^&7y)*eND?0nSq6-$m#5a|B2|Bd&L$GxnSfOX=ttBX`!`wj}UwsR&JvDI@ z^P$bKSEe+A4N?C|w% z73#jNQt7`SOm}B;wC%OyW`9b3&z)=5(r#sk8b4qPUv^{Lcy?-9zi12@iC9Yx8>@85 z>z)hLrLcDk9RhT~A`z-E>$;4Pnt#Km!hf*Knit53KXDIq<+!i(y?V7r)Zr+Yq3SxI z#Qt0a+i|&?k}M*5KHPtE;a6srWW?~B*~^rw4GcjdPOBtm7RK_ouvw1O21$JC{?{VQecBTZ;@JcobO zj45D^9M!&D6y|n(#8R){9{7DhGUDi42>DuGA=~G+tK7pJiJ*kv{s+pv~&?(s-=+g;=X*0 zY^r#i1Zq$Q>J62vM37Sd6>2i7vUi zQGF&266TGxKIVviyIqoc`Y1H2Wi~)Qg~Dh*elO9)DXQghb?#Ebe6gw4LG$?zdAzrK z`zPGr9a*pqUVF?o3mb_Eu5yl5$;tpv;h#uwDU{8T>d7j*{w%1#JDSwTIh%FK^K#vs z8_qwm@_{g$ay?J;dB|xFN`zAz#8r0?S3whyGxD+7x#2D_y`j_O@pN@7q6Q{xj-DsUUiXc;j?yXB0oRjrb&{o@?Cz1I6 z)*JfQxFqqbk9N=8i0`+i`XI$XE5Wes^D3lxMCq{Hf6BxP$9|AI_ZAYCCLF+LkEd(Q=c zQdj}zi)cR^ZSgCgm8Pe7Nir#auUs7Bb}JwA*=9k|hr#PlI|eUnTqTgv%S4dNMdRYh zt|mXO*QS*ph>ep5`7*o~2K0zp#eRqcvO5eBoyv#%d&IE8K8fAitOd4HgAtcL9n(9K~9injruCb?MRtC~Mm2p6LAwf)&VfdeL5wNmg#BcsV zCWT-D8bqC$-juC`GZk#*wLk2b=M_oUbvhX|t%Ns}2R<;` z%Fy0?u@l<|;LZ@1f+Y*EN*+!H|MXSD)mK*E#u#aX2nhBZlTu(6%aIyyOhd}`*hPl6FXY}@?RDlJ&A~g8KJo-kQ^aAp2yUi{X^>UCOr%N7gE=^jHey`17Zs8 z`ueU?_YTM^4hR^@BKTsQCsG82kpqZld~x|A4bZk2NQLmEW7aqx|HfKCRUsdqADf{^ zYf6S#$~?Kz{lapn)~lW#>v;wL6!HUqSgO<6Dwr)%Z)vS{g!NvZXQQmb$_JsUJk@7fVFHoDD z7WmhFT6A0Ao2w`xU_&1|Th|8dQ3F|wMXId58K_sP#QjVW8=F;cN+iSp{%SU(gm;zKQ)wO|;Sc=*KKKlG`s_^sj@)yv zQAbou?)NRzxmO@>QfoVMCjzZ=ync3Pl>(5o5n73KL}7DK>C@)GVij_0g~wE8JbqaE zkVGh!PjicYezhs$yn9n)kF0V&E3z|%J&YsUoZXw6;fOhWnp=TnkbrCF0+)s>My~>* zVu0<5m7VVL#4s*)$9z)Q9irdOY2wuuJqm?bnvrgTXJM%mqgWD7b;9K1$AdL-40lkQ z^O&?am_yn(JL_!E%t`LSxMLV>qP1@VaqYu`O|c7|{60Yu2xo>3fPlonaD<=&z>U|+ zP+Gp7U&34`LIv3y@cK(#7Z5jcPmJstlUmxQa=DmP>xn*=ZN;TpXRw)&aqBc4nt-CBFo5jzIMIzqOX+de+FTj+${RHzi#EvXS7T4_4>b-QIHu-p0uyWk| zSzk?u&Uceenn^jYmi|{_UO7?% z$*-w@=t#TRAdb)Z^9-?bBC*{Fyl7i{W4yQlS^+3hNFb*V`5H%ad`iw$HUK~$LLdgn zN$$nJ<{A=`A=sW9j+FF zl0%8ZF`P*PLg9)Es3&nuX|EMkSQUd7{zTAws}BaDe|F~n+$BNio5zOo5|s}VUI7q< zc`6zGZ}h9G39RT@{BJWf6Eb~RF;YlIGHXQt6ah_ z0Zm>9ig0Nm=YGq^V?YEPG6UR@Kn{p9bjcQ&+n+JSh6)?O3Nsi-#H_}Z(b z`e9S6+EG=gzNrleMHJ|KO67&`k&Qia#{#bVRO>iEK~xU%mqovWJ$`&_z!#Dw@1NT8 zN6EqXcAV=9bn0hliac=N6zrVoLfmXSIssgDyfW*J8m@uhVZ`HwHTu1rr+iBvxxH`` zzINTU7j zhL|(#BLik#^LnQ;%nFw2)ZNwzn-Lf^4Q8@}w@7RA%CR)^3yBZR5ZziO!rmg)a5U~ilX6wlfl^iup!w95?r zNk20q07xks*-TnnBHo7e;U0_-Jl@lxTFn&}{a-(Vz+X$|yMSXCB5ViOx?8ln8jsch zted?QbF>~Xf(bdbvZea?g-j8n1%gQt)y<0mh}eMN-z~5N{?aFN9ChXAcUHHR!D9@G zA|Jxb*0x#c>eVO!&%4`gRsgS7g_SB-60RN}5R6B8xWA+jKA?eD-@%YN01$?Dl6n{n z4}1+sL5=`%M%n5pjRa723s7SxH znZty76Vb5ed+=WVtLIwneD+Gpt5XcmsLh}kZ~Bn+#p|%~(kN&Z2Ce>aKlGo2`Ru^) zr2R2vJ=)uyQ6Fg+ZEt}}zfsDC%FdkSAaSqEmO{0zqkf*(J2x#r_DMq@KN?l|^>B-x zph8`Ww%cNVC?jXB6=!YITNF71yCGO>41;Rfu*fjY8Gw%RC8xG14!qVV@OyJVe#MYF z8r?I8Nh`GITx!G*V+#SdR`b@m&e!z08j|r2)fE9GF5t&~1Ak3e3#7m?&=`!sX>^S&k=j$I_&&#_cIULke+kdGv~ z5k$X3kTD9xWsj%Q55cG^M0U+?6`wuD9mGVx)}7J0Vn!1B2w_wYPS?gEI#A$<{1Vkj za2yuq`R5_jm`2>@4{$c`VVZ8n8ojFyj-xUc>N0b+Me6q$F7$^IV*q3ZuD*odJf@s) zs?eH$vb8EvWolGCViPz&2uh)~Pdrt@DPR39jj21-qV#=Iqe-^bYh(}xVBBLRA|3fj zRIULdj4dN~PFh0GM&|gS6pcf}sQ~}tx(CKnldBDQ*7Zd zx97~{&{nk*?h5#APNl4zToxf~&nIM*3bfz5^5>G{U!y;S8z0K{X8M0cLE7#h(;Zn- zM&+->>U47hQ1lTzNKqX z_#T-vUBH8nfar$DytqZ@@>cJ?IYk9tBYqe6KF~+!`hmWrsEr0xCJb8Eq#rSFfsTJt zh&au1clKI^;;tog16V&;x_lS#`+bx8`FJXR3GBuV8Z3sq3Fgjc!&yYqj0p?0ji!R~ zC^gBn`E3k@1*~ZZYxqfrLbC9va>oJLQd~X|^tT$#A{hy~aW~R}`U)3k)dxZ{+~u-D zA1F=v-h8d)XQ5ss>A~zex&4eqxkm<=Vqi0^7(qNXK$&P^tq!i2zD?`63^c>RUhLrJLo&>oZebi*?Q?uz5w4Im>9Bd z)0`?H%E@-90itM^5Z{hq%9HwBe?yGUVcY91Ca&C9d5rE@ys@^CMIWqdMMOTIW7z>% zLc;)pqu0J*gufzZde>v#E9YNIc@<}J2xHpQ{vd(G;=HZvvju;+({=eIYd6+m5%l2`^89UL8XZ73d!gd6VAda&=&@&oIi~QN*LM|rEpKXu zw7&=986qGQze8Rx;00KLb*~+TZ@%##>^FS5xWs4V3l%xJoa3`G_s=qeFXI0XIO0&fDDU@AdE9P9f=Bx#!`tzZBQ8 zidX9_Q|{^3h)iALcl2zjcsR^f+qG2e`3dlVUKp_@i^wqqu!NUV<3+A3t9GH_rf@v2 zm~^?J;K)5*qO-&)t3GKU9ILa4HO*%|&-2f;>yiL^9u@}-J1LP^Lclg+DsiLAf^|bS zp+mb@FF$}K@k*L}$9m=LmFPN-?=x@EPo}wp6K0X_$-B9h3;)q|k0$`-@N~|Kq<8Va z?(m;g6qe{aMQ}rb1GHKM;B42M1u)!sOb(JGH@~Cdg(|W54A&1j zlxJD+?KS56Bf=DEoDi3n7pK)p#=jV27$~rjt(2`l=~!Zd7wRMpc!KPM?KuMY~j(0ono)0H3n^;>O- zU4x1?#`-`UdxrJa=+K$u1B~v8NkpMsLp^EbXs5_$fX4|S3&)y@DeDD!Q%{WYX~^r9 zZi(2_Xs>H`lJ2&kyr>beb>HcUE;L~sZlhH2_UgGA08y?B_9XtN#iGQdRmTW@RlG-iZ~aRg`!Q}AXLL|w^T4{n>5&0gqJigz zwLIjmh8^&%lcuT_4Db=qSsPZHfJ3WLNQV7bi6_`f2whr(vYaS7lwQ{xS^sX@&rsuY z=0F^S;M@u(VyK`z)=4XM4c2{RTB_%VlCuJ;9W$;3YNN4z|Bb;it8<6Zq_wC|uyi+` z+9G{t!gQ0r56ZWw8wt|TsS}fTyL=_>;epMkc45XjHDZ-H4sxpu)75kFk&Qqqb>!;K zg&^EuRJxuSMLIlKT3bRsMjWLO1Nwziud+4XkUO1LGliL4hzy73HWx;8b{VN3?mlcM z5IbFmw&Pn>j^l2x^fSH!x9SN#rADOuDUl^Al_#UzBR3Vj1eeXV5YXP`W3B3+YDF}R znA+JvTM4HIe;TP_3SKmIDIs;(a8XQIsx#Eg6cf1IRh=_ zwM)FV0;%^(D*qnyGK?>)OE^*Xbre|=L5SSGITvSM>q=3ZPj`FXj< z#TDjCp9iFRvruc24b@+mCsV8!k|SW;XV#OP@V2%NXex|#)>fyG+MboUXS6NfUIBR2 z4kikgVsbe8uA{f_=t_~5Upf*3d?jXt+%E02H(98$x?0litjxU)9i~?cwN)9jls8%u z*j>2ofivl{@f#6idbn7S+I}Pl`pX__q0MKn{)w0M*!mi zCfV=nYVfpp>&1rH?ugOB z9b}{icuXqDgwA}D>Mc}J?^#jWto+N40y0uz#-7qZJ5*j~I3)*+AF)&-aLtO1S(1AN zSD@6)thb^v$TTGmoakl#Gp%wV0movVP<*^P+-*ipG(d%!(Fy?kkUBGrCV_hg*4kIc ztk{HgNGIW#H3AW)JZGn!Hd z@(nC@BTuZ@mIb+2_T6UyIBd|-#}Q0e=4!Q;L>u4xaH$V$$cUBK3X%ZwMP{UB4cLk@ z%V-jacCp%UHLzx57UZ5I5Mmj^6%+I}qy1^h}(A+NI;)36o8%U(M$bAJJb5m(0EI<1hjPx>m8{QEB0i1?)lv% z3_(eQrXooyvd(X54t4kWVs7C6pkWdmo%_7 z=1=HJjul%nJ@+aSWWpHF4NBj~ZHnJfxsq6K(Q7RJs_VNSdvB#+?qFCAh`3WfMwQo( zaB0~D`YGmT^ukBZ_#+-7OMQBdjhUW%!xYbsBnUalNjy^G%gVi&jI%7fNB4q1ul^Qe z@S)Y{~T8ixqhm773(Uk+wdRI$soY zxb>kO`UsZr+;VR`+_*lRY+N`_Ivzf-8w1EcFs~C80)~11koc;7*`CbKJ&#m)wjuve z^0#5H8)Sa*2jv7jiQ7 zY3^~fI!MQc%+9^70aLdv`ALoVocAEA?=JFS#w$Bg!;V6^-gzr(bmN8N8NQ?=zo@FW zBWp}%!aRIN+YSVQpne9hd8=GKFLQa7dsz=au5{yp&vTEWzOZxQD{nJD_pTn9+8Vgz zrv#tz_aeH(PPLS}?!aFB_2)%x;Qq0jO=$d%luW6X7k)%NK*;rZbN8;=2N=ivOayoJ zNXf}uUgn-w4TAqM9KQ}UnIrf7dzbd8Hm^*XpL?k!Q-g;snTqk5HJ`Fo>QRo}+vmr{ z(`hF!d?MmIHF{HiUqz;_GJwaZpKV4iJ$ytPN)2G7(oC%v-R|N#bC~9yyVa^jBnaJO z2+5c^a*yV!nZx z=I5TT#MJ$6TBZtoWc`PNDrRdwVW*yU8bB<{lLfPN`s+C--RO7y4w3l^Lez-WgMS#f&_Q@Byvs zcCBGEO?`3ij0|QEjD3s4>*7wqiD@z$dvag%N9AwV8dFJC0nn0Ct@9SoqnvsG647dg z*ZdnX*^cvpSGfmHkCZUXm3s+&b%3J1n4NpE(#zlGMYTjk?ef0Do@&_cz!Go%RV}=< z{_=Id0{*b$ICuQ5N`U3UU1K{xX~ui(RuUSf3N<3?p#bzmT)jPaEr)3?z2O8OG!Bci~!4z z>iiPg7)!M41t9CbD)Wx)y;QTj%gDW)^n3vT3cKi=puXYNs5t#ob0?A*I_0BDJH z?#b;a^kjQ{t;DVIj(u0aj$NUZnM+{<^%t*RpGHB-@F=`xJzxE zSi*A8Vo>6P?F^M8=M8JG{{=4t%06YR3DtqV)Z|?`Zt~5QmX_cPWun ztCd%B07WILs4;KuCG^#vuAP{jd#C-x$=0Egsk}OWkUnoKpUc2jy(`Z`fm>eLbG~?e z8p4?ZlENFbs~armZ$6D~?;|2Ae_h;3JF(7DU+Kd*A47Cn;RZsfce z3MswK$Ku!T4srM#yymTYfq8R}uCl}NKr05bb1yiKh(2!I6@pI9yPnAP&JRT-nc*!* zu0R+&8vDw;(tr8d&r-QiYiny40#FVgw}SwiJINYkuI1q*gk?heH;z-Eh(xc>53&gj zOC?G%Nu8AWjOS6V3cM#YlxmYtd@#h}JLf&^`EpN7-LqmFrsiJZS^~QrKmW3Rmy*C6 zef9pLJs_?wmPuoV>%*Lb%#v!=CB zym2UzVNT;~>dB}Cu1jn2&_8vO&20D9Cwl*D|IBoV!)#vPHY+w^R_-NI`bfjjyct$q zZly(NiH0NXMt_bTrECV=sY z?QRW6C*hsm@m5}>+IBK=qN*My3ciS4juxGfmK#2HG=LYmcOoM|Qx7z%Ubg;I?iqo% zI|;nV{g2rqp7s3MVMJoZ0{=y(*~!(iM{y^=%ilJJI83$WvR#AyvN9+4Hq>nfm^4wj z8flH6Jhg2#Ew{vdu^_;Wy0UGh#2Xv)3C9k+$i29n0&(djzkYx8B^`crp!H4VIQ z!O8!iytd_*ROwrEd`yQpOtkJ^jjP#&DY$>mIg03A4Iq?{Ch$JU_nOW! zmBSt46Fw(EU)NwL8TjE2!cELymzMJ=8Yq|7<&ik^@)W|Q+iG;Oq`f^`i1J z)#`ntV|9!n4ig*k))H%`Qb+UGzhoKcT!U=4_DF(@)>_3capr`bBie7_`Lg6_wqm599@b&*!gak z;7x}(Orqt-tk}U{?A-@b;#?F4;CatA0TO!eT|he0q)D+MqW=GX^8pddM~e$$f_Y}v z%#Q2Kj^fL0=iCG2UX&#;W6V&&f(^WW-hmm>?W!nfKyX;HX1TBx@qhZ79dqXZf|#*m zGtOSiC&wiN5ebzWUpwmk_0e-6;U??riy{}l;!+XEfizF_>VQ9I|J<`%yOPJ=9iSs~394Tr- zqjr1do|hg0m;@c<5Di_-q)+lN05{xGm&=LXcr<(2DwMmur-qBs9CJ0&w`1RR`>g7* z2cdj=M@FDnL6sMb9j1t5Qc>anT=NZk=ialK12C1l$(5xhfGDZ*w`PYr%i%MN==D@n z`A*VzmzfM3E}j+yLo7Y$>aObfR5NE++2)|FGh(b%SMLhfA&NMP>=avs|1taJo}RS< zFvdM^<@@vh4T1jGK?7nk}#{e zk{(R&e92D!WfpTL3yv+2e}5j3CDM11L3K-d>+D~|AzIaYN2o@!U+$$d0PN&|Z0TD7 zB71~N1Vvje5Y6WA9}2qhMK4`3JNb{0i#<&)#=62o+fksl*QA8?Ip6U8c_?E3LZHgk z2`x_SS;W!eRdIMnv&o*hr>8Rj4?+;K<(UXzq`;yL?%d?{)FRnXXBKhKnL)|#b_wiD z#@)P@6W5(kHX6v;L<>GSHz;N5UcY&N9)!15Vr-RpUjwp2H40 zt5OaXr*!Yn{RI*Y5~eCTr6=2dMH~*&t`S$ay>d?%10Yfs>zJxoXOaOBpfHw94+gT~ zM+8M%aS?F$4_Cf^_hyo=xLsEEk#L4t!VRX+y25M}cNS@S!uE3x|G-0v+2YGRyg&D2 zOg%fvtgEVcCuS?Dh@)V_zwnQMV9S4!ZwPS>D`*RPb%K@)~!rT2wMI1UizOPBCwv)6I%&PUx+!KhNE=9aCTmwj&u}0iOWvo*r z6997(Gqx)yF`5ZLj-Y4{djipvU=mvk_eV(vhkL za#Fzktjp7y_7ty�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 diff --git a/docs/src/figure/simple.png b/docs/src/figure/simple.png new file mode 100644 index 0000000000000000000000000000000000000000..f3be227b37d0d8a507bb007406f3d32677218171 GIT binary patch literal 6574 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 KBBDgx(Eook_1@S3 literal 0 HcmV?d00001 diff --git a/docs/src/index.md b/docs/src/index.md index c201c42e..8831f762 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,382 +1,106 @@ -Automa.jl -========= - -Overview --------- - -Automa.jl is a package for generating [finite-state machines -(FSMs)](https://en.wikipedia.org/wiki/Finite-state_machine) and -[tokenizers](https://en.wikipedia.org/wiki/Lexical_analysis) in Julia. - -The following code is an example of tokenizing various kinds of numeric literals in Julia. -```julia -import Automa -import Automa.RegExp: @re_str -const re = Automa.RegExp - -# Describe patterns in regular expression. -oct = re"0o[0-7]+" -dec = re"[-+]?[0-9]+" -hex = re"0x[0-9A-Fa-f]+" -prefloat = re"[-+]?([0-9]+\.[0-9]*|[0-9]*\.[0-9]+)" -float = prefloat | re.cat(prefloat | re"[-+]?[0-9]+", re"[eE][-+]?[0-9]+") -number = oct | dec | hex | float -numbers = re.cat(re.opt(number), re.rep(re" +" * number), re" *") - -# Register action names to regular expressions. -number.actions[:enter] = [:mark] -oct.actions[:exit] = [:oct] -dec.actions[:exit] = [:dec] -hex.actions[:exit] = [:hex] -float.actions[:exit] = [:float] - -# Compile a finite-state machine. -machine = Automa.compile(numbers) - -# This generates a SVG file to visualize the state machine. -# write("numbers.dot", Automa.machine2dot(machine)) -# run(`dot -Tpng -o numbers.png numbers.dot`) - -# Bind an action code for each action name. -actions = Dict( - :mark => :(mark = p), - :oct => :(emit(:oct)), - :dec => :(emit(:dec)), - :hex => :(emit(:hex)), - :float => :(emit(:float)), -) - -# Generate a tokenizing function from the machine. -context = Automa.CodeGenContext() -@eval function tokenize(data::String) - tokens = Tuple{Symbol,String}[] - mark = 0 - $(Automa.generate_init_code(context, machine)) - emit(kind) = push!(tokens, (kind, data[mark:p-1])) - $(Automa.generate_exec_code(context, machine, actions)) - return tokens, cs == 0 ? :ok : cs < 0 ? :error : :incomplete -end - -tokens, status = tokenize("1 0x0123BEEF 0o754 3.14 -1e4 +6.022045e23") -``` - -Finally, space-separated numbers are tokenized as follows: -```jlcon -julia> tokens -6-element Array{Tuple{Symbol,String},1}: - (:dec,"1") - (:hex,"0x0123BEEF") - (:oct,"0o754") - (:float,"3.14") - (:float,"1e-4") - (:float,"+6.022045e23") - -julia> status -:ok - -``` - -![](figure/numbers.png) - -Automa.jl is composed of three elements: regular expressions, compilers, and code generators. Regular expressions are used to specify patterns that you want to match and bind actions to. A regular expression can be built using APIs provided from the `Automa.RegExp` module. The regular expression with actions is then fed to a compiler function that creates a finite state machine and optimizes it to minimize the number of states. Finally, the machine object is used to generate Julia code that can be spliced into functions. - -Machines are byte-oriented in a sense that input data fed into a machine is a sequence of bytes. The generated code of a machine reads input data byte by byte and updates a current state variable based on transition rules defined by regular expressions. If one or more actions are associated to a state transition they will be executed before reading a next byte. If no transition rule is found for a byte of a specific state the machine sets the current state to an error value, stops executing, and breaks from a loop. - - -Regular expressions -------------------- - -Regular expressions in Automa.jl is somewhat more restricted than usual regular expressions in Julia. Some features like lookahead or backreference are not provided. In Automa.jl, `re"..."` is used instead of `r"..."` because these are different regular expressions. However, the syntax of Automa.jl's regular expressions is a subset of Julia's ones and hence it would be already familiar. Some examples are shown below: - -```julia -decimal = re"[-+]?[0-9]+" -keyword = re"if|else|while|end" -identifier = re"[A-Za-z_][0-9A-Za-z_]*" -``` - -An important feature of regular expressions is composition of (sub-) regular expressions. One or more regular expressions can be composed using following functions: - -| Function | Alias | Meaning | -| -------- | ------ | ------- | -| `cat(re...)` | `*` | concatenation | -| `alt(re1, re2...)` | `\|` | alternation | -| `rep(re)` | | zero or more repetition | -| `rep1(re)` | | one or more repetition | -| `opt(re)` | | zero or one repetition | -| `isec(re1, re2)` | `&` | intersection | -| `diff(re1, re2)` | `\` | difference (subtraction) | -| `neg(re)` | `!` | negation | - -Actions can be bind to regular expressions. Currently, there are four kinds of actions: enter, exit, :all and final. Enter actions will be executed when it enters the regular expression. In contrast, exit actions will be executed when it exits from the regular expression. All actions will be executed in all transitions and final actions will be executed every time when it reaches a final (or accept) state. The following code and figure demonstrate transitions and actions between states. - -```julia -using Automa -using Automa.RegExp: @re_str -const re = Automa.RegExp - -ab = re"ab*" -c = re"c" -pattern = re.cat(ab, c) - -ab.actions[:enter] = [:enter_ab] -ab.actions[:exit] = [:exit_ab] -ab.actions[:all] = [:all_ab] -onfinal!(ab, :final_ab) -c.actions[:enter] = [:enter_c] -c.actions[:exit] = [:exit_c] -c.actions[:final] = [:final_c] - -write("actions.dot", Automa.machine2dot(Automa.compile(pattern))) -run(`dot -Tpng -o src/figure/actions.png actions.dot`) -``` - -![](figure/actions.png) - -Transitions can be conditioned by actions that return a boolean value. Assigning a name to the `when` field of a regular expression can bind an action to all transitions within the regular expression as the following example shows. - -```julia -using Automa -using Automa.RegExp: @re_str -const re = Automa.RegExp - -ab = re"ab*" -ab.when = :cond -c = re"c" -pattern = re.cat(ab, c) - -write("preconditions.dot", Automa.machine2dot(Automa.compile(pattern))) -run(`dot -Tpng -o src/figure/preconditions.png preconditions.dot`) -``` - -![](figure/preconditions.png) - - -Compilers ---------- - -After finished defining a regular expression with optional actions you can compile it into a finite-state machine using the `compile` function. The `Machine` type is defined as follows: - -```julia -mutable struct Machine - start::Node - n_states::Int - final_states::Set{Int} - eof_actions::Dict{Int,Set{Action}} -end -``` - -For the purpose of debugging, Automa.jl offers the `execute` function, which emulates the machine execution and returns the last state with the action log. Let's execute a machine of `re"a*b"` with actions used in the previous example. -```jlcon -julia> machine = Automa.compile(ab) -Automa.Machine() - -julia> Automa.execute(machine, "b") -(2,Symbol[:enter_a,:exit_a,:enter_b,:final_b,:exit_b]) - -julia> Automa.execute(machine, "ab") -(2,Symbol[:enter_a,:final_a,:exit_a,:enter_b,:final_b,:exit_b]) - -julia> Automa.execute(machine, "aab") -(2,Symbol[:enter_a,:final_a,:final_a,:exit_a,:enter_b,:final_b,:exit_b]) - -``` - -The `Tokenizer` type is also a useful tool built on top of `Machine`: - -```julia -mutable struct Tokenizer - machine::Machine - actions_code::Vector{Tuple{Symbol,Expr}} +```@meta +CurrentModule = Automa +DocTestSetup = quote + using TranscodingStreams + using Automa end ``` -A tokenizer can be created using the `compile` function as well but the -argument types are different. When defining a tokenizer, `compile` takes a list -of pattern and action pairs as follows: -```julia -tokenizer = Automa.compile( - re"if|else|while|end" => :(emit(:keyword)), - re"[A-Za-z_][0-9A-Za-z_]*" => :(emit(:identifier)), - re"[0-9]+" => :(emit(:decimal)), - re"=" => :(emit(:assign)), - re"(" => :(emit(:lparen)), - re")" => :(emit(:rparen)), - re"[-+*/]" => :(emit(:operator)), - re"[\n\t ]+" => :(), -) -``` - -The order of arguments is used to resolve ambiguity of pattern matching. A -tokenizer tries to find the longest token that is available from the current -reading position. When multiple patterns match a substring of the same length, -higher priority token placed at a former position in the arguments list will be -selected. For example, `"else"` matches both `:keyword` and `:identifier` but -the `:keyword` action will be run because it is placed before `:identifier` in -the arguments list. - -Once a pattern is determined, the start and end positions of the token -substring can be accessed via `ts` and `te` local variables in the action code. -Other special variables (i.e. `p`, `p_end`, `is_eof` and `cs`) will be explained -in the following section. See example/tokenizer.jl for a complete example. - - -Code generators ---------------- +# Automa.jl +Automa is a regex-to-Julia compiler. +By compiling regex to Julia code in the form of `Expr` objects, +Automa provides facilities to create efficient and robust regex-based lexers, tokenizers and parsers using Julia's metaprogramming capabilities. +You can view Automa as a regex engine that can insert arbitrary Julia code into its input matching process, which will be executed when certain parts of the regex matches an input. -Once a machine or a tokenizer is created it's ready to generate Julia code using metaprogramming techniques. -Here is an example to count the number of words in a string: -```julia -import Automa -import Automa.RegExp: @re_str -const re = Automa.RegExp +![Schema of Automa.jl](figure/Automa.png) -word = re"[A-Za-z]+" -words = re.cat(re.opt(word), re.rep(re" +" * word), re" *") +Automa.jl is designed to generate very efficient code to scan large text data, which is often much faster than handcrafted code. +Automa.jl is a regex engine that can insert arbitrary Julia code into its input matching process, that will be executed in when certain parts of the regex matches an input. -onexit!(word, :word) +## Where to start +If you're not familiar with regex engines, start by reading the [theory section](theory.md), +then you might want to read every section from the top. +They're structured like a tutorial, beginning from the simplest use of Automa and moving to more advanced uses. -machine = Automa.compile(words) +If you like to dive straight in, you might want to start by reading the examples below, then go through the examples in the `examples/` directory in the Automa repository. -actions = Dict(:word => :(count += 1)) +## Examples +### Validate some text only is composed of ASCII alphanumeric characters +```jldoctest; output = false +generate_buffer_validator(:validate_alphanumeric, re"[a-zA-Z0-9]*") |> eval -# Generate a function using @eval. -context = Automa.CodeGenContext() -@eval function count_words(data) - # initialize a result variable - count = 0 - - # Generate code to initialize FSM and execute main loop - $(Automa.generate_code(context, machine)) - - # check if FSM properly finished - if cs != 0 - error("failed to count words") - end - - return count +for s in ["abc", "aU81m", "!,>"] + println("$s is alphanumeric? $(isnothing(validate_alphanumeric(s)))") end -``` - -This will work as we expect: -```jlcon -julia> count_words("") -0 - -julia> count_words("The") -1 - -julia> count_words("The quick") -2 - -julia> count_words("The quick brown") -3 - -julia> count_words("The quick brown fox") -4 - -julia> count_words("A!") -ERROR: failed to count words - in count_words(::String) at ./REPL[10]:16 +# output +abc is alphanumeric? true +aU81m is alphanumeric? true +!,> is alphanumeric? false ``` -There are two code-generating functions: `generate_init_code` and -`generate_exec_code`. Both of them take a `CodeGenContext` object as the first -argument and a `Machine` object as the second. The `generate_init_code` -generates variable declatarions used by the finite state machine (FSM). - -```jlcon -julia> Automa.generate_init_code(context, machine) -quote # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 67: - p::Int = 1 # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 68: - p_end::Int = sizeof(data) # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 69: - is_eof::Bool = true # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 70: - cs::Int = 1 -end - +### Making a lexer +```jldoctest; output = false +tokens = [ + :identifier => re"[A-Za-z_][0-9A-Za-z_!]*", + :lparens => re"\(", + :rparens => re"\)", + :comma => re",", + :quot => re"\"", + :space => re"[\t\f ]+", +]; +@eval @enum Token errortoken $(first.(tokens)...) +make_tokenizer((errortoken, + [Token(i) => j for (i,j) in enumerate(last.(tokens))] +)) |> eval + +collect(tokenize(Token, """(alpha, "beta15")""")) + +# output +8-element Vector{Tuple{Int64, Int32, Token}}: + (1, 1, lparens) + (2, 5, identifier) + (7, 1, comma) + (8, 1, space) + (9, 1, quot) + (10, 6, identifier) + (16, 1, quot) + (17, 1, rparens) ``` -The input byte sequence is stored in the `data` variable, which, in this case, -is passed as an argument. The `data` object must support `Automa.pointerstart` -and `Automa.pointerend` methods. These point to the start and end memory -positions, respectively. There are default implementations for these methods, -which depend on `Base.pointer` and `Base.sizeof` methods. So, if `data` is a -`Vector{UInt8}` or a `String` object, there is no need to implement them. But if -you want to use your own type, you need to support them. - -The variable `p` points at the next byte position in `data`. `p_end` points at -the end position of data available in `data`. `is_eof` marks whether `p_end` -points at the *actual* end of the input sequence, instead of the end of a smaller -buffer. In the example above, `p_end` is set to `sizeof(data)`, and `is_eof` is true. -`is_eof` would be `false` to store in memory. -The `cs` variable stores the current state of a machine. - -The `generate_exec_code` generates code that emulates the FSM execution by -updating `cs` (current state) while reading bytes from `data`. You don't need to -care about the details of generated code because it is often too complicated to -read for human. In short, the generated code tries to read as many bytes as -possible from `data` and stops when it reaches `p_end` or when it fails -transition. - -```jlcon -julia> Automa.generate_exec_code(context, machine, actions) -quote # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 116: - ##659 = (Automa.SizedMemory)(data) # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 117: - while p ≤ p_end && cs > 0 # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 118: - ##660 = (getindex)(##659, p) # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 119: - @inbounds ##661 = ([0 0; 0 0; … ; 0 0; 0 0])[(cs - 1) << 8 + ##660 + 1] # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 120: - @inbounds cs = ([-1 -2; -1 -2; … ; -1 -2; -1 -2])[(cs - 1) << 8 + ##660 + 1] # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 121: - if ##661 == 1 - count += 1 - else - () - end # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 122: - p += 1 - end # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 124: - if is_eof && p > p_end && cs ∈ Set([2, 1]) # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 125: - if cs == 2 - count += 1 - else - if cs == 1 - else - () - end - end # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 126: - cs = 0 - else # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 127: - if cs < 0 # /Users/kenta/.julia/v0.6/Automa/src/codegen.jl, line 128: - p -= 1 - end - end +### Make a simple TSV file parser +```jldoctest; output = false +machine = let + name = onexit!(onenter!(re"[^\t\r\n]+", :mark), :name) + field = onexit!(onenter!(re"[^\t\r\n]+", :mark), :field) + nameline = name * rep('\t' * name) + record = onexit!(field * rep('\t' * field), :record) + compile(nameline * re"\r?\n" * record * rep(re"\r?\n" * record) * rep(re"\r?\n")) end -``` - -After finished execution, the value stored in `cs` indicates whether the execution successfully finished or not. `cs == 0` means the FSM read all data and finished successfully. `cs < 0` means it failed somewhere. `cs > 0` means it is still in the middle of execution and needs more input data if any. The following snippet is a pseudocode of the machine execution: - -``` -# start main loop -while p ≤ p_end && cs > 0 - l = {{ read a byte of `data` at position `p` }} - if {{ transferable from `cs` with `l` }} - cs = {{ next state of `cs` with `l` }} - {{ execute actions if any }} - else - cs = -cs +actions = Dict( + :mark => :(pos = p), + :name => :(push!(headers, String(data[pos:p-1]))), + :field => quote + n_fields += 1 + push!(fields, String(data[pos:p-1])) + end, + :record => quote + n_fields == length(headers) || error("Malformed TSV") + n_fields = 0 end - p += 1 # increment the position variable -end +) -if is_eof && p > p_end && cs ∈ machine.final_states - {{ execute EOF actions if any }} - cs = 0 -elseif cs < 0 - p -= 1 # point at the last read byte +@eval function parse_tsv(data) + headers = String[] + fields = String[] + pos = n_fields = 0 + $(generate_code(machine, actions)) + (headers, reshape(fields, length(headers), :)) end -``` -Automa.jl has four kinds of code generators. The first and default one uses two lookup tables to pick up the next state and the actions for the current state and input. The second one expands these lookup tables into a series of if-else branches. The third one is based on `@goto` jumps. The fourth one is identitical to the third one, except uses SIMD operations where applicable. These two code generators are named as `:table`, and `:goto`, respectively. To sepcify a code generator, you can pass the `code=:table|:goto` argument to `Automa.generate_exec_code`. The generated code size and its runtime speed highly depends on the machine and actions. However, as a rule of thumb, `:table` is simpler with smaller code, but is also slower. +header, data = parse_tsv("a\tabc\n12\t13\r\nxyc\tz\n\n") -Note that the `:goto` generator has more requirements than the `:table` generator: -* First, `boundscheck=false` must be set -* Second, `getbyte` must be the default `Base.getindex` +# output +(["a", "abc"], ["12" "xyc"; "13" "z"]) +``` diff --git a/docs/src/io.md b/docs/src/io.md new file mode 100644 index 00000000..d11801d1 --- /dev/null +++ b/docs/src/io.md @@ -0,0 +1,309 @@ +```@meta +CurrentModule = Automa +DocTestSetup = quote + using TranscodingStreams + using Automa +end +``` + +# Parsing from an IO + +!!! note + Parsing from an IO relies on TranscodingStreams.jl, and the relevant methods are defined in an extension module in Automa. + If you use Julia 1.9 or later, you must load TranscodingStreams before loading Automa to test this functionality. + +Some file types are gigabytes or tens of gigabytes in size. +For these files, parsing from a buffer may be impractical, as they require you to read in the entire file in memory at once. +Automa enables this by hooking into `TranscodingStreams.jl`, a package that provides a wrapper IO of the type `TranscodingStream`. +Importantly, these streams buffer their input data. +Automa is thus able to operate directly on the input buffers of `TranscodingStream` objects. + +Unfortunately, this _significantly_ complicates things compared to parsing from a simple buffer. +The main problem is that, when reading from a buffered stream, the byte array visible from `Automa` is only a small slice of the total input data. +Worse, when the end of the stream is reached, data from the buffer is flushed, i.e. removed from the stream. +To handle this, Automa must reach deep into the implementation details of `TranscodingStreams`, and also break some of its own abstractions. +It's not pretty, but it's what we have. + +Practically speaking, parsing from IO is done with the function `Automa.generate_reader`. +Despite its name, this function is NOT directly used to generate objects like `FASTA.Reader`. +Instead, this function produces Julia code (an `Expr` object) that, when evaluated, defines a function that can execute an Automa machine on an IO. +Let me first show the code generated by `generate_reader` in pseudocode format: + +``` +function { function name }(stream::TranscodingStream, { args... }) + { init code } + + @label __exec__ + + p = current buffer position + p_end = final buffer position + + # the eof call below will first flush any used data from buffer, + # then load in new data, before checking if it's really eof. + is_eof = eof(stream) + execute normal automa parsing of the buffer + update buffer position to match p + + { loop code } + + if cs < 0 # meaning: erroneous input or erroneous EOF + { error code } + end + + if machine errored or reached EOF + @label __return__ + { return code } + end + @goto __exec__ +end +``` + +The content marked `{ function name }`, `{ args... }`, `{ init code }`, `{ loop code }`, `{ error code }` and `{ return code }` are arguments provided to `Automa.generate_reader`. +By providing these, the user can customize the generated function further. + +The main difference from the code generated to parse a buffer is the label/GOTO pair `__exec__`, which causes Automa to repeatedly load data into the buffer, execute the machine, then flush used data from the buffer, then execute the machine, and so on, until interrupted. + +Importantly, when parsing from a buffer, `p` and `p_end` refer to the position _in the current buffer_. +This may not be the position in the stream, and when the data in the buffer is flushed, it may move the data in the buffer so that `p` now become invalid. +This means you can't simply store a variable `marked_pos` that points to the current value of `p` and expect that the same data is at that position later. +Furthermore, `is_eof` is set to whether the stream has reached EOF. + +## Example use +Let's show the simplest possible example of such a function. +We have a `Machine` (which, recall, is a compiled regex) called `machine`, and we want to make a function that returns `true` if a given `IO` contain data that conforms to the regex format specified by the `Machine`. + +We will still use the machine from before, just without any actions: + +```jldoctest io1; output = false +machine = let + header = re"[a-z]+" + seqline = re"[ACGT]+" + record = re">" * header * '\n' * rep1(seqline * '\n') + compile(rep(record)) +end +@assert machine isa Automa.Machine + +# output + +``` + +To create our simple IO reader, we simply need to call `generate_reader`, where the `{ return code }` is a check if `iszero(cs)`, meaning if the machine exited at a proper exit state. +We also need to set `error_code` to an empty expression in order to prevent throwing an error on invalid code. Instead, we want it to go immediately to return - we call this section `__return__`, so we need to `@goto __return__`. +Then, we need to evaluate the code created by `generate_reader` in order to define the function `validate_fasta` + +```jldoctest io1 +julia> return_code = :(iszero(cs)); + +julia> error_code = :(@goto __return__); + +julia> eval(generate_reader(:validate_fasta, machine; returncode=return_code, errorcode=error_code)); +``` + +The generated function `validate_fasta` has the function signature: +`validate_fasta(stream::TranscodingStream)`. +If our input IO is not a `TranscodingStream`, we can wrap it in the relatively lightweight `NoopStream`, which, as the name suggests, does nothing to the data: + +```jldoctest io1 +julia> io = NoopStream(IOBuffer(">a\nTAG\nTA\n>bac\nG\n")); + +julia> validate_fasta(io) +true + +julia> validate_fasta(NoopStream(IOBuffer("random data"))) +false +``` + +## Reading a single record + +!!! danger + The following code is only for demonstration purposes. + It has several one important flaw, which will be adressed in a later section, so do not copy-paste it for serious work. + +There are a few more subtleties related to the `generate_reader` function. +Suppose we instead want to create a function that reads a single FASTA record from an IO. +In this case, it's no good that the function created from `generate_reader` will loop until the IO reaches EOF - we need to find a way to stop it after reading a single record. +We can do this with the pseudomacro `@escape`, as shown below. + +We will reuse our `Seq` struct and our `Machine` from the "parsing from a buffer" section of this tutorial: + +```jldoctest io2; output = false +struct Seq + name::String + seq::String +end + +machine = let + header = onexit!(onenter!(re"[a-z]+", :mark_pos), :header) + seqline = onexit!(onenter!(re"[ACGT]+", :mark_pos), :seqline) + record = onexit!(re">" * header * '\n' * rep1(seqline * '\n'), :record) + compile(rep(record)) +end +@assert machine isa Automa.Machine + +# output +``` + +The code below contains `@escape` in the `:record` action - meaning: Break out of machine execution. + +```jldoctest io2; output = false +actions = Dict{Symbol, Expr}( + :mark_pos => :(pos = p), + :header => :(header = String(data[pos:p-1])), + :seqline => :(append!(seqbuffer, data[pos:p-1])), + + # Only this action is different from before! + :record => quote + seq = Seq(header, String(seqbuffer)) + found_sequence = true + # Reset p one byte if we're not at the end + p -= !(is_eof && p > p_end) + @escape + end +) +@assert actions isa Dict + +# output +``` + +`@escape` is not actually a real macro, but what Automa calls a "pseudomacro". +It is expanded during Automa's own compiler pass _before_ Julia's lowering. +The `@escape` pseudomacro is replaced with code that breaks it out of the executing machine, without reaching EOF or an invalid byte. + +Let's see how I use `generate_reader`, then I will explain each part: + +```jldoctest io2; output = false +generate_reader( + :read_record, + machine; + actions=actions, + initcode=quote + seqbuffer = UInt8[] + pos = 0 + found_sequence = false + header = "" + end, + loopcode=quote + if (is_eof && p > p_end) || found_sequence + @goto __return__ + end + end, + returncode=:(found_sequence ? seq : nothing) +) |> eval + +# output +read_record (generic function with 1 method) +``` + +In the `:record`, action, a few new things happen. +* First, I set the flag `found_sequence = false`. + In the loop code, I look for this flag to signal that the function should return. + Remember, the loop code happens after machine execution, which can mean either that the execution was broken out of by `@escape`, or than the buffer ran out and need to be refilled. + I could just return the sequence directly in the action, but then I would skip a bunch of the code generated by `generate_reader` which sets the buffer state correctly, _so this is never adviced_. + Instead, in the _loop code_, which executes after the buffer has been flushed, I check for this flag, and goes to `__return__` if necessary. + I could also just return directly in the loopcode, but I prefer only having one place to retun from the function. +* I use `@escape` to break out of the machine, i.e. stop machine execution +* Finally, I decrement `p`, if and only if the machine has not reached EOF (which happens when `is_eof` is true, meaning the last part of the IO has been buffered, and `p > p_end`, meaning the end of the buffer has been reached). + This is because, the first record ends when the IO reads the second `>` symbol. + If I then were to read another record from the same IO, I would have already read the `>` symbol. + I need to reset `p` by 1, so the `>` is also read on the next call to `read_record`. + +I can use the function like this: + +```jldoctest io2 +julia> io = NoopStream(IOBuffer(">a\nT\n>tag\nGAGA\nTATA\n")); + +julia> read_record(io) +Seq("a", "T") + +julia> read_record(io) +Seq("tag", "GAGATATA") + +julia> read_record(io) +``` + +## Preserving data by marking the buffer +There are several problems with the implementation above: The following code in my actions dict: + +```julia +header = String(data[pos:p-1]) +``` + +Creates `header` by accessing the data buffer. +However, when reading an IO, how can I know that the data hasn't shifted around in the buffer between when I defined `pos`? +For example, suppose we have a short buffer of only 8 bytes, and the following FASTA file: `>abcdefghijkl\nA`. +Then, the buffer is first filled with `>abcdefg`. +When entering the header, I execute the action `:mark_position` at `p = 2`, so `pos = 2`. +But now, when I reach the end of the header, the used data in the buffer has been flushed, and the data is now: +`hijkl\nA`, and `p = 14`. +I then try to access `data[2:13]`, which is out of bounds! + +Luckily, the buffers of `TranscodingStreams` allow us to "mark" a position to save it. +The buffer will not flush the marked position, or any position after the marked position. +If necessary, it will resize the buffer to be able to load more data while keeping the marked position. + +Inside the function generated by `generate_reader`, we can use the zero-argument pseudomacro `@mark()`, which marks the position `p`. +The macro `@markpos()` can then be used to get the marked position, which will point to the same data in the buffer, even after the data in the buffer has been shifted after it's been flushed. +This works because the mark is stored inside the `TranscodingStream` buffer, and the buffer makes sure to update the mark if the content moves. +Hence, we can re-write the actions: + +```julia +actions = Dict{Symbol, Expr}( + :mark_position => :(@mark), + :header => :(header = String(data[@markpos():p-1])), + :seqline => :(append!(buffer, data[@markpos():p-1])), + + [:record action omitted...] +) +``` + +In our example above with the small 8-byte buffer, this is what would happen: +First, the buffer contains the first 8 bytes. +When `p = 2`, the mark is set, and the second byte is marked:: + +``` +content: >abcdefg +mark: ^ +p = 2 ^ +``` + +Then, when `p = 9` the buffer is exhausted, the used data is removed, BUT, the mark stays, so byte 2 is preserved, and only the first byte is removed. +The code in `generate_reader` loops around to `@label __exec__`, which sets p to the current buffer position. +The buffer now looks like this: + +``` +content: abcdefgh +mark: ^ +p = 8 ^ +``` + +Only 1 byte was cleared, so when `p = 9`, the buffer will be exhausted again. +This time, no data can be cleared, so instead, the buffer is resized to fit more data: + +``` +content: abcdefghijkl\nA +mark: ^ +p = 9 ^ +``` + +Finally, when we reach the newline `p = 13`, the whole header is in the buffer, and so `data[@markpos():p-1]` will correctly refer to the header (now, `1:12`). + +``` +content: abcdefghijkl\nA +mark: ^ +p = 13 ^ +``` + +Remember to update the mark, or to clear it with `@unmark()` in order to be able to flush data from the buffer afterwards. + +## Reference +```@docs +Automa.generate_reader +Automa.@escape +Automa.@mark +Automa.@unmark +Automa.@markpos +Automa.@bufferpos +Automa.@relpos +Automa.@abspos +Automa.@setbuffer +``` diff --git a/docs/src/parser.md b/docs/src/parser.md new file mode 100644 index 00000000..73e7f901 --- /dev/null +++ b/docs/src/parser.md @@ -0,0 +1,210 @@ +```@meta +CurrentModule = Automa +DocTestSetup = quote + using TranscodingStreams + using Automa +end +``` + +# Parsing from a buffer +Automa can leverage metaprogramming to combine regex and julia code to create parsers. +This is significantly more difficult than simply using validators or tokenizers, but still simpler than parsing from an IO. +Currently, Automa loads data through pointers, and therefore needs data backed by `Array{UInt8}` or `String` or similar - it does not work with types such as `UnitRange{UInt8}`. +Furthermore, be careful about passing strided views to Automa - while Automa can extract a pointer from a strided view, it will always advance the pointer one byte at a time, disregarding the view's stride. + +As an example, let's use the simplified FASTA format intoduced in the regex section, with the following format: `re"(>[a-z]+\n([ACGT]+\n)+)*"`. +We want to parse it into a `Vector{Seq}`, where `Seq` is defined as: + +```jldoctest parse1 +julia> struct Seq + name::String + seq::String + end +``` + +## Adding actions to regex +To do this, we need to inject Julia code into the regex validator while it is running. +The first step is to add _actions_ to our regex: These are simply names of Julia expressions to splice in, +where the expressions will be executed when the regex is matched. +We can choose the names arbitrarily. + +Currently, actions can be added in the following places in a regex: +* With `onenter!`, meaning it will be executed when reading the first byte of the regex +* With `onfinal!`, where it will be executed when reading the last byte of the regex. + Note that it's not possible to determine the final byte for some regex like `re"X+"`, since + the machine reads only 1 byte at a time and cannot look ahead. +* With `onexit!`, meaning it will be executed on reading the first byte AFTER the regex, + or when exiting the regex by encountering the end of inputs (only for a regex match, not an unexpected end of input) +* With `onall!`, where it will be executed when reading every byte that is part of the regex. + +You can set the actions to be a single action name (represented by a `Symbol`), or a list of action names: +```jldoctest +julia> my_regex = re"ABC"; + +julia> onenter!(my_regex, [:action_a, :action_b]); + +julia> onexit!(my_regex, :action_c); +``` + +In which case the code named `action_a`, then that named `action_b` will executed in order when entering the regex, and the code named `action_c` will be executed when exiting the regex. + +The `onenter!` etc functions return the regex they modify, so the above can be written: +```jldoctest +julia> my_regex = onexit!(onenter!(re"ABC", [:action_a, :action_b]), :action_c); + +julia> my_regex isa RE +true +``` + +When the the following regex' actions are visualized in its corresponding [DFA](theory.md#deterministic-finite-automata): + +```julia +regex = + ab = re"ab*" + onenter!(ab, :enter_ab) + onexit!(ab, :exit_ab) + onfinal!(ab, :final_ab) + onall!(ab, :all_ab) + c = re"c" + onenter!(c, :enter_c) + onexit!(c, :exit_c) + onfinal!(c, :final_c) + + ab * c +end +``` + +The result DFA looks below. +Here, the edge labeled `'a'/enter_ab,all_ab,final_ab` means that the edge consumes input byte `'a'`, and executes the three actions `enter_ab`, `all_ab` and `final_ab`, in that order. + +![Visualization of regex with actions](figure/actions.png) + +## Compiling regex to `Machine`s +In order to create code, the regex must first be compiled to a `Machine`, which is a struct that represents an optimised DFA. +We can do that with `compile(regex)`. +Under the hood, this compiles the regex to an NFA, then compiles the NFA to a DFA, and then optimises the DFA to a `Machine` (see the section on [Automa theory](theory.md)). + +Normally, we don't care about the regex directly, but only want the `Machine`. +So, it is idiomatic to compile the regex in the same `let` statement it is being built in: + +```jldoctest; output = false +machine = let + header = re"[a-z]+" + seqline = re"[ACGT]+" + record = re">" * header * '\n' * rep1(seqline * '\n') + compile(rep(record)) +end +@assert machine isa Automa.Machine + +# output + +``` + +Note that, if this code is placed at top level in a package, the regex will be constructed and compiled to a `Machine` during package precompilation, which greatly helps load times. + +## Creating our parser +However, in this case, we don't just need a `Machine` with the regex, we need a `Machine` with the regex _containing the relevant actions_. +To parse a simplified FASTA file into a `Vector{Seq}`, I'm using these four actions: + +* When the machine enters into the header, or a sequence line, I want it to mark the position with where it entered into the regex. + The marked position will be used as the leftmost position where the header or sequence is extracted later. +* When exiting the header, I want to extract the bytes from the marked position in the action above, + to the last header byte (i.e. the byte before the current byte), and use these bytes as the sequence header +* When exiting a sequence line, I want to do the same: + Extract from the marked position to one before the current position, + but this time I want to append the current line to a buffer containing all the lines of the sequence +* When exiting a record, I want to construct a `Seq` object from the header bytes and the buffer with all the sequence lines, then push the `Seq` to the result, + +```jldoctest parse1 +julia> machine = let + header = onexit!(onenter!(re"[a-z]+", :mark_pos), :header) + seqline = onexit!(onenter!(re"[ACGT]+", :mark_pos), :seqline) + record = onexit!(re">" * header * '\n' * rep1(seqline * '\n'), :record) + compile(rep(record)) + end; +``` + +We can now write the code we want executed. +When writing this code, we want access to a few variables used by the machine simulation. +For example, we might want to know at which byte position the machine is when an action is executed. +Currently, the following variables are accessible in the code: + +* `byte`: The current input byte as a `UInt8` +* `p`: The 1-indexed position of `byte` in the buffer +* `p_end`: The length of the input buffer +* `is_eof`: Whether the machine has reached the end of the input. +* `cs`: The current state of the machine, as an integer +* `data`: The input buffer +* `mem`: The memory being read from, an `Automa.SizedMemory` object containing a pointer and a length + +The actions we want executed, we place in a `Dict{Symbol, Expr}`: +```jldoctest parse1 +julia> actions = Dict( + :mark_pos => :(pos = p), + :header => :(header = String(data[pos:p-1])), + :seqline => :(append!(buffer, data[pos:p-1])), + :record => :(push!(seqs, Seq(header, String(buffer)))) + ); +``` + +For multi-line `Expr`, you can construct them with `quote ... end` blocks. + +We can now construct a function that parses our data. +In the code written in the action dict above, besides the variables defined for us by Automa, +we also refer to the variables `buffer`, `header`, `pos` and `seqs`. +Some of these variables are defined in the code above (for example, in the `:(pos = p)` expression), +but we can't necessarily control the order in which Automa will insert these expressions into out final function. +Hence, let's initialize these variables at the top of the function we generate, such that we know for sure they are defined when used - whenever they are used. + +The code itself is generated using `generate_code`: + +```jldoctest parse1 +julia> @eval function parse_fasta(data) + pos = 0 + buffer = UInt8[] + seqs = Seq[] + header = "" + $(generate_code(machine, actions)) + return seqs + end +parse_fasta (generic function with 1 method) +``` + +We can now use it: +```jldoctest parse1 +julia> parse_fasta(">abc\nTAGA\nAAGA\n>header\nAAAG\nGGCG\n") +2-element Vector{Seq}: + Seq("abc", "TAGAAAGA") + Seq("header", "AAAGGGCG") +``` + +If we give out function a bad input - for example, if we forget the trailing newline, it throws an error: + +```jldoctest parse1 +julia> parse_fasta(">abc\nTAGA\nAAGA\n>header\nAAAG\nGGCG") +ERROR: Error during FSM execution at buffer position 33. +Last 32 bytes were: + +">abc\nTAGA\nAAGA\n>header\nAAAG\nGGCG" + +Observed input: EOF at state 5. Outgoing edges: + * '\n'/seqline + * [ACGT] + +Input is not in any outgoing edge, and machine therefore errored. +``` + +The code above parses with about 300 MB/s on my laptop. +Not bad, but Automa can do better - read on to learn how to customize codegen. + +## Reference +```@docs +Automa.onenter! +Automa.onexit! +Automa.onall! +Automa.onfinal! +Automa.precond! +Automa.generate_code +Automa.generate_init_code +Automa.generate_exec_code +``` diff --git a/docs/src/reader.md b/docs/src/reader.md new file mode 100644 index 00000000..6d208c41 --- /dev/null +++ b/docs/src/reader.md @@ -0,0 +1,123 @@ +```@meta +CurrentModule = Automa +DocTestSetup = quote + using TranscodingStreams + using Automa +end +``` + +# Creating a `Reader` type +The use of `generate_reader` as we learned in the previous section "Parsing from an io" has an issue we need to address: +While we were able to read multiple records from the reader by calling `read_record` multiple times, no state was preserved between these calls, and so, no state can be preserved between reading individual records. +This is also what made it necessary to clumsily reset `p` after emitting each record. + +Imagine you have a format with two kinds of records, A and B types. +A records must come before B records in the file. +Hence, while a B record can appear at any time, once you've seen a B record, there can't be any more A records. +When reading records from the file, you must be able to store whether you've seen a B record. + +We address this by creating a `Reader` type which wraps the IO being parsed, and which store any state we want to preserve between records. +Let's stick to our simplified FASTA format parsing sequences into `Seq` objects: + +```jldoctest reader1; output = false +struct Seq + name::String + seq::String +end + +machine = let + header = onexit!(onenter!(re"[a-z]+", :mark_pos), :header) + seqline = onexit!(onenter!(re"[ACGT]+", :mark_pos), :seqline) + record = onexit!(re">" * header * '\n' * rep1(seqline * '\n'), :record) + compile(rep(record)) +end +@assert machine isa Automa.Machine + +# output + +``` + +This time, we use the following `Reader` type: +```jldoctest reader1; output = false +mutable struct Reader{S <: TranscodingStream} + io::S + automa_state::Int +end + +Reader(io::TranscodingStream) = Reader{typeof(io)}(io, 1) +Reader(io::IO) = Reader(NoopStream(io)) + +# output +Reader +``` + +The `Reader` contains an instance of `TranscodingStream` to read from, and stores the Automa state between records. +The beginning state of Automa is always 1. +We can now create our reader function like below. +There are only three differences from the definitions in the previous section: +* I no longer have the code to decrement `p` in the `:record` action - because we can store the Automa state between records such that the machine can handle beginning in the middle of a record if necessary, there is no need to reset the value of `p` in order to restore the IO to the state right before each record. +* I return `(cs, state)` instead of just `state`, because I want to update the Automa state of the Reader, so when it reads the next record, it begins in the same state where the machine left off from the previous state +* In the arguments, I add `start_state`, and in the `initcode` I set `cs` to the start state, so the machine begins from the correct state + +```jldoctest reader1; output = false +actions = Dict{Symbol, Expr}( + :mark_pos => :(@mark), + :header => :(header = String(data[@markpos():p-1])), + :seqline => :(append!(seqbuffer, data[@markpos():p-1])), + :record => quote + seq = Seq(header, String(seqbuffer)) + found_sequence = true + @escape + end +) + +generate_reader( + :read_record, + machine; + actions=actions, + arguments=(:(start_state::Int),), + initcode=quote + seqbuffer = UInt8[] + found_sequence = false + header = "" + cs = start_state + end, + loopcode=quote + if (is_eof && p > p_end) || found_sequence + @goto __return__ + end + end, + returncode=:(found_sequence ? (cs, seq) : throw(EOFError())) +) |> eval + +# output +read_record (generic function with 1 method) +``` + +We then create a function that reads from the `Reader`, making sure to update the `automa_state` of the reader: + +```jldoctest reader1; output = false +function read_record(reader::Reader) + (cs, seq) = read_record(reader.io, reader.automa_state) + reader.automa_state = cs + return seq +end + +# output +read_record (generic function with 2 methods) +``` + +Let's test it out: + +```jldoctest reader1 +julia> reader = Reader(IOBuffer(">a\nT\n>tag\nGAG\nATATA\n")); + +julia> read_record(reader) +Seq("a", "T") + +julia> read_record(reader) +Seq("tag", "GAGATATA") + +julia> read_record(reader) +ERROR: EOFError: read end of file +``` \ No newline at end of file diff --git a/docs/src/references.md b/docs/src/references.md deleted file mode 100644 index 7ddb5a75..00000000 --- a/docs/src/references.md +++ /dev/null @@ -1,22 +0,0 @@ -References -========== - -Data ----- - -```@docs -Automa.SizedMemory -Automa.pointerend -Automa.pointerstart -``` - -Code generator --------------- - -```@docs -Automa.Variables -Automa.CodeGenContext -Automa.generate_code -Automa.generate_init_code -Automa.generate_exec_code -``` diff --git a/docs/src/regex.md b/docs/src/regex.md new file mode 100644 index 00000000..d2810a90 --- /dev/null +++ b/docs/src/regex.md @@ -0,0 +1,72 @@ +```@meta +CurrentModule = Automa +DocTestSetup = quote + using TranscodingStreams + using Automa +end +``` + +# Regex +Automa regex (of the type `Automa.RE`) are conceptually similar to the Julia built-in regex. +They are made using the `@re_str` macro, like this: `re"ABC[DEF]"`. + +Automa regex matches individual bytes, not characters. Hence, `re"Æ"` (with the UTF-8 encoding `[0xc3, 0x86]`) is equivalent to `re"\xc3\x86"`, and is considered the concatenation of two independent input bytes. + +The `@re_str` macro supports the following content: +* Literal symbols, such as `re"ABC"`, `re"\xfe\xa2"` or `re"Ø"` +* `|` for alternation, as in `re"A|B"`, meaning "`A` or `B`". +* Byte sets with `[]`, like `re"[ABC]"`. + This means any of the bytes in the brackets, e.g. `re"[ABC]"` is equivalent to `re"A|B|C"`. +* Inverted byte sets, e.g. `re"[^ABC]"`, meaning any byte, except those in `re[ABC]`. +* Repetition, with `X*` meaning zero or more repetitions of X +* `+`, where `X+` means `XX*`, i.e. 1 or more repetitions of X +* `?`, where `X?` means `X | ""`, i.e. 0 or 1 occurrences of X. It applies to the last element of the regex +* Parentheses to group expressions, like in `A(B|C)?` + +You can combine regex with the following operations: +* `*` for concatenation, with `re"A" * re"B"` being the same as `re"AB"`. + Regex can also be concatenated with `Char`s and `String`s, which will cause the chars/strings to be converted to regex first. +* `|` for alternation, with `re"A" | re"B"` being the same as `re"A|B"` +* `&` for intersection of regex, i.e. for regex `A` and `B`, the set of inputs matching `A & B` is exactly the intersection of the inputs match `A` and those matching `B`. + As an example, `re"A[AB]C+D?" & re"[ABC]+"` is `re"ABC"`. +* `\` for difference, such that for regex `A` and `B`, `A \ B` creates a new regex matching all those inputs that match `A` but not `B`. +* `!` for inversion, such that `!re"[A-Z]"` matches all other strings than those which match `re"[A-Z]"`. + Note that `!re"a"` also matches e.g. `"aa"`, since this does not match `re"a"`. + +Finally, the funtions `opt`, `rep` and `rep1` is equivalent to the operators `?`, `*` and `+`, so i.e. `opt(re"a" * rep(re"b") * re"c")` is equivalent to `re"(ab*c)?"`. + +## Example +Suppose we want to create a regex that matches a simplified version of the FASTA format. +This "simple FASTA" format is defined like so: + +* The format is a series of zero or more _records_, concatenated +* A _record_ consists of the concatenation of: + - A leading '>' + - A header, composed of one or more letters in 'a-z', + - A newline symbol '\n' + - A series of one or more _sequence lines_ +* A _sequence line_ is the concatenation of: + - One or more symbols from the alphabet [ACGT] + - A newline + +We can represent this concisely as a regex: `re"(>[a-z]+\n([ACGT]+\n)+)*"` +To make it easier to read, we typically construct regex incrementally, like such: + +```jldoctest; output = false +fasta_regex = let + header = re"[a-z]+" + seqline = re"[ACGT]+" + record = '>' * header * '\n' * rep1(seqline * '\n') + rep(record) +end +@assert fasta_regex isa RE + +# output + +``` + +## Reference +```@docs +RE +@re_str +``` diff --git a/docs/src/theory.md b/docs/src/theory.md new file mode 100644 index 00000000..31964c58 --- /dev/null +++ b/docs/src/theory.md @@ -0,0 +1,142 @@ +# Theory of regular expressions +Most programmers are familiar with _regular expressions_, or _regex_, for short. +What many programmers don't know is that regex have a deep theoretical underpinning, which is leaned on by regex engines to produce highly efficient code. + +Informally, a regular expression can be thought of as any pattern that can be constructed from the following atoms: +* The empty string is a valid regular expression, i.e. `re""` +* Literal matching of a single symbol from a finite alphabet, such as a character, i.e. `re"p"` + +Atoms can be combined with the following operations, if R and P are two regular expressions: +* Alternation, i.e `R | P`, meaning either match R or P. +* Concatenation, i.e. `R * P`, meaning match first R, then P +* Repetition, i.e. `R*`, meaning match R zero or more times consecutively. + +!!! note + In Automa, the alphabet is _bytes_, i.e. `0x00:0xff`, and so each symbol is a single byte. + Multi-byte characters such as `Æ` is interpreted as the two concatenated of two symbols, + `re"\xc3" * re"\x86"`. + The fact that Automa considers one input to be one byte, not one character, can become relevant if you instruct Automa to complete an action "on every input". + +Popular regex libraries include more operations like `?` and `+`. +These can trivially be constructed from the above mentioned primitives, +i.e. `R?` is `"" | R`, +and `R+` is `RR*`. + +Some implementations of regular expression engines, such as PCRE which is the default in Julia as of Julia 1.8, +also support operations like backreferences and lookbehind. +These operations can NOT be constructed from the above atoms and axioms, meaning that PCRE expressions are not regular expressions in the theoretical sense. + +The practical importance of theoretically sound regular expressions is that there exists algorithms that can match regular expressions on O(N) time and O(1) space, +whereas this is not true for PCRE expressions, which are therefore significantly slower. + +!!! note + Automa.jl only supports real regex, and as such does not support e.g. backreferences, + in order to gurantee fast runtime performance. + +To match regex to strings, the regex are transformed to _finite automata_, which are then implemented in code. + +## Nondeterministic finite automata +The programmer Ken Thompson, of Unix fame, deviced _Thompson's construction_, an algorithm to constuct a nondeterministic finite automaton (NFA) from a regex. +An NFA can be thought of as a flowchart (or a directed graph), where one can move from node to node on directed edges. +Edges are either labeled `ϵ`, in which the machine can freely move through the edge to its destination node, +or labeled with one or more input symbols, in which the machine may traverse the edge upon consuming said input. + +To illustrate, let's look at one of the simplest regex: `re"a"`, matching the letter `a`: + +![](figure/simple.png) + +You begin at the small dot on the right, then immediately go to state 1, the cirle marked by a `1`. +By moving to the next state, state 2, you consume the next symbol from the input string, which must be the symbol marked on the edge from state 1 to state 2 (in this case, an `a`). +Some states are "accept states", illustrated by a double cirle. If you are at an accept state when you've consumed all symbols of the input string, the string matches the regex. + +Each of the operaitons that combine regex can also combine NFAs. +For example, given the two regex `a` and `b`, which correspond to the NFAs `A` and `B`, the regex `a * b` can be expressed with the following NFA: + +![](figure/cat.png) + +Note the `ϵ` symbol on the edge - this signifies an "epsilon transition", meaning you move directly from `A` to `B` without consuming any symbols. + +Similarly, `a | b` correspond to this NFA structure... + +![](figure/alt.png) + +...and `a*` to this: + +![](figure/kleenestar.png) + +For a larger example, `re"(\+|-)?(0|1)*"` combines alternation, concatenation and repetition and so looks like this: + +![](figure/larger.png) + +ϵ-transitions means that there are states from which there are multiple possible next states, e.g. in the larger example above, state 1 can lead to state 2 or state 12. +That's what makes NFAs nondeterministic. + +In order to match a regex to a string then, the movement through the NFA must be emulated. +You begin at state 1. When a non-ϵ edge is encountered, you consume a byte of the input data if it matches. +If there are no edges that match your input, the string does not match. +If an ϵ-edge is encountered from state `A` that leads to states `B` and `C`, the machine goes from state `A` to state `{B, C}`, i.e. in both states at once. + +For example, if the regex `re"(\+|-)?(0|1)*` visualized above is matched to the string `-11`, this is what happens: +* NFA starts in state 1 +* NFA immediately moves to all states reachable via ϵ transition. It is now in state {5, 7, 10, 13, 16}. +* NFA sees input `-`. States {5, 7, 10, 16} do not have an edge with `-` leading out, so these states die. + Therefore, the machine is in state 13, consumes the input, and moves to state 14. +* NFA immediately moves to all states reachable from state 14 via ϵ transitions, so goes to {5, 7, 10} +* NFA sees input `1`, must be in state 7, moves to state 8, then through ϵ transitions to state {5, 7, 10} +* The above point repeats, NFA is still in state {5, 7, 10} +* Input ends. Since state 5 is an accept state, the string matches. + +Using only a regex-to-NFA converter, you could create a simple regex engine simply by emulating the NFA as above. +The existence of ϵ transitions means the NFA can be in multiple states at once which adds unwelcome complexity to the emulation and makes it slower. +Luckily, every NFA has an equivalent _determinisitic finite automaton_, which can be constructed from the NFA using the so-called _powerset construction_. + +## Deterministic finite automata +Or DFAs, as they are called, are similar to NFAs, but do not contain ϵ-edges. +This means that a given input string has either zero paths (if it does not match the regex), one, unambiguous path, through the DFA. +In other words, every input symbol _must_ trigger one unambiguous state transition from one state to one other state. + +Let's visualize the DFA equivalent to the larger NFA above: + +![](figure/large_dfa.png) + +It might not be obvious, but the DFA above accepts exactly the same inputs as the previous NFA. +DFAs are way simpler to simulate in code than NFAs, precisely because at every state, for every input, there is exactly one action. +DFAs can be simulated either using a lookup table, of possible state transitions, +or by hardcoding GOTO-statements from node to node when the correct input is matched. +Code simulating DFAs can be ridicuously fast, with each state transition taking less than 1 nanosecond, if implemented well. + +Furthermore, DFAs can be optimised. +Two edges between the same nodes with labels `A` and `B` can be collapsed to a single edge with labels `[AB]`, and redundant nodes can be collapsed. +The optimised DFA equivalent to the one above is simply: + +![](figure/large_machine.png) + +Unfortunately, as the name "powerset construction" hints, convering an NFA with N nodes may result in a DFA with up to 2^N nodes. +This inconvenient fact drives important design decisions in regex implementations. +There are basically two approaches: + +Automa.jl will just construct the DFA directly, and accept a worst-case complexity of O(2^N). +This is acceptable (I think) for Automa, because this construction happens in Julia's package precompilation stage (not on package loading or usage), +and because the DFAs are assumed to be constants within a package. +So, if a developer accidentally writes an NFA which is unacceptably slow to convert to a DFA, it will be caught in development. +Luckily, it's pretty rare to have NFAs that result in truly abysmally slow conversions to DFA's: +While bad corner cases exist, they are rarely as catastrophic as the O(2^N) would suggest. +Currently, Automa's regex/NFA/DFA compilation pipeline is very slow and unoptimized, but, since it happens during precompile time, it is insignificant compared to LLVM compile times. + +Other implementations, like the popular `ripgrep` command line tool, uses an adaptive approach. +It constructs the DFA on the fly, as each symbol is being matched, and then caches the DFA. +If the DFA size grows too large, the cache is flushed. +If the cache is flushed too often, it falls back to simulating the NFA directly. +Such an approach is necessary for `ripgrep`, because the regex -> NFA -> DFA compilation happens at runtime and must be near-instantaneous, unlike Automa, where it happens during package precompilation and can afford to be slow. + +## Automa in a nutshell +Automa simulates the DFA by having the DFA create a Julia Expr, which is then used to generate a Julia function using metaprogramming. +Like all other Julia code, this function is then optimized by Julia and then LLVM, making the DFA simulations very fast. + +Because Automa just constructs Julia functions, we can do extra tricks that ordinary regex engines cannot: +We can splice arbitrary Julia code into the DFA simulation. +Currently, Automa supports two such kinds of code: _actions_, and _preconditions_. + +Actions are Julia code that is executed during certain state transitions. +Preconditions are Julia code, that evaluates to a `Bool` value, and which is checked before a state transition. +If it evaluates to `false`, the transition is not taken. diff --git a/docs/src/tokenizer.md b/docs/src/tokenizer.md new file mode 100644 index 00000000..fe04f2d6 --- /dev/null +++ b/docs/src/tokenizer.md @@ -0,0 +1,145 @@ +```@meta +CurrentModule = Automa +DocTestSetup = quote + using TranscodingStreams + using Automa +end +``` + +# Tokenizers (lexers) +A _tokenizer_ or a _lexer_ is a program that breaks down an input text into smaller chunks, and classifies them as one of several _tokens_. +For example, consider an imagininary format that only consists of nested tuples of strings containing letters, like this: + +``` +(("ABC", "v"),(("x", ("pj",(("a", "k")), ("L"))))) +``` + +Any text of this format can be broken down into a sequence of the following tokens: +* Left parenthesis: `re"\("` +* Right parenthesis: `re"\)"` +* Comma: `re","` +* Quote: `re"\""` +* Spaces: `re" +"` +* Letters: `re"[A-Za-z]+"` + +Such that e.g. `("XY", "A")` can be represented as `lparent, quote, XY, quote, comma, space, quote A quote rparens`. + +Breaking the text down to its tokens is called tokenization or lexing. Note that lexing in itself is not sufficient to parse the format: Lexing is _context unaware_, so e.g. the test `"((A` can be perfectly well tokenized to `quote lparens lparens A`, even if it's invalid. + +The purpose of tokenization is to make subsequent parsing easier, because each part of the text has been classified. That makes it easier to, for example, to search for letters in the input. Instead of having to muck around with regex to find the letters, you use regex once to classify all text. + +## Making and using a tokenizer +Let's use the example above to create a tokenizer. +The most basic default tokenizer uses `UInt32` as tokens: You pass in a list of regex matching each token, then evaluate the resulting code: + +```jldoctest tok1 +julia> make_tokenizer( + [re"\(", re"\)", re",", re"\"", re" +", re"[a-zA-Z]+"] + ) |> eval +``` + +Since the default tokenizer uses `UInt32` as tokens, you can then obtain a lazy iterator of tokens by calling `tokenize(UInt32, data)`: + +```jldoctest tok1 +julia> iterator = tokenize(UInt32, """("XY", "A")"""); typeof(iterator) +Tokenizer{UInt32, String, 1} +``` + +This will return `Tuple{Int64, Int32, UInt32}` elements, with each element being: +* The start index of the token +* The length of the token +* The token itself, in this example `UInt32(1)` for '(', `UInt32(2)` for ')' etc: + +```jldoctest tok1 +julia> collect(iterator) +10-element Vector{Tuple{Int64, Int32, UInt32}}: + (1, 1, 0x00000001) + (2, 1, 0x00000004) + (3, 2, 0x00000006) + (5, 1, 0x00000004) + (6, 1, 0x00000003) + (7, 1, 0x00000005) + (8, 1, 0x00000004) + (9, 1, 0x00000006) + (10, 1, 0x00000004) + (11, 1, 0x00000002) +``` + +Any data which could not be tokenized is given the error token `UInt32(0)`: +```jldoctest tok1 +julia> collect(tokenize(UInt32, "XY!!)")) +3-element Vector{Tuple{Int64, Int32, UInt32}}: + (1, 2, 0x00000006) + (3, 2, 0x00000000) + (5, 1, 0x00000002) +``` + +Both `tokenize` and `make_tokenizer` takes an optional argument `version`, which is `1` by default. +This sets the last parameter of the `Tokenizer` struct, and as such allows you to create multiple different tokenizers with the same element type. + +## Using enums as tokens +Using `UInt32` as tokens is not very convenient - so it's possible to use enums to create the tokenizer: + +```jldoctest tok2 +julia> @enum Token error lparens rparens comma quot space letters + +julia> make_tokenizer((error, [ + lparens => re"\(", + rparens => re"\)", + comma => re",", + quot => re"\"", + space => re" +", + letters => re"[a-zA-Z]+" + ])) |> eval + +julia> collect(tokenize(Token, "XY!!)")) +3-element Vector{Tuple{Int64, Int32, Token}}: + (1, 2, letters) + (3, 2, error) + (5, 1, rparens) +``` + +To make it even easier, you can define the enum and the tokenizer in one go: +```jldoctest; output = false +tokens = [ + :lparens => re"\(", + :rparens => re"\)", + :comma => re",", + :quot => re"\"", + :space => re" +", + :letters => re"[a-zA-Z]+" +] +@eval @enum Token error $(first.(tokens)...) +make_tokenizer((error, + [Token(i) => j for (i,j) in enumerate(last.(tokens))] +)) |> eval + +# output + +``` + +## Token disambiguation +It's possible to create a tokenizer where the different token regexes overlap: +```jldoctest +julia> make_tokenizer([re"[ab]+", re"ab*", re"ab"]) |> eval +``` + +In this case, an input like `ab` will match all three regex. +Which tokens are emitted is determined by two rules: + +First, the emitted tokens will be as long as possible. +So, the input `aa` could be emitted as one token of the regex `re"[ab]+"`, two tokens of the same regex, or of two tokens of the regex `re"ab*"`. +In this case, it will be emitted as a single token of `re"[ab]+"`, since that will make the first token as long as possible (2 bytes), whereas the other options would only make it 1 byte long. + +Second, tokens with a higher index in the input array beats previous tokens. +So, `a` will be emitted as `re"ab*"`, as its index of 2 beats the previous regex `re"[ab]+"` with the index 1, and `ab` will match the third regex. + +If you don't want emitted tokens to depend on these priority rules, you can set the optional keyword `unambiguous=true` in the `make_tokenizer` function, in which case `make_tokenizer` will error if any input text could be broken down into different tokens. +However, note that this may cause most tokenizers to error when being built, as most tokenization processes are ambiguous. + +## Reference +```@docs +Automa.Tokenizer +Automa.tokenize +Automa.make_tokenizer +``` \ No newline at end of file diff --git a/docs/src/validators.md b/docs/src/validators.md new file mode 100644 index 00000000..02875771 --- /dev/null +++ b/docs/src/validators.md @@ -0,0 +1,76 @@ +```@meta +CurrentModule = Automa +DocTestSetup = quote + using TranscodingStreams + using Automa +end +``` + +# Text validators +The simplest use of Automa is to simply match a regex. +It's unlikely you are going to want to use Automa for this instead of Julia's built-in regex engine PCRE, unless you need the extra performance that Automa brings over PCRE. +Nonetheless, it serves as a good starting point to introduce Automa. + +Suppose we have the FASTA regex from the regex page: + +```jldoctest val1 +julia> fasta_regex = let + header = re"[a-z]+" + seqline = re"[ACGT]+" + record = '>' * header * '\n' * rep1(seqline * '\n') + rep(record) + end; +``` + +## Buffer validator +Automa comes with a convenience function `generate_buffer_validator`: + +Given a regex (`RE`) like the one above, we can do: + +```jldoctest val1 +julia> eval(generate_buffer_validator(:validate_fasta, fasta_regex)); + +julia> validate_fasta +validate_fasta (generic function with 1 method) +``` + +And we now have a function that checks if some data matches the regex: +```jldoctest val1 +julia> validate_fasta(">hello\nTAGAGA\nTAGAG") # missing trailing newline +0 + +julia> validate_fasta(">helloXXX") # Error at byte index 7 +7 + +julia> validate_fasta(">hello\nTAGAGA\nTAGAG\n") # nothing; it matches +``` + +## IO validators +For large files, having to read the data into a buffer to validate it may not be possible. +When the package `TranscodingStreams` is loaded, Automa also supports creating IO validators with the `generate_io_validator` function: + +This works very similar to `generate_buffer_validator`, but the generated function takes an `IO`, and has a different return value: +* If the data matches, still return `nothing` +* Else, return (byte, (line, column)) where byte is the first errant byte, and (line, column) the position of the byte. If the errant byte is a newline, column is 0. If the input reaches unexpected EOF, byte is `nothing`, and (line, column) points to the last line/column in the IO: + +```julia val1 +julia> eval(generate_io_validator(:validate_io, fasta_regex)); + +julia> validate_io(IOBuffer(">hello\nTAGAGA\n")) + +julia> validate_io(IOBuffer(">helX")) +(0x58, (1, 5)) + +julia> validate_io(IOBuffer(">hello\n\n")) +(0x0a, (3, 0)) + +julia> validate_io(IOBuffer(">hello\nAC")) +(nothing, (2, 2)) +``` + +## Reference +```@docs +Automa.generate_buffer_validator +Automa.generate_io_validator +Automa.compile +``` \ No newline at end of file diff --git a/ext/AutomaStream.jl b/ext/AutomaStream.jl index fddefdea..213dac1f 100644 --- a/ext/AutomaStream.jl +++ b/ext/AutomaStream.jl @@ -3,31 +3,6 @@ module AutomaStream using Automa: Automa using TranscodingStreams: TranscodingStream, NoopStream -""" - generate_reader(funcname::Symbol, machine::Automa.Machine; kwargs...) - -Generate a streaming reader function of the name `funcname` from `machine`. - -The generated function consumes data from a stream passed as the first argument -and executes the machine with filling the data buffer. - -This function returns an expression object of the generated function. The user -need to evaluate it in a module in which the generated function is needed. - -# Keyword Arguments -- `arguments`: Additional arguments `funcname` will take (default: `()`). - The default signature of the generated function is `(stream::TranscodingStream,)`, - but it is possible to supply more arguments to the signature with this keyword argument. -- `context`: Automa's codegenerator (default: `Automa.CodeGenContext()`). -- `actions`: A dictionary of action code (default: `Dict{Symbol,Expr}()`). -- `initcode`: Initialization code (default: `:()`). -- `loopcode`: Loop code (default: `:()`). -- `returncode`: Return code (default: `:(return cs)`). -- `errorcode`: Executed if `cs < 0` after `loopcode` (default error message) - -See the source code of this function to see how the generated code looks like -``` -""" function Automa.generate_reader( funcname::Symbol, machine::Automa.Machine; @@ -103,20 +78,6 @@ function Automa.generate_reader( return functioncode end -""" - generate_io_validator(funcname::Symbol, regex::RE; goto::Bool=false) - -Create code that, when evaluated, defines a function named `funcname`. -This function takes an `IO`, and checks if the data in the input conforms -to the regex, without executing any actions. -If the input conforms, return `nothing`. -Else, return `(byte, (line, col))`, where `byte` is the first invalid byte, -and `(line, col)` the 1-indexed position of that byte. -If the invalid byte is a `\n` byte, `col` is 0 and the line number is incremented. -If the input errors due to unexpected EOF, `byte` is `nothing`, and the line and column -given is the last byte in the file. -If `goto`, the function uses the faster but more complicated `:goto` code. -""" function Automa.generate_io_validator( funcname::Symbol, regex::Automa.RegExp.RE; diff --git a/src/Automa.jl b/src/Automa.jl index 55212a84..d6d8311e 100644 --- a/src/Automa.jl +++ b/src/Automa.jl @@ -23,7 +23,51 @@ function range_encode(set::ScanByte.ByteSet) return result end +""" + generate_reader(funcname::Symbol, machine::Automa.Machine; kwargs...) + +**NOTE: This method requires TranscodingStreams to be loaded** + +Generate a streaming reader function of the name `funcname` from `machine`. + +The generated function consumes data from a stream passed as the first argument +and executes the machine with filling the data buffer. + +This function returns an expression object of the generated function. The user +need to evaluate it in a module in which the generated function is needed. + +# Keyword Arguments +- `arguments`: Additional arguments `funcname` will take (default: `()`). + The default signature of the generated function is `(stream::TranscodingStream,)`, + but it is possible to supply more arguments to the signature with this keyword argument. +- `context`: Automa's codegenerator (default: `Automa.CodeGenContext()`). +- `actions`: A dictionary of action code (default: `Dict{Symbol,Expr}()`). +- `initcode`: Initialization code (default: `:()`). +- `loopcode`: Loop code (default: `:()`). +- `returncode`: Return code (default: `:(return cs)`). +- `errorcode`: Executed if `cs < 0` after `loopcode` (default error message) + +See the source code of this function to see how the generated code looks like +``` +""" function generate_reader end + +""" + generate_io_validator(funcname::Symbol, regex::RE; goto::Bool=false) + +**NOTE: This method requires TranscodingStreams to be loaded** + +Create code that, when evaluated, defines a function named `funcname`. +This function takes an `IO`, and checks if the data in the input conforms +to the regex, without executing any actions. +If the input conforms, return `nothing`. +Else, return `(byte, (line, col))`, where `byte` is the first invalid byte, +and `(line, col)` the 1-indexed position of that byte. +If the invalid byte is a `\n` byte, `col` is 0 and the line number is incremented. +If the input errors due to unexpected EOF, `byte` is `nothing`, and the line and column +given is the last byte in the file. +If `goto`, the function uses the faster but more complicated `:goto` code. +""" function generate_io_validator end include("re.jl") @@ -48,9 +92,8 @@ using .RegExp: RE, @re_str, opt, rep, rep1, onenter!, onexit!, onall!, onfinal!, include("workload.jl") # This list of exports lists the API -export RE, - @re_str, - CodeGenContext, +export CodeGenContext, + Variables, Tokenizer, tokenize, compile, @@ -65,6 +108,8 @@ export RE, make_tokenizer, # cat and alt is not exported in favor of * and | + RE, + @re_str, opt, rep, rep1, diff --git a/src/codegen.jl b/src/codegen.jl index 169c90c8..1106f69a 100644 --- a/src/codegen.jl +++ b/src/codegen.jl @@ -2,18 +2,30 @@ # ============== """ -Variable names used in generated code. - -The following variable names may be used in the code. - +Struct used to store variable names used in generated code. +Contained in a `CodeGenContext`. +Create a custom `Variables` for your `CodeGenContext` if you +want to customize the variables used in Automa codegen, typically +if you have conflicting variables with the same name. + +Automa generates code with the following variables, shown below +with their default names: - `p::Int`: current position of data - `p_end::Int`: end position of data - `is_eof::Bool`: Whether `p_end` marks end file stream - `cs::Int`: current state - `data::Any`: input data -- `mem::SizedMemory`: input data memory -- `byte::UInt8`: current data byte -- `buffer::TranscodingStreams.Buffer`: If reading from an IO +- `mem::SizedMemory`: Memory wrapping `data` +- `byte::UInt8`: current byte being read from `data` +- `buffer::TranscodingStreams.Buffer`: (`generate_reader` only) + +# Example +```julia +julia> ctx = CodeGenContext(vars=Variables(byte=:u8)); + +julia> ctx.vars.byte +:u8 +``` """ struct Variables p::Symbol @@ -58,15 +70,26 @@ function generate_goto_code end clean=false ) -Create a code generation context. +Create a `CodeGenContext` (ctx), a struct that stores options for Automa code generation. +Ctxs are used for Automa's various code generator functions. +They currently take the following options (more may be added in future versions) -Arguments ---------- +- `vars::Variables`: variable names used in generated code. See the `Variables` struct. +- `generator::Symbol`: code generator mechanism (`:table` or `:goto`). + The table generator creates smaller, simpler code that uses a vector of integers to + determine state transitions. The goto-generator uses a maze of `@goto`-statements, + and create larger, more complex code, that is faster. +- `getbyte::Function` (table generator only): function `f(data, p)` to access byte from data. + Default: `Base.getindex`. +- `clean`: Whether to remove some `QuoteNode`s (line information) from the generated code -- `vars`: variable names used in generated code -- `generator`: code generator (`:table` or `:goto`) -- `getbyte`: function of byte access (i.e. `getbyte(data, p)`) -- `clean`: flag of code cleansing, e.g. removing line comments +# Example +```julia +julia> ctx = CodeGenContext(generator=:goto, vars=Variables(buffer=:tbuffer)); + +julia> generate_code(ctx, compile(re"a+")) isa Expr +true +``` """ function CodeGenContext(; vars::Variables=Variables(:p, :p_end, :is_eof, :cs, :data, :mem, :byte, :buffer), @@ -136,12 +159,25 @@ end generate_code([::CodeGenContext], machine::Machine, actions=nothing)::Expr Generate init and exec code for `machine`. -Shorthand for: +The default code generator function for creating functions, preferentially use +this over generating init and exec code directly, due to its convenience. +Shorthand for producing the concatenated code of: + +* `generate_init_code(ctx, machine)` +* `generate_action_code(ctx, machine, actions)` +* `generate_input_error_code(ctx, machine)` [elided if actions == :debug] + +# Examples ``` -generate_init_code(ctx, machine) -generate_action_code(ctx, machine, actions) -generate_input_error_code(ctx, machine) [elided if actions == :debug] +@eval function foo(data) + # Initialize variables used in actions + data_buffer = UInt8[] + \$(generate_code(machine, actions)) + return data_buffer +end ``` + +See also: [`generate_init_code`](@ref), [`generate_exec_code`](@ref) """ function generate_code(ctx::CodeGenContext, machine::Machine, actions=nothing) # If actions are :debug, the user presumably wants to programatically @@ -165,8 +201,25 @@ generate_code(machine::Machine, actions=nothing) = generate_code(DefaultCodeGenC """ generate_init_code([::CodeGenContext], machine::Machine)::Expr -Generate variable initialization code. +Generate variable initialization code, initializing variables such as `p`, +and `p_end`. The names of these variables are set by the `CodeGenContext`. If not passed, the context defaults to `DefaultCodeGenContext` + +Prefer using the more generic `generate_code` over this function where possible. +This function should be used if the initialized data should be modified before +the execution code. + +# Example +```julia +@eval function foo(data) + \$(generate_init_code(machine)) + p = 2 # maybe I want to start from position 2, not 1 + \$(generate_exec_code(machine, actions)) + return cs +end +``` + +See also: [`generate_code`](@ref), [`generate_exec_code`](@ref) """ function generate_init_code(ctx::CodeGenContext, machine::Machine) vars = ctx.vars @@ -187,8 +240,25 @@ generate_init_code(machine::Machine) = generate_init_code(DefaultCodeGenContext, """ generate_exec_code([::CodeGenContext], machine::Machine, actions=nothing)::Expr -Generate machine execution code with actions. +Generate machine execution code with actions. This code should be run after the +machine has been initialized with `generate_init_code`. If not passed, the context defaults to `DefaultCodeGenContext` + +Prefer using the more generic `generate_code` over this function where possible. +This function should be used if the initialized data should be modified before +the execution code. + +# Examples +``` +@eval function foo(data) + \$(generate_init_code(machine)) + p = 2 # maybe I want to start from position 2, not 1 + \$(generate_exec_code(machine, actions)) + return cs +end +``` + +See also: [`generate_init_code`](@ref), [`generate_exec_code`](@ref) """ function generate_exec_code(ctx::CodeGenContext, machine::Machine, actions=nothing) # make actions @@ -713,7 +783,7 @@ identifier_pos = @relpos(p) identifier = data[@abspos(identifier_pos):p] ``` -See also: [`abspos`](@ref) +See also: [`@abspos`](@ref) """ macro relpos(p) :($WARNING_STRING) @@ -723,7 +793,7 @@ end abspos(p) Automa pseudomacro. Used to obtain the actual position of a relative position -obtained from `@relpos`. See `@relpos` for more details. +obtained from `@relpos`. See [`@relpos`](@ref) for more details. """ macro abspos(p) :($WARNING_STRING) @@ -745,7 +815,7 @@ description = sub_parser(stream) p = @bufferpos() ``` -See also: [`@setbuffer`](@ref) +See also: [`@bufferpos`](@ref) """ macro setbuffer() :($WARNING_STRING) diff --git a/src/dfa.jl b/src/dfa.jl index ffd5d55f..60f24e21 100644 --- a/src/dfa.jl +++ b/src/dfa.jl @@ -223,7 +223,7 @@ function validate_paths( repr(Char(first(intersect(edge1.labels, edge2.labels)))) end error( - "Ambiguous NFA. After inputs $input_until_now, observing $final_input " * + "Ambiguous NFA.\nAfter inputs $input_until_now, observing $final_input " * "lead to conflicting action sets $act1 and $act2" ) end diff --git a/src/dot.jl b/src/dot.jl index b02343b5..c377e7fe 100644 --- a/src/dot.jl +++ b/src/dot.jl @@ -43,6 +43,17 @@ end machine2dot(machine::Machine)::String Return a String with a flowchart of the machine in Graphviz (dot) format. +Using `Graphviz`, a command-line tool, the dot file can be converted to various +picture formats. + +# Example +```julia +open("/tmp/machine.dot", "w") do io + println(io, machine2dot(machine)) +end +# Requires graphviz to be installed +run(pipeline(`dot -Tsvg /tmp/machine.dot`), stdout="/tmp/machine.svg") +``` """ function machine2dot(machine::Machine) out = IOBuffer() diff --git a/src/machine.jl b/src/machine.jl index dd161184..cff034a0 100644 --- a/src/machine.jl +++ b/src/machine.jl @@ -108,9 +108,9 @@ function reorder_machine(machine::Machine) end """ - compile(re::RegExp; optimize::Bool=true, unambiguous::Bool=true) -> Machine + compile(re::RE; optimize::Bool=true, unambiguous::Bool=true)::Machine -Compile a finite state machine (FSM) from RegExp `re`. +Compile a finite state machine (FSM) from `re`. If `optimize`, attempt to minimize the number of states in the FSM. If `unambiguous`, disallow creation of FSM where the actions are not deterministic. @@ -120,7 +120,7 @@ machine = let name = re"[A-Z][a-z]+" first_last = name * re" " * name last_first = name * re", " * name - Automa.compile(first_last | last_first) + compile(first_last | last_first) end ``` """ diff --git a/src/re.jl b/src/re.jl index 2383c84a..eb36916f 100644 --- a/src/re.jl +++ b/src/re.jl @@ -22,6 +22,9 @@ Regex can be combined with other regex, strings or chars with `*`, `|`, `&` and * `a \\ b` matches input that mathes `a` but not `b` * `!a` matches all inputs that does not match `a`. +Set actions to regex with [`onenter!`](@ref), [`onexit!`](@ref), [`onall!`](@ref) +and [`onfinal!`](@ref), and preconditions with [`precond!`](@ref). + # Example ```julia julia> regex = (re"a*b?" | opt('c')) * re"[a-z]+"; @@ -57,16 +60,113 @@ function actions!(re::RE) re.actions end +""" + onenter!(re::RE, a::Union{Symbol, Vector{Symbol}}) -> re + +Set action(s) `a` to occur when reading the first byte of regex `re`. +If multiple actions are set by passing a vector, execute the actions in order. + +See also: [`onexit!`](@ref), [`onall!`](@ref), [`onfinal!`](@ref) + +# Example +```julia +julia> regex = re"ab?c*"; + +julia> regex2 = onenter!(regex, :entering_regex); + +julia> regex === regex2 +true +``` +""" onenter!(re::RE, v::Vector{Symbol}) = (actions!(re)[:enter] = v; re) onenter!(re::RE, s::Symbol) = onenter!(re, [s]) + +""" + onexit!(re::RE, a::Union{Symbol, Vector{Symbol}}) -> re + +Set action(s) `a` to occur when reading the first byte no longer part of regex +`re`, or if experiencing an expected end-of-file. +If multiple actions are set by passing a vector, execute the actions in order. + +See also: [`onenter!`](@ref), [`onall!`](@ref), [`onfinal!`](@ref) + +# Example +```julia +julia> regex = re"ab?c*"; + +julia> regex2 = onexit!(regex, :exiting_regex); + +julia> regex === regex2 +true +``` +""" onexit!(re::RE, v::Vector{Symbol}) = (actions!(re)[:exit] = v; re) onexit!(re::RE, s::Symbol) = onexit!(re, [s]) + +""" + onfinal!(re::RE, a::Union{Symbol, Vector{Symbol}}) -> re + +Set action(s) `a` to occur when the last byte of regex `re`. +If `re` does not have a definite final byte, e.g. `re"a(bc)*"`, where more "bc" +can always be added, compiling the regex will error after setting a final action. +If multiple actions are set by passing a vector, execute the actions in order. + +See also: [`onenter!`](@ref), [`onall!`](@ref), [`onexit!`](@ref) + +# Example +```julia +julia> regex = re"ab?c"; + +julia> regex2 = onfinal!(regex, :entering_last_byte); + +julia> regex === regex2 +true + +julia> compile(onfinal!(re"ab?c*", :does_not_work)) +ERROR: [...] +``` +""" onfinal!(re::RE, v::Vector{Symbol}) = (actions!(re)[:final] = v; re) onfinal!(re::RE, s::Symbol) = onfinal!(re, [s]) + +""" + onall!(re::RE, a::Union{Symbol, Vector{Symbol}}) -> re + +Set action(s) `a` to occur when reading any byte part of the regex `re`. +If multiple actions are set by passing a vector, execute the actions in order. + +See also: [`onenter!`](@ref), [`onexit!`](@ref), [`onfinal!`](@ref) + +# Example +```julia +julia> regex = re"ab?c*"; + +julia> regex2 = onall!(regex, :reading_re_byte); + +julia> regex === regex2 +true +``` +""" onall!(re::RE, v::Vector{Symbol}) = (actions!(re)[:all] = v; re) onall!(re::RE, s::Symbol) = onall!(re, [s]) -precond!(re::RE, s::Symbol) = re.when = s +""" + precond!(re::RE, s::Symbol) -> re + +Set `re`'s precondition to `s`. Before any state transitions to `re`, or inside +`re`, the precondition code `s` is checked before the transition is taken. + +# Example +```julia +julia> regex = re"ab?c*"; + +julia> regex2 = precond!(regex, :some_condition); + +julia> regex === regex2 +true +``` +""" +precond!(re::RE, s::Symbol) = (re.when = s; re) const Primitive = Union{RE, ByteSet, UInt8, UnitRange{UInt8}, Char, String, Vector{UInt8}} @@ -154,6 +254,20 @@ end Base.:!(re::RE) = neg(re) +""" + @re_str -> RE + +Construct an Automa regex of type `RE` from a string. +Note that due to Julia's raw string escaping rules, `re"\\\\"` means a single backslash, and so does `re"\\\\\\\\"`, while `re"\\\\\\\\\\""` means a backslash, then a quote character. + +Examples: +```julia +julia> re"ab?c*[def][^ghi]+" isa RE +true +``` + +See also: [`RE`](@ref) +""" macro re_str(str::String) parse(str) end diff --git a/todo.md b/todo.md new file mode 100644 index 00000000..dd8a034c --- /dev/null +++ b/todo.md @@ -0,0 +1,9 @@ +* Doctests in all docstrings and documentation +* + +================ PRECONDITIONS +Seems like it's not quite though out yet. Does anyone use it? + +What do we REALLY want? Some kind of toggle: + precond(::Expr, re1, [re2]), where if only 1 regex is passed, you can only move into + regex if Expr. If two are passed, you check Expr, and move into re1, else re2. From 1dda546eb484344aec7c5905569278fb839eb743 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Wed, 8 Mar 2023 19:11:27 +0100 Subject: [PATCH 51/64] Bump CI version to Julia 1.6 --- .github/workflows/Downstream.yml | 1 - .github/workflows/UnitTests.yml | 2 +- src/codegen.jl | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/Downstream.yml b/.github/workflows/Downstream.yml index c4c2fb76..b306e169 100644 --- a/.github/workflows/Downstream.yml +++ b/.github/workflows/Downstream.yml @@ -20,7 +20,6 @@ jobs: - {user: BioJulia, repo: BED.jl, group: Automa} - {user: BioJulia, repo: BigBed.jl, group: Automa} - {user: BioJulia, repo: FASTX.jl, group: Automa} - - {user: BioJulia, repo: GeneticVariation.jl, group: Automa} - {user: BioJulia, repo: GFF3.jl, group: Automa} - {user: BioJulia, repo: XAM.jl, group: Automa} - {user: dellison, repo: ConstituencyTrees.jl, group: Automa} diff --git a/.github/workflows/UnitTests.yml b/.github/workflows/UnitTests.yml index 96da9bbc..a548814a 100644 --- a/.github/workflows/UnitTests.yml +++ b/.github/workflows/UnitTests.yml @@ -12,7 +12,7 @@ jobs: fail-fast: false matrix: julia-version: - - '1.5' + - '1.6' - '1' julia-arch: [x86] os: [ubuntu-latest, windows-latest, macOS-latest] diff --git a/src/codegen.jl b/src/codegen.jl index 1106f69a..c93964ab 100644 --- a/src/codegen.jl +++ b/src/codegen.jl @@ -692,8 +692,8 @@ end # Add a warning if users go down a rabbit hole trying to figure out what these macros # expand to. # See the function `rewrite_special_macros` below, where the expansion happens -const WARNING_STRING = """This string comes from the expansion of a fake macro in Automa.jl. \ -It is intercepted and expanded by Automa's own compiler, not by the Julia compiler. \ +const WARNING_STRING = """This string comes from the expansion of a fake macro in Automa.jl. +It is intercepted and expanded by Automa's own compiler, not by the Julia compiler. Search for this string in the Automa source code to learn more.""" """ From 151a4c597a85fc1b28f5411ac341339c4ff7dde2 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Wed, 8 Mar 2023 22:52:07 +0100 Subject: [PATCH 52/64] Make generate_buffer_validator goto into kwarg --- src/codegen.jl | 10 ++++++++-- test/simd.jl | 2 +- test/test13.jl | 2 +- test/test18.jl | 2 +- test/validator.jl | 4 ++-- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/codegen.jl b/src/codegen.jl index c93964ab..c7c36f7c 100644 --- a/src/codegen.jl +++ b/src/codegen.jl @@ -114,15 +114,21 @@ end const DefaultCodeGenContext = CodeGenContext() """ - generate_buffer_validator(name::Symbol, regexp::RE, goto=false) + generate_buffer_validator(name::Symbol, regexp::RE; goto=true; docstring=true) Generate code that, when evaluated, defines a function named `name`, which takes a single argument `data`, interpreted as a sequence of bytes. The function returns `nothing` if `data` matches `Machine`, else the index of the first invalid byte. If the machine reached unexpected EOF, returns `0`. If `goto`, the function uses the faster but more complicated `:goto` code. +If `docstring`, automatically create a docstring for the generated function. """ -function generate_buffer_validator(name::Symbol, regex::RegExp.RE, goto::Bool=false; docstring::Bool=true) +function generate_buffer_validator( + name::Symbol, + regex::RegExp.RE; + goto::Bool=true, + docstring::Bool=true +) ctx = goto ? CodeGenContext(generator=:goto) : DefaultCodeGenContext machine = compile(RegExp.strip_actions(regex)) code = quote diff --git a/test/simd.jl b/test/simd.jl index b8958df0..4f13140d 100644 --- a/test/simd.jl +++ b/test/simd.jl @@ -16,7 +16,7 @@ using Automa context = CodeGenContext(generator=:goto) - eval(generate_buffer_validator(:is_valid_fasta, regex, true)) + eval(generate_buffer_validator(:is_valid_fasta, regex; goto=true)) s1 = ">seq\nTAGGCTA\n>hello\nAJKGMP" s2 = ">seq1\nTAGGC" diff --git a/test/test13.jl b/test/test13.jl index 38b1c418..92918560 100644 --- a/test/test13.jl +++ b/test/test13.jl @@ -11,7 +11,7 @@ using Test (!re"A[BC]D?E", ["ABCDE", "ABCE"], ["ABDE", "ACE", "ABE"]) ] for goto in (false, true) - @eval $(Automa.generate_buffer_validator(:validate, regex, goto; docstring=false)) + @eval $(Automa.generate_buffer_validator(:validate, regex; goto=goto, docstring=false)) for string in good_strings @test validate(string) === nothing end diff --git a/test/test18.jl b/test/test18.jl index 3bc8076b..775985f3 100644 --- a/test/test18.jl +++ b/test/test18.jl @@ -7,7 +7,7 @@ using Test @testset "Test18" begin regex = re"\0\a\b\t\n\v\r\x00\xff\xFF[\\][^\\]" for goto in (false, true) - @eval $(Automa.generate_buffer_validator(:validate, regex, goto; docstring=false)) + @eval $(Automa.generate_buffer_validator(:validate, regex; goto=goto, docstring=false)) # Bad input types @test_throws Exception validate(18) diff --git a/test/validator.jl b/test/validator.jl index 3fd0edf0..f4f47f7e 100644 --- a/test/validator.jl +++ b/test/validator.jl @@ -6,8 +6,8 @@ using Test @testset "Validator" begin regex = re"a(bc)*|(def)|x+" | re"def" | re"x+" - eval(Automa.generate_buffer_validator(:foobar, regex, false)) - eval(Automa.generate_buffer_validator(:barfoo, regex, true)) + eval(Automa.generate_buffer_validator(:foobar, regex; goto=false)) + eval(Automa.generate_buffer_validator(:barfoo, regex; goto=true)) eval(Automa.generate_io_validator(:io_bar, regex; goto=false)) eval(Automa.generate_io_validator(:io_foo, regex; goto=true)) From 2bb69f26ae6c9f41b470a2cfe1ec80c3db012bce Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Thu, 9 Mar 2023 08:57:42 +0100 Subject: [PATCH 53/64] Update README.md --- README.md | 142 ++++++++++++++++++++++++++---------------------------- 1 file changed, 67 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index 5afad714..3c6b885f 100644 --- a/README.md +++ b/README.md @@ -1,91 +1,83 @@ -Automa.jl -========= +# Automa.jl [![Docs Latest](https://img.shields.io/badge/docs-latest-blue.svg)](https://biojulia.github.io/Automa.jl/latest/) [![codecov.io](http://codecov.io/github/BioJulia/Automa.jl/coverage.svg?branch=master)](http://codecov.io/github/BioJulia/Automa.jl?branch=master) -A Julia package for text validation, parsing, and tokenizing based on state machine compiler. +Automa is a regex-to-Julia compiler. +By compiling regex to Julia code in the form of `Expr` objects, +Automa provides facilities to create efficient and robust regex-based lexers, tokenizers and parsers using Julia's metaprogramming capabilities. +You can view Automa as a regex engine that can insert arbitrary Julia code into its input matching process, which will be executed when certain parts of the regex matches an input. -![Schema of Automa.jl](/docs/src/figure/Automa.png) +![Schema of Automa.jl](figure/Automa.png) -Automa.jl compiles regular expressions into Julia code, which is then compiled -into low-level machine code by the Julia compiler. Automa.jl is designed to -generate very efficient code to scan large text data, which is often much faster -than handcrafted code. Automa.jl can insert arbitrary Julia code that will be -executed in state transitions. This makes it possible, for example, to extract -substrings that match a part of a regular expression. +Automa is designed to generate very efficient code to scan large text data, often much faster than handcrafted code. -This is a number literal tokenizer using Automa.jl ([numbers.jl](example/numbers.jl)): +For more information [read the documentation](https://biojulia.github.io/Automa.jl/latest/), or read the examples below and in the `examples/` directory in this repository. + +## Examples +### Validate some text only is composed of ASCII alphanumeric characters ```julia -# A tokenizer of octal, decimal, hexadecimal and floating point numbers -# ===================================================================== - -import Automa -import Automa.RegExp: @re_str -const re = Automa.RegExp - -# Describe patterns in regular expression. -oct = re"0o[0-7]+" -dec = re"[-+]?[0-9]+" -hex = re"0x[0-9A-Fa-f]+" -prefloat = re"[-+]?([0-9]+\.[0-9]*|[0-9]*\.[0-9]+)" -float = prefloat | re.cat(prefloat | re"[-+]?[0-9]+", re"[eE][-+]?[0-9]+") -number = oct | dec | hex | float -numbers = re.cat(re.opt(number), re.rep(re" +" * number), re" *") - -# Register action names to regular expressions. -number.actions[:enter] = [:mark] -oct.actions[:exit] = [:oct] -dec.actions[:exit] = [:dec] -hex.actions[:exit] = [:hex] -float.actions[:exit] = [:float] - -# Compile a finite-state machine. -machine = Automa.compile(numbers) - -# This generates a SVG file to visualize the state machine. -# write("numbers.dot", Automa.machine2dot(machine)) -# run(`dot -Tpng -o numbers.png numbers.dot`) - -# Bind an action code for each action name. -actions = Dict( - :mark => :(mark = p), - :oct => :(emit(:oct)), - :dec => :(emit(:dec)), - :hex => :(emit(:hex)), - :float => :(emit(:float)), -) +using Automa -# Generate a tokenizing function from the machine. -context = Automa.CodeGenContext() -@eval function tokenize(data::String) - tokens = Tuple{Symbol,String}[] - mark = 0 - $(Automa.generate_init_code(context, machine)) - emit(kind) = push!(tokens, (kind, data[mark:p-1])) - $(Automa.generate_exec_code(context, machine, actions)) - return tokens, cs == 0 ? :ok : cs < 0 ? :error : :incomplete -end +generate_buffer_validator(:validate_alphanumeric, re"[a-zA-Z0-9]*") |> eval -tokens, status = tokenize("1 0x0123BEEF 0o754 3.14 -1e4 +6.022045e23") +for s in ["abc", "aU81m", "!,>"] + println("$s is alphanumeric? $(isnothing(validate_alphanumeric(s)))") +end ``` -This emits tokens and the final status: +### Making a lexer +```julia +using Automa + +tokens = [ + :identifier => re"[A-Za-z_][0-9A-Za-z_!]*", + :lparens => re"\(", + :rparens => re"\)", + :comma => re",", + :quot => re"\"", + :space => re"[\t\f ]+", +]; +@eval @enum Token errortoken $(first.(tokens)...) +make_tokenizer((errortoken, + [Token(i) => j for (i,j) in enumerate(last.(tokens))] +)) |> eval + +collect(tokenize(Token, """(alpha, "beta15")""")) +``` - ~/.j/v/Automa (master) $ julia -qL example/numbers.jl - julia> tokens - 6-element Array{Tuple{Symbol,String},1}: - (:dec,"1") - (:hex,"0x0123BEEF") - (:oct,"0o754") - (:float,"3.14") - (:float,"-1e4") - (:float,"+6.022045e23") +### Make a simple TSV file parser +```julia +using Automa + +machine = let + name = onexit!(onenter!(re"[^\t\r\n]+", :mark), :name) + field = onexit!(onenter!(re"[^\t\r\n]+", :mark), :field) + nameline = name * rep('\t' * name) + record = onexit!(field * rep('\t' * field), :record) + compile(nameline * re"\r?\n" * record * rep(re"\r?\n" * record) * rep(re"\r?\n")) +end - julia> status - :ok +actions = Dict( + :mark => :(pos = p), + :name => :(push!(headers, String(data[pos:p-1]))), + :field => quote + n_fields += 1 + push!(fields, String(data[pos:p-1])) + end, + :record => quote + n_fields == length(headers) || error("Malformed TSV") + n_fields = 0 + end +) -The compiled deterministic finite automaton (DFA) looks like this: -![DFA](/docs/src/figure/numbers.png) +@eval function parse_tsv(data) + headers = String[] + fields = String[] + pos = n_fields = 0 + $(generate_code(machine, actions)) + (headers, reshape(fields, length(headers), :)) +end -For more details, see [fasta.jl](/example/fasta.jl) and read the docs page. +header, data = parse_tsv("a\tabc\n12\t13\r\nxyc\tz\n\n") +``` \ No newline at end of file From 3e0467943aa275623bd5ff6941d1bc6ab924c8ab Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Thu, 9 Mar 2023 08:58:01 +0100 Subject: [PATCH 54/64] Add documentation preview --- .github/workflows/Documentation.yml | 17 +++++++++++++++++ .github/workflows/UnitTests.yml | 19 +------------------ docs/make.jl | 3 +-- 3 files changed, 19 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/Documentation.yml diff --git a/.github/workflows/Documentation.yml b/.github/workflows/Documentation.yml new file mode 100644 index 00000000..9f782a37 --- /dev/null +++ b/.github/workflows/Documentation.yml @@ -0,0 +1,17 @@ +name: Documentation + +on: + push: + pull_request: + +jobs: + Documenter: + name: Documentation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/julia-buildpkg@latest + - uses: julia-actions/julia-docdeploy@latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} diff --git a/.github/workflows/UnitTests.yml b/.github/workflows/UnitTests.yml index a548814a..cff13466 100644 --- a/.github/workflows/UnitTests.yml +++ b/.github/workflows/UnitTests.yml @@ -12,7 +12,7 @@ jobs: fail-fast: false matrix: julia-version: - - '1.6' + - '1.6' # LTS - '1' julia-arch: [x86] os: [ubuntu-latest, windows-latest, macOS-latest] @@ -42,20 +42,3 @@ jobs: name: codecov-umbrella fail_ci_if_error: false token: ${{ secrets.CODECOV_TOKEN }} - docs: - name: Documentation - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: julia-actions/setup-julia@latest - with: - version: '1' - - run: | - julia --project=docs -e ' - using Pkg - Pkg.develop(PackageSpec(path=pwd())) - Pkg.instantiate()' - - run: julia --project=docs docs/make.jl - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} diff --git a/docs/make.jl b/docs/make.jl index 981bd0c3..fac10257 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -29,6 +29,5 @@ makedocs( deploydocs( repo = "github.com/BioJulia/Automa.jl.git", - target = "build", - push_preview = true, + push_preview = true ) From 3c953e63f054837552bc535f89f89e468c61f7fd Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Thu, 9 Mar 2023 10:01:19 +0100 Subject: [PATCH 55/64] Always remove dead nodes Currently, the functions `re2nfa` and `nfa2dfa` can produce dead (unreachable) nodes, which is pointless. Instead of relying on the user to themselves remove dead nodes by calling `remove_dead_nodes`, this should just happen automatically. --- docs/create_pngs.jl | 5 ++--- docs/src/figure/larger.png | Bin 56495 -> 40695 bytes docs/src/theory.md | 14 +++++++------- src/dfa.jl | 2 +- src/machine.jl | 4 ++-- src/nfa.jl | 2 +- 6 files changed, 13 insertions(+), 14 deletions(-) diff --git a/docs/create_pngs.jl b/docs/create_pngs.jl index 1e45b383..bffae824 100644 --- a/docs/create_pngs.jl +++ b/docs/create_pngs.jl @@ -63,9 +63,8 @@ digraph { dot_png(dot, "$DIR/kleenestar.png") open("/tmp/re.dot", "w") do io - nfa = Automa.remove_dead_nodes(Automa.re2nfa(re"(\+|-)?(0|1)*")) - #dfa = Automa.remove_dead_nodes(Automa.reduce_nodes(Automa.nfa2dfa(nfa))) - dfa = Automa.remove_dead_nodes(Automa.nfa2dfa(nfa)) + nfa = Automa.re2nfa(re"(\+|-)?(0|1)*") + dfa = Automa.nfa2dfa(nfa) println(io, Automa.dfa2dot(dfa)) end run(pipeline(`dot -Tpng /tmp/re.dot`, stdout="$DIR/large_dfa.png")) diff --git a/docs/src/figure/larger.png b/docs/src/figure/larger.png index 02e4aeff39e2166d01edbdba18988f3be28b4115..476f9969a86830551696d836573799eb79e64394 100644 GIT binary patch literal 40695 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 PpNV6qn#LF}-v0jpHFL#R literal 56495 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

qGr24yBwneO2^4GCjv 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 i$Ua(Q-yg&$d*vz&wI`{pY;Y9*VwT-J+jJXo Date: Fri, 21 Apr 2023 14:20:48 +0200 Subject: [PATCH 56/64] Do not make TranscodingStreams an extension Since Automa doesn't just extend an existing function but adds a new function based on TranscodingStreams, this is not the intended use case of extensions. --- docs/make.jl | 1 - docs/src/io.md | 5 -- docs/src/validators.md | 4 +- src/Automa.jl | 53 +------------------- ext/AutomaStream.jl => src/stream.jl | 74 +++++++++++++++++++++------- 5 files changed, 59 insertions(+), 78 deletions(-) rename ext/AutomaStream.jl => src/stream.jl (67%) diff --git a/docs/make.jl b/docs/make.jl index fac10257..d6b33dc4 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,5 +1,4 @@ using Documenter -using TranscodingStreams # to load extension using Automa DocMeta.setdocmeta!(Automa, :DocTestSetup, :(using Automa); recursive=true) diff --git a/docs/src/io.md b/docs/src/io.md index d11801d1..0fd31696 100644 --- a/docs/src/io.md +++ b/docs/src/io.md @@ -7,11 +7,6 @@ end ``` # Parsing from an IO - -!!! note - Parsing from an IO relies on TranscodingStreams.jl, and the relevant methods are defined in an extension module in Automa. - If you use Julia 1.9 or later, you must load TranscodingStreams before loading Automa to test this functionality. - Some file types are gigabytes or tens of gigabytes in size. For these files, parsing from a buffer may be impractical, as they require you to read in the entire file in memory at once. Automa enables this by hooking into `TranscodingStreams.jl`, a package that provides a wrapper IO of the type `TranscodingStream`. diff --git a/docs/src/validators.md b/docs/src/validators.md index 02875771..7c2b4347 100644 --- a/docs/src/validators.md +++ b/docs/src/validators.md @@ -47,7 +47,7 @@ julia> validate_fasta(">hello\nTAGAGA\nTAGAG\n") # nothing; it matches ## IO validators For large files, having to read the data into a buffer to validate it may not be possible. -When the package `TranscodingStreams` is loaded, Automa also supports creating IO validators with the `generate_io_validator` function: +Automa also supports creating IO validators with the `generate_io_validator` function: This works very similar to `generate_buffer_validator`, but the generated function takes an `IO`, and has a different return value: * If the data matches, still return `nothing` @@ -73,4 +73,4 @@ julia> validate_io(IOBuffer(">hello\nAC")) Automa.generate_buffer_validator Automa.generate_io_validator Automa.compile -``` \ No newline at end of file +``` diff --git a/src/Automa.jl b/src/Automa.jl index d6d8311e..bfd18065 100644 --- a/src/Automa.jl +++ b/src/Automa.jl @@ -1,6 +1,7 @@ module Automa using ScanByte: ScanByte, ByteSet +using TranscodingStreams: TranscodingStreams, TranscodingStream, NoopStream # Encode a byte set into a sequence of non-empty ranges. function range_encode(set::ScanByte.ByteSet) @@ -23,53 +24,6 @@ function range_encode(set::ScanByte.ByteSet) return result end -""" - generate_reader(funcname::Symbol, machine::Automa.Machine; kwargs...) - -**NOTE: This method requires TranscodingStreams to be loaded** - -Generate a streaming reader function of the name `funcname` from `machine`. - -The generated function consumes data from a stream passed as the first argument -and executes the machine with filling the data buffer. - -This function returns an expression object of the generated function. The user -need to evaluate it in a module in which the generated function is needed. - -# Keyword Arguments -- `arguments`: Additional arguments `funcname` will take (default: `()`). - The default signature of the generated function is `(stream::TranscodingStream,)`, - but it is possible to supply more arguments to the signature with this keyword argument. -- `context`: Automa's codegenerator (default: `Automa.CodeGenContext()`). -- `actions`: A dictionary of action code (default: `Dict{Symbol,Expr}()`). -- `initcode`: Initialization code (default: `:()`). -- `loopcode`: Loop code (default: `:()`). -- `returncode`: Return code (default: `:(return cs)`). -- `errorcode`: Executed if `cs < 0` after `loopcode` (default error message) - -See the source code of this function to see how the generated code looks like -``` -""" -function generate_reader end - -""" - generate_io_validator(funcname::Symbol, regex::RE; goto::Bool=false) - -**NOTE: This method requires TranscodingStreams to be loaded** - -Create code that, when evaluated, defines a function named `funcname`. -This function takes an `IO`, and checks if the data in the input conforms -to the regex, without executing any actions. -If the input conforms, return `nothing`. -Else, return `(byte, (line, col))`, where `byte` is the first invalid byte, -and `(line, col)` the 1-indexed position of that byte. -If the invalid byte is a `\n` byte, `col` is 0 and the line number is incremented. -If the input errors due to unexpected EOF, `byte` is `nothing`, and the line and column -given is the last byte in the file. -If `goto`, the function uses the faster but more complicated `:goto` code. -""" -function generate_io_validator end - include("re.jl") include("precond.jl") include("action.jl") @@ -82,10 +36,7 @@ include("dot.jl") include("memory.jl") include("codegen.jl") include("tokenizer.jl") - -if !isdefined(Base, :get_extension) - include("../ext/AutomaStream.jl") -end +include("stream.jl") using .RegExp: RE, @re_str, opt, rep, rep1, onenter!, onexit!, onall!, onfinal!, precond! diff --git a/ext/AutomaStream.jl b/src/stream.jl similarity index 67% rename from ext/AutomaStream.jl rename to src/stream.jl index 213dac1f..72a3bce1 100644 --- a/ext/AutomaStream.jl +++ b/src/stream.jl @@ -1,18 +1,40 @@ -module AutomaStream +""" + generate_reader(funcname::Symbol, machine::Automa.Machine; kwargs...) -using Automa: Automa -using TranscodingStreams: TranscodingStream, NoopStream +**NOTE: This method requires TranscodingStreams to be loaded** -function Automa.generate_reader( +Generate a streaming reader function of the name `funcname` from `machine`. + +The generated function consumes data from a stream passed as the first argument +and executes the machine with filling the data buffer. + +This function returns an expression object of the generated function. The user +need to evaluate it in a module in which the generated function is needed. + +# Keyword Arguments +- `arguments`: Additional arguments `funcname` will take (default: `()`). + The default signature of the generated function is `(stream::TranscodingStream,)`, + but it is possible to supply more arguments to the signature with this keyword argument. +- `context`: Automa's codegenerator (default: `Automa.CodeGenContext()`). +- `actions`: A dictionary of action code (default: `Dict{Symbol,Expr}()`). +- `initcode`: Initialization code (default: `:()`). +- `loopcode`: Loop code (default: `:()`). +- `returncode`: Return code (default: `:(return cs)`). +- `errorcode`: Executed if `cs < 0` after `loopcode` (default error message) + +See the source code of this function to see how the generated code looks like +``` +""" +function generate_reader( funcname::Symbol, - machine::Automa.Machine; + machine::Machine; arguments=(), - context::Automa.CodeGenContext=Automa.DefaultCodeGenContext, + context::CodeGenContext=DefaultCodeGenContext, actions::Dict{Symbol,Expr}=Dict{Symbol,Expr}(), initcode::Expr=:(), loopcode::Expr=:(), returncode::Expr=:(return $(context.vars.cs)), - errorcode::Expr=Automa.generate_input_error_code(context, machine) + errorcode::Expr=generate_input_error_code(context, machine) ) # Add a `return` to the return expression if the user forgot it if returncode.head != :return @@ -27,7 +49,7 @@ function Automa.generate_reader( # at_eof and cs is meaningless, and when both are set to nothing, @escape # will error at parse time function rewrite(ex::Expr) - Automa.rewrite_special_macros(; + rewrite_special_macros(; ctx=context, ex=ex, at_eof=nothing, @@ -38,7 +60,7 @@ function Automa.generate_reader( functioncode.args[2] = quote $(vars.buffer) = stream.state.buffer1 $(vars.data) = $(vars.buffer).data - $(Automa.generate_init_code(context, machine)) + $(generate_init_code(context, machine)) $(rewrite(initcode)) # Overwrite is_eof for Stream, since we don't know the real EOF # until after we've actually seen the stream eof @@ -56,7 +78,7 @@ function Automa.generate_reader( $(vars.is_eof) = eof(stream) $(vars.p) = $(vars.buffer).bufferpos $(vars.p_end) = $(vars.buffer).marginpos - 1 - $(Automa.generate_exec_code(context, machine, actions)) + $(generate_exec_code(context, machine, actions)) # Advance the buffer, hence advancing the stream itself $(vars.buffer).bufferpos = $(vars.p) @@ -78,15 +100,31 @@ function Automa.generate_reader( return functioncode end -function Automa.generate_io_validator( +""" + generate_io_validator(funcname::Symbol, regex::RE; goto::Bool=false) + +**NOTE: This method requires TranscodingStreams to be loaded** + +Create code that, when evaluated, defines a function named `funcname`. +This function takes an `IO`, and checks if the data in the input conforms +to the regex, without executing any actions. +If the input conforms, return `nothing`. +Else, return `(byte, (line, col))`, where `byte` is the first invalid byte, +and `(line, col)` the 1-indexed position of that byte. +If the invalid byte is a `\n` byte, `col` is 0 and the line number is incremented. +If the input errors due to unexpected EOF, `byte` is `nothing`, and the line and column +given is the last byte in the file. +If `goto`, the function uses the faster but more complicated `:goto` code. +""" +function generate_io_validator( funcname::Symbol, - regex::Automa.RegExp.RE; + regex::RegExp.RE; goto::Bool=false ) ctx = if goto - Automa.CodeGenContext(generator=:goto) + CodeGenContext(generator=:goto) else - Automa.DefaultCodeGenContext + DefaultCodeGenContext end vars = ctx.vars returncode = quote @@ -131,8 +169,8 @@ function Automa.generate_io_validator( p_newline = 0 end end - machine = Automa.compile(Automa.RegExp.set_newline_actions(regex)) - actions = if :newline ∈ Automa.machine_names(machine) + machine = compile(RegExp.set_newline_actions(regex)) + actions = if :newline ∈ machine_names(machine) Dict{Symbol, Expr}(:newline => quote line_num += 1 cleared_cols = 0 @@ -142,7 +180,7 @@ function Automa.generate_io_validator( else Dict{Symbol, Expr}() end - function_code = Automa.generate_reader( + function_code = generate_reader( funcname, machine; context=ctx, @@ -169,5 +207,3 @@ function Automa.generate_io_validator( $(funcname)(io::$(IO)) = $(funcname)($(NoopStream)(io)) end end - -end # module From fec085bc938f6b84fb718cc35bc47359d6dc24e3 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Tue, 25 Apr 2023 12:50:34 +0200 Subject: [PATCH 57/64] Add todo to gitignore --- .gitignore | 1 + todo.md | 9 --------- 2 files changed, 1 insertion(+), 9 deletions(-) delete mode 100644 todo.md diff --git a/.gitignore b/.gitignore index 32972416..57d5c69d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ docs/build/ docs/site/ .Rproj.user Manifest.toml +todo.md diff --git a/todo.md b/todo.md deleted file mode 100644 index dd8a034c..00000000 --- a/todo.md +++ /dev/null @@ -1,9 +0,0 @@ -* Doctests in all docstrings and documentation -* - -================ PRECONDITIONS -Seems like it's not quite though out yet. Does anyone use it? - -What do we REALLY want? Some kind of toggle: - precond(::Expr, re1, [re2]), where if only 1 regex is passed, you can only move into - regex if Expr. If two are passed, you check Expr, and move into re1, else re2. From 76145dc777412085e47a527c6766acf964502d17 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Tue, 25 Apr 2023 12:53:53 +0200 Subject: [PATCH 58/64] Migrate from SnoopPrecompile to PrecompileTools --- Project.toml | 2 +- src/workload.jl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Project.toml b/Project.toml index cd4bc89b..4ff7451d 100644 --- a/Project.toml +++ b/Project.toml @@ -4,8 +4,8 @@ authors = ["Kenta Sato ", "Jakob Nybo Nissen Date: Sat, 1 Jul 2023 11:00:44 +0200 Subject: [PATCH 59/64] Disable SIMD capability I've come to the conclusion that Julia does not make it possible to robustly check what CPU instructions the user has available. The current options are all undocumented, complex and brittle, and not suitable for code that cannot be accepted to break at any time Whenever a robust way of checking for CPU instructions are available, the change is easy to revert. --- Project.toml | 5 ++-- src/Automa.jl | 5 ++-- src/byteset.jl | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/codegen.jl | 18 ++++++++++++- 4 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 src/byteset.jl diff --git a/Project.toml b/Project.toml index 4ff7451d..f0c2cc06 100644 --- a/Project.toml +++ b/Project.toml @@ -5,14 +5,13 @@ version = "1.0.0" [deps] PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" -ScanByte = "7b38b023-a4d7-4c5e-8d43-3f3097f304eb" TranscodingStreams = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" [compat] ScanByte = "0.4.0" -SnoopPrecompile = "1" -TranscodingStreams = "0.9" julia = "1.6" +PrecompileTools = "1" +TranscodingStreams = "0.9" [extras] Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/src/Automa.jl b/src/Automa.jl index bfd18065..6e157b87 100644 --- a/src/Automa.jl +++ b/src/Automa.jl @@ -1,10 +1,11 @@ module Automa -using ScanByte: ScanByte, ByteSet using TranscodingStreams: TranscodingStreams, TranscodingStream, NoopStream +include("byteset.jl") + # Encode a byte set into a sequence of non-empty ranges. -function range_encode(set::ScanByte.ByteSet) +function range_encode(set::ByteSet) result = UnitRange{UInt8}[] it = iterate(set) it === nothing && return result diff --git a/src/byteset.jl b/src/byteset.jl new file mode 100644 index 00000000..953f3250 --- /dev/null +++ b/src/byteset.jl @@ -0,0 +1,72 @@ +struct ByteSet <: AbstractSet{UInt8} + data::NTuple{4, UInt64} + ByteSet(x::NTuple{4, UInt64}) = new(x) +end + +ByteSet() = ByteSet((UInt64(0), UInt64(0), UInt64(0), UInt64(0))) +Base.length(s::ByteSet) = mapreduce(count_ones, +, s.data) +Base.isempty(s::ByteSet) = s === ByteSet() + +function ByteSet(it) + a = b = c = d = UInt64(0) + for i in it + vi = convert(UInt8, i) + if vi < 0x40 + a |= UInt(1) << ((vi - 0x00) & 0x3f) + elseif vi < 0x80 + b |= UInt(1) << ((vi - 0x40) & 0x3f) + elseif vi < 0xc0 + c |= UInt(1) << ((vi - 0x80) & 0x3f) + else + d |= UInt(1) << ((vi - 0xc0) & 0x3f) + end + end + ByteSet((a, b, c, d)) +end + +function Base.minimum(s::ByteSet) + y = iterate(s) + y === nothing ? Base._empty_reduce_error() : first(y) +end + +function Base.maximum(s::ByteSet) + offset = 0x03 * UInt8(64) + for i in 0:3 + @inbounds bits = s.data[4 - i] + iszero(bits) && continue + return ((3-i)*64 + (64 - leading_zeros(bits)) - 1) % UInt8 + end + Base._empty_reduce_error() +end + +function Base.in(byte::UInt8, s::ByteSet) + i, o = divrem(byte, UInt8(64)) + @inbounds !(iszero(s.data[i & 0x03 + 0x01] >>> (o & 0x3f) & UInt(1))) +end + +@inline function Base.iterate(s::ByteSet, state=UInt(0)) + ioffset, offset = divrem(state, UInt(64)) + n = UInt(0) + while iszero(n) + ioffset > 3 && return nothing + n = s.data[ioffset + 1] >>> offset + offset *= !iszero(n) + ioffset += 1 + end + tz = trailing_zeros(n) + result = (64 * (ioffset - 1) + offset + tz) % UInt8 + (result, UInt(result) + UInt(1)) +end + +function Base.:~(s::ByteSet) + a, b, c, d = s.data + ByteSet((~a, ~b, ~c, ~d)) +end + +is_contiguous(s::ByteSet) = isempty(s) || (maximum(s) - minimum(s) + 1 == length(s)) + +Base.union(a::ByteSet, b::ByteSet) = ByteSet(map(|, a.data, b.data)) +Base.intersect(a::ByteSet, b::ByteSet) = ByteSet(map(&, a.data, b.data)) +Base.symdiff(a::ByteSet, b::ByteSet) = ByteSet(map(⊻, a.data, b.data)) +Base.setdiff(a::ByteSet, b::ByteSet) = ByteSet(map((i,j) -> i & ~j, a.data, b.data)) +Base.isdisjoint(a::ByteSet, b::ByteSet) = isempty(intersect(a, b)) diff --git a/src/codegen.jl b/src/codegen.jl index c7c36f7c..424431f5 100644 --- a/src/codegen.jl +++ b/src/codegen.jl @@ -469,7 +469,16 @@ function generate_goto_code(ctx::CodeGenContext, machine::Machine, actions::Dict # This can be effectively SIMDd # If such an edge is detected, we treat it specially with code here, and leave the # non-SIMDable edges for below + + # SIMD code temporarily disabled. simd, non_simd = peel_simd_edge(s) + if simd !== nothing + push!(non_simd, (simd, s)) + end + simd = nothing + simd_code = :() + + #= simd_code = if simd !== nothing quote $(generate_simd_loop(ctx, simd.labels)) @@ -481,6 +490,7 @@ function generate_goto_code(ctx::CodeGenContext, machine::Machine, actions::Dict else :() end + =# # If no inputs match, then we set cs = -cs to signal error, and go to exit default = :($(ctx.vars.cs) = $(-s.state); @goto exit) @@ -555,6 +565,11 @@ end # Note: This function has been carefully crafted to produce (nearly) optimal # assembly code for AVX2-capable CPUs. Change with great care. + +# Temporarily disabled because I've come to the realization that Julia does not +# yet make it possible to robustly check what CPU instructions the user has available +# See related issue +#= function generate_simd_loop(ctx::CodeGenContext, bs::ByteSet) # ScanByte finds first byte in a byteset. We want to find first # byte NOT in this byteset, as this is where we can no longer skip ahead to @@ -581,6 +596,7 @@ end @inline function loop_simd(ptr::Ptr, len::UInt, valbs::Val) ScanByte.memchr(ptr, len, valbs) end +=# # Make if/else statements for each state that is an acceptable end state, and execute # the actions attached with ending in this state. @@ -949,7 +965,7 @@ function debug_actions(machine::Machine) end "If possible, remove self-simd edge." -function peel_simd_edge(node) +function peel_simd_edge(node)::Tuple{Union{Nothing, Edge}, Vector{Tuple{Edge, Node}}} non_simd = Tuple{Edge, Node}[] simd = nothing # A simd-edge has no actions or preconditions, and its source is same as destination. From 6e3b6760bbb26a4f158c232992acea91e6beed42 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Tue, 18 Jul 2023 21:05:54 +0200 Subject: [PATCH 60/64] Fix preconditions Allow preconditions to be set to `:enter` only, and have directly conflicting preconditions resolve an ambiguous NFA. --- .gitignore | 1 + docs/src/parser.md | 59 ++++++++++++++++++++++++++++++++++++++++++++++ src/dfa.jl | 7 ++---- src/edge.jl | 21 ----------------- src/nfa.jl | 38 ++++++++++++++++++++++++++--- src/re.jl | 31 +++++++++++++++++------- test/test11.jl | 19 +++++++++++++-- test/test19.jl | 1 + 8 files changed, 137 insertions(+), 40 deletions(-) diff --git a/.gitignore b/.gitignore index 57d5c69d..1f1d8928 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ docs/site/ .Rproj.user Manifest.toml todo.md +/LocalPreferences.toml diff --git a/docs/src/parser.md b/docs/src/parser.md index 73e7f901..3cb55d49 100644 --- a/docs/src/parser.md +++ b/docs/src/parser.md @@ -197,6 +197,65 @@ Input is not in any outgoing edge, and machine therefore errored. The code above parses with about 300 MB/s on my laptop. Not bad, but Automa can do better - read on to learn how to customize codegen. +## Preconditions +You might have noticed a peculiar detail about our FASTA format: It demands a trailing newline after each record. +In other words, `>a\nA` is not a valid FASTA record. + +We can easily rewrite the regex such that the last record does not need a trailing `\n`. +But look what happens when we try that: + +```jldoctest parse1 +julia> machine = let + header = onexit!(onenter!(re"[a-z]+", :mark_pos), :header) + seqline = onexit!(onenter!(re"[ACGT]+", :mark_pos), :seqline) + record = onexit!(re">" * header * '\n' * seqline * rep('\n' * seqline), :record) + compile(opt(record) * rep('\n' * record) * rep(re"\n")) + end; +ERROR: Ambiguous NFA. +``` + +Why does this error? Well, remember that Automa processes one byte at a time, and at each byte, makes a decision on what actions to execute. +Hence, if it sees the input `>a\nA\n`, it does not know what to do when encountering the second `\n`. If the next byte e,g. `A`, then it would need to execute the `:seqline` action. If the byte is `>`, it would need to execute first `:seqline`, then `:record`. +Automa can't read ahead, so, the regex is ambiguous and the true behaviour when reading the inputs `>a\nA\n` is undefined. +Therefore, Automa refuses to compile it. + +There are several ways to solve this: +* First, you can rewrite the regex to not be ambiguous. This is usually the preferred option: After all, if the regex is ambiguous, you probably made a mistake with the regex +* You can manually diasable the ambiguity check by passing the keyword `unambiguous=false` to `compile`. This will cause the machine to undefined behaviour if an input like `>a\nA\n` is seen, so this is usually a poor idea. +* You can rewrite the actions, such that the action itself uses an if-statement to check what to do. In the example above, you could remove the `:record` action and have the `:seqline` action conditionally emit a record if the next byte was `>`. + +Finally, you can use _preconditions_. +A precondition is a symbol, attached to a regex, just like an action. +Just like an action, the symbol is attached to an `Expr` object, but for preconditions this must evaluate to a `Bool`. +If `false`, the regex is not entered. + +Let's have an example. The following machine is obviously ambiguous: + +```jldoctest parse1 +julia> machine = let + a = onenter!(re"XY", :a) + b = onenter!(re"XZ", :b) + compile('A' * (a | b)) + end; +ERROR: Ambiguous NFA. +``` + +We can add a precondition with `precond!`. Below, `precond!(regex, label)` is equivalent to `precond!(regex, label; when=:enter, bool=true)`. This means "only enter `regex` when the boolean expression `label` evaluates to `bool` (`true`)": + +```jldoctest parse1 +julia> machine = let + a = precond!(onenter!(re"XY", :a), :test) + b = precond!(onenter!(re"XZ", :b), :test; bool=false) + compile('A' * (a | b)) + end; + +julia> machine isa Automa.Machine +true +``` + +Here, `re"XY"` can only be entered when `:test` is `true`, and `re"XZ"` only when `:test` is `false`. +So, there can be no ambiguous behaviour and the regex compiles fine. + ## Reference ```@docs Automa.onenter! diff --git a/src/dfa.jl b/src/dfa.jl index 11931704..4a9b2f96 100644 --- a/src/dfa.jl +++ b/src/dfa.jl @@ -204,12 +204,9 @@ function validate_paths( eof = (edge1 === nothing) & (edge2 === nothing) if !eof - # If they are real edges but do not overlap, there is no conflict + # If they are real edges but do not overlap, or there are conflicting + # preconditions, there is no conflict overlaps(edge1, edge2) || continue - - # If the FSM may disambiguate the two edges based on preconditions - # there is no conflict (or, rather, we can't prove a conflict. - has_potentially_conflicting_precond(edge1, edge2) && continue end # Now we know there is an ambiguity, so we just need to create diff --git a/src/edge.jl b/src/edge.jl index 731a5a4c..95dd9e68 100644 --- a/src/edge.jl +++ b/src/edge.jl @@ -52,24 +52,3 @@ function in_sort_order(e1::Edge, e2::Edge) # so if we reach here, something went wrong. error() end - -"""Check if two edges have preconditions that could be disambiguating. -I.e. can an FSM distinguish the edges based on their conditions? -""" -function has_potentially_conflicting_precond(e1::Edge, e2::Edge) - # This is true for most edges, to check it first - isempty(e1.precond.names) && isempty(e2.precond.names) && return false - - symbols = union(Set(e1.precond.names), Set(e2.precond.names)) - for symbol in symbols - v1 = e1.precond[symbol] - v2 = e2.precond[symbol] - - # NONE means the edge can never be taken, so they are trivially disambiguated - (v1 == NONE || v2 == NONE) && return true - - # If they are the same, they cannot be used to distinguish - v1 == v2 || return true - end - return false -end diff --git a/src/nfa.jl b/src/nfa.jl index 8bd964b0..d472e2ec 100644 --- a/src/nfa.jl +++ b/src/nfa.jl @@ -167,11 +167,22 @@ function re2nfa(re::RegExp.RE, predefined_actions::Dict{Symbol,Action}=Dict{Symb final = final_in end - if re.when !== nothing - name = re.when + # Add preconditions: The enter precondition is only added to the edge leading + # into this regex's NFA, whereas the all precondition is added to all edges. + # We do not add it to eps edges, since these are NFA artifacts, and will be + # removed during compilation to DFA anyway: The salient part is that the non-eps + # edges have preconditions. + if re.precond_enter !== nothing + (name, bool) = re.precond_enter + for e in traverse_first_noneps(start) + push!(e.precond, (name => (bool ? TRUE : FALSE))) + end + end + if re.precond_all !== nothing + (name, bool) = re.precond_all for s in traverse(start), (e, _) in s.edges if !iseps(e) - push!(e.precond, (name => TRUE)) + push!(e.precond, (name => (bool ? TRUE : FALSE))) end end end @@ -184,6 +195,27 @@ function re2nfa(re::RegExp.RE, predefined_actions::Dict{Symbol,Action}=Dict{Symb return remove_dead_nodes(NFA(nfa_start, nfa_final)) end +# Return the set of the first non-epsilon edges reachable from the node +function traverse_first_noneps(node::NFANode)::Set{Edge} + result = Set{Edge}() + stack = [node] + seen = Set(stack) + while !isempty(stack) + node = pop!(stack) + for (edge, child) in node.edges + if iseps(edge) + if !in(child, seen) + push!(stack, child) + push!(seen, child) + end + else + push!(result, edge) + end + end + end + result +end + # Removes both dead nodes, i.e. nodes from which there is no path to # the final node, and also unreachable nodes, i.e. nodes that cannot be # reached from the start node. diff --git a/src/re.jl b/src/re.jl index eb36916f..e430bc89 100644 --- a/src/re.jl +++ b/src/re.jl @@ -44,11 +44,12 @@ mutable struct RE head::Symbol args::Vector actions::Union{Nothing, Dict{Symbol, Vector{Symbol}}} - when::Union{Symbol, Nothing} + precond_all::Union{Tuple{Symbol, Bool}, Nothing} + precond_enter::Union{Tuple{Symbol, Bool}, Nothing} end function RE(head::Symbol, args::Vector) - return RE(head, args, nothing, nothing) + return RE(head, args, nothing, nothing, nothing) end RE(s::AbstractString) = parse(s) @@ -151,10 +152,13 @@ onall!(re::RE, v::Vector{Symbol}) = (actions!(re)[:all] = v; re) onall!(re::RE, s::Symbol) = onall!(re, [s]) """ - precond!(re::RE, s::Symbol) -> re + precond!(re::RE, s::Symbol; [when=:enter], [bool=true]) -> re Set `re`'s precondition to `s`. Before any state transitions to `re`, or inside -`re`, the precondition code `s` is checked before the transition is taken. +`re`, the precondition code `s` is checked to be `bool` before the transition is taken. + +`when` controls if the condition is checked when the regex is entered (if `:enter`), +or at every state transition inside the regex (if `:all`) # Example ```julia @@ -166,7 +170,16 @@ julia> regex === regex2 true ``` """ -precond!(re::RE, s::Symbol) = (re.when = s; re) +function precond!(re::RE, s::Symbol; when::Symbol=:enter, bool::Bool=true) + if when === :enter + re.precond_enter = (s, bool) + elseif when === :all + re.precond_all = (s, bool) + else + error("`precond!` only takes :enter or :all in third position") + end + re +end const Primitive = Union{RE, ByteSet, UInt8, UnitRange{UInt8}, Char, String, Vector{UInt8}} @@ -527,7 +540,7 @@ end # Create a deep copy of the regex without any actions function strip_actions(re::RE) args = [arg isa RE ? strip_actions(arg) : arg for arg in re.args] - RE(re.head, args, Dict{Symbol, Vector{Symbol}}(), re.when) + RE(re.head, args, Dict{Symbol, Vector{Symbol}}(), re.precond_enter, re.precond_all) end # Create a deep copy with the only actions being a :newline action @@ -542,11 +555,11 @@ function set_newline_actions(re::RE)::RE if re.head == :set set = only(re.args)::ByteSet if UInt8('\n') ∈ set - re1 = RE(:set, [ByteSet(UInt8('\n'))], Dict(:enter => [:newline]), re.when) + re1 = RE(:set, [ByteSet(UInt8('\n'))], Dict(:enter => [:newline]), re.precond_enter, re.precond_all) if length(set) == 1 re1 else - re2 = RE(:set, [setdiff(set, ByteSet(UInt8('\n')))], Dict{Symbol, Vector{Symbol}}(), re.when) + re2 = RE(:set, [setdiff(set, ByteSet(UInt8('\n')))], Dict{Symbol, Vector{Symbol}}(), re.precond_enter, re.precond_all) re1 | re2 end else @@ -554,7 +567,7 @@ function set_newline_actions(re::RE)::RE end else args = [arg isa RE ? set_newline_actions(arg) : arg for arg in re.args] - RE(re.head, args, Dict{Symbol, Vector{Symbol}}(), re.when) + RE(re.head, args, Dict{Symbol, Vector{Symbol}}(), re.precond_enter, re.precond_all) end end diff --git a/test/test11.jl b/test/test11.jl index 288f2e72..cef20e85 100644 --- a/test/test11.jl +++ b/test/test11.jl @@ -10,11 +10,15 @@ using Test onexit!(a, :one) b = re"[a-z]+[0-9]+" onexit!(b, :two) + c = re"[A-Z][a-z]+" + precond!(c, :le, when=:all) + onexit!(c, :three) - machine = compile((a | b) * '\n') + machine = compile((a | b | c) * '\n') actions = Dict( :one => :(push!(logger, :one)), :two => :(push!(logger, :two)), + :three => :(push!(logger, :three)), :le => :(p ≤ n)) ctx = CodeGenContext(generator=:table) @@ -28,13 +32,24 @@ using Test $(Automa.generate_exec_code(ctx, machine, actions)) return logger, cs == 0 ? :ok : cs < 0 ? :error : :incomplete end + # p > n @test validate(b"a\n", 0) == ([], :error) + # p == n @test validate(b"a\n", 1) == ([:one], :ok) + # p == n @test validate(b"a1\n", 1) == ([:two], :ok) - @test validate(b"aa\n", 1) == ([], :error) + # p == n on enter, but not after first + @test validate(b"Aa\n", 1) == ([], :error) + @test validate(b"Aaa\n", 3) == ([:three], :ok) + @test validate("A\n", 1) == ([], :error) + @test validate("aa", 2) == ([], :incomplete) + # matches b @test validate(b"aa1\n", 1) == ([:two], :ok) + # Matches a @test validate(b"aa\n", 2) == ([:one], :ok) + # Matches b @test validate(b"aa1\n", 2) == ([:two], :ok) + # Matches neither @test validate(b"1\n", 1) == ([], :error) end end diff --git a/test/test19.jl b/test/test19.jl index 8a82841f..47cc6c93 100644 --- a/test/test19.jl +++ b/test/test19.jl @@ -41,6 +41,7 @@ using Test A = re"XY" precond!(A, :cond) B = re"XZ" + precond!(B, :cond, when=:enter, bool=false) onenter!(A, :enter_A) @test compile(A | B, unambiguous=true) isa Automa.Machine end From e2b285a7bddb293b0704ecaa944609dfbc07a386 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Wed, 19 Jul 2023 06:00:32 +0200 Subject: [PATCH 61/64] Add more tests --- docs/src/parser.md | 2 +- src/machine.jl | 2 +- test/byteset.jl | 77 +++++++++++++++++++++++++++++++++++++++++++++ test/input_error.jl | 15 +++++++++ test/runtests.jl | 6 ++-- test/test11.jl | 2 ++ 6 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 test/byteset.jl create mode 100644 test/input_error.jl diff --git a/docs/src/parser.md b/docs/src/parser.md index 3cb55d49..868536c2 100644 --- a/docs/src/parser.md +++ b/docs/src/parser.md @@ -183,7 +183,7 @@ If we give out function a bad input - for example, if we forget the trailing new ```jldoctest parse1 julia> parse_fasta(">abc\nTAGA\nAAGA\n>header\nAAAG\nGGCG") ERROR: Error during FSM execution at buffer position 33. -Last 32 bytes were: +Last 32 byte(s) were: ">abc\nTAGA\nAAGA\n>header\nAAAG\nGGCG" diff --git a/src/machine.jl b/src/machine.jl index 7784774c..4d8499f9 100644 --- a/src/machine.jl +++ b/src/machine.jl @@ -212,7 +212,7 @@ function throw_input_error( string(index), ".\nLast ", string(length(slice)), - " bytes were:\n\n" + " byte(s) were:\n\n" ) write(buf, bytes, "\n\n") diff --git a/test/byteset.jl b/test/byteset.jl new file mode 100644 index 00000000..bdf265fc --- /dev/null +++ b/test/byteset.jl @@ -0,0 +1,77 @@ +module TestByteSet + +using Automa: Automa, ByteSet +using Test + + +function test_membership(members) + bs = ByteSet(members) + refset = Set{UInt8}([UInt8(i) for i in members]) + @test refset == Set{UInt8}(collect(bs)) + @test all(i -> in(i, bs), refset) +end + +function test_inversion(bs) + inv = ~bs + all = true + for i in 0x00:0xff + all &= (in(i, bs) ⊻ in(i, inv)) + end + @test all +end + +@testset "Instantiation" begin + @test isempty(ByteSet()) + @test iszero(length(ByteSet())) + + for set in ["hello", "kdjy82zxxcbnpw", [0x00, 0x1a, 0xff, 0xf8, 0xd2]] + test_membership(set) + end +end + +@testset "Min/max" begin + @test_throws ArgumentError maximum(ByteSet()) + @test_throws ArgumentError minimum(ByteSet()) + @test minimum(ByteSet("xylophone")) == UInt8('e') + @test maximum(ByteSet([0xa1, 0x0f, 0x4e, 0xf1, 0x40, 0x39])) == 0xf1 +end + +@testset "Contiguity" begin + @test Automa.is_contiguous(ByteSet(0x03:0x41)) + @test Automa.is_contiguous(ByteSet()) + @test Automa.is_contiguous(ByteSet(0x51)) + @test Automa.is_contiguous(ByteSet(0xc1:0xd2)) + @test Automa.is_contiguous(ByteSet(0x00:0xff)) + + @test !Automa.is_contiguous(ByteSet([0x12:0x3a; 0x3c:0x4a])) + @test !Automa.is_contiguous(ByteSet([0x01, 0x02, 0x04, 0x05])) +end + +@testset "Inversion" begin + test_inversion(ByteSet()) + test_inversion(ByteSet(0x00:0xff)) + test_inversion(ByteSet([0x04, 0x06, 0x91, 0x92])) + test_inversion(ByteSet(0x54:0x71)) + test_inversion(ByteSet(0x12:0x11)) + test_inversion(ByteSet("abracadabra")) +end + +@testset "Set operations" begin + sets = map(ByteSet, [ + [], + [0x00:0xff;], + [0x00:0x02; 0x04; 0x19], + [0x01; 0x03; 0x09; 0xa1; 0xa1], + [0x41:0x8f; 0xd1:0xe1; 0xa0:0xf0], + [0x81:0x89; 0xd0:0xd0] + ]) + ssets = map(Set, sets) + for (s1, ss1) in zip(sets, ssets), (s2, ss2) in zip(sets, ssets) + for f in [union, intersect, symdiff, setdiff] + @test Set(f(s1, s2)) == f(ss1, ss2) + end + @test isdisjoint(s1, s2) == isdisjoint(ss1, ss2) + end +end + +end # module diff --git a/test/input_error.jl b/test/input_error.jl new file mode 100644 index 00000000..0cb1e2cd --- /dev/null +++ b/test/input_error.jl @@ -0,0 +1,15 @@ +module TestInputError + +using Automa +using Test + +@testset "Input error" begin + machine = compile(re"xyz") + @eval function test_input_error(data) + $(generate_code(machine)) + end + + @test_throws Exception test_input_error("a") +end + +end # module diff --git a/test/runtests.jl b/test/runtests.jl index 50ddacfd..56ccebef 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -32,10 +32,7 @@ using Test end @testset "ByteSet" begin - x = Automa.ByteSet() - @test isempty(x) - @test_throws ArgumentError minimum(x) - @test_throws ArgumentError maximum(x) + include("byteset.jl") end @testset "RegExp" begin @@ -124,6 +121,7 @@ include("test16.jl") include("test17.jl") include("test18.jl") include("test19.jl") +include("input_error.jl") include("simd.jl") include("unicode.jl") include("validator.jl") diff --git a/test/test11.jl b/test/test11.jl index cef20e85..907b2949 100644 --- a/test/test11.jl +++ b/test/test11.jl @@ -4,6 +4,8 @@ using Automa using Test @testset "Test11" begin + @test_throws Exception precond!(re"A", :foo; when=:never) + a = re"[a-z]+" precond!(a, :le) a = rep1(a) From 355c86976a12742b65913df7918d51d7af65ea45 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Wed, 19 Jul 2023 07:48:18 +0200 Subject: [PATCH 62/64] Some JET fixes --- src/dfa.jl | 4 ++-- src/nfa.jl | 2 +- src/re.jl | 8 +++++--- src/tokenizer.jl | 10 +++++----- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/dfa.jl b/src/dfa.jl index 4a9b2f96..8af76864 100644 --- a/src/dfa.jl +++ b/src/dfa.jl @@ -206,7 +206,7 @@ function validate_paths( if !eof # If they are real edges but do not overlap, or there are conflicting # preconditions, there is no conflict - overlaps(edge1, edge2) || continue + overlaps(edge1::Edge, edge2::Edge) || continue end # Now we know there is an ambiguity, so we just need to create @@ -217,7 +217,7 @@ function validate_paths( final_input = if eof "EOF" else - repr(Char(first(intersect(edge1.labels, edge2.labels)))) + repr(Char(first(intersect((edge1::Edge).labels, (edge2::Edge).labels)))) end error( "Ambiguous NFA.\nAfter inputs $input_until_now, observing $final_input " * diff --git a/src/nfa.jl b/src/nfa.jl index d472e2ec..7d84232c 100644 --- a/src/nfa.jl +++ b/src/nfa.jl @@ -191,7 +191,7 @@ function re2nfa(re::RegExp.RE, predefined_actions::Dict{Symbol,Action}=Dict{Symb end nfa_start = NFANode() - nfa_final = rec!(nfa_start, re) + nfa_final::NFANode = rec!(nfa_start, re) return remove_dead_nodes(NFA(nfa_start, nfa_final)) end diff --git a/src/re.jl b/src/re.jl index e430bc89..478a9014 100644 --- a/src/re.jl +++ b/src/re.jl @@ -55,10 +55,12 @@ end RE(s::AbstractString) = parse(s) function actions!(re::RE) - if isnothing(re.actions) - re.actions = Dict{Symbol, Vector{Symbol}}() + x = re.actions + if x === nothing + x = Dict{Symbol, Vector{Symbol}}() + re.actions = x end - re.actions + x end """ diff --git a/src/tokenizer.jl b/src/tokenizer.jl index d0e597c9..8a4299a2 100644 --- a/src/tokenizer.jl +++ b/src/tokenizer.jl @@ -90,7 +90,7 @@ See also: [`Tokenizer`](@ref), [`tokenize`](@ref), [`compile`](@ref) """ function make_tokenizer( machine::TokenizerMachine; - tokens::Tuple{E, AbstractVector{E}}=(UInt32(1):UInt32(machine.n_tokens), UInt32(0)), + tokens::Tuple{E, AbstractVector{E}}=(UInt32(0), UInt32(1):UInt32(machine.n_tokens)), goto::Bool=true, version::Int=1 ) where E @@ -120,7 +120,7 @@ function make_tokenizer( ) actions[action_name] = quote stop = $(vars.p) - token = $(nonerror_tokens[parse(Int, only(m.captures))]) + token = $(nonerror_tokens[parse(Int, something(only(m.captures)))]) end end return quote @@ -209,10 +209,10 @@ function make_tokenizer( version::Int=1, unambiguous=false ) where E - (regex, _tokens) = if tokens isa Vector - (tokens, (UInt32(0), UInt32(1):UInt32(length(tokens)))) + (regex, _tokens) = if tokens isa AbstractVector + (Vector(tokens)::Vector, (UInt32(0), UInt32(1):UInt32(length(tokens)))) else - (map(last, last(tokens)), (first(tokens), map(first, last(tokens)))) + ([last(i) for i in last(tokens)]::Vector, (first(tokens), map(first, last(tokens)))) end make_tokenizer( compile(regex; unambiguous=unambiguous); From b72dae59ff7b5a9bb0f6509274c481e030e5e7cb Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Wed, 19 Jul 2023 09:02:33 +0200 Subject: [PATCH 63/64] Improve string/char and RE operations --- src/re.jl | 7 ++++--- src/stream.jl | 3 --- test/runtests.jl | 2 ++ 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/re.jl b/src/re.jl index 478a9014..25cc731c 100644 --- a/src/re.jl +++ b/src/re.jl @@ -52,7 +52,8 @@ function RE(head::Symbol, args::Vector) return RE(head, args, nothing, nothing, nothing) end -RE(s::AbstractString) = parse(s) +RE(s::AbstractString) = parse(string(s)) +RE(c::AbstractChar) = primitive(Char(c)) function actions!(re::RE) x = re.actions @@ -263,8 +264,8 @@ Base.:&(re1::RE, re2::RE) = isec(re1, re2) Base.:\(re1::RE, re2::RE) = diff(re1, re2) for f in (:*, :|, :&, :\) - @eval Base.$(f)(x::Union{AbstractString, AbstractChar}, re::RE) = $(f)(RE(string(x)), re) - @eval Base.$(f)(re::RE, x::Union{AbstractString, AbstractChar}) = $(f)(re, RE(string(x))) + @eval Base.$(f)(x::Union{AbstractString, AbstractChar}, re::RE) = $(f)(RE(x), re) + @eval Base.$(f)(re::RE, x::Union{AbstractString, AbstractChar}) = $(f)(re, RE(x)) end Base.:!(re::RE) = neg(re) diff --git a/src/stream.jl b/src/stream.jl index 72a3bce1..cbec40f0 100644 --- a/src/stream.jl +++ b/src/stream.jl @@ -1,8 +1,6 @@ """ generate_reader(funcname::Symbol, machine::Automa.Machine; kwargs...) -**NOTE: This method requires TranscodingStreams to be loaded** - Generate a streaming reader function of the name `funcname` from `machine`. The generated function consumes data from a stream passed as the first argument @@ -23,7 +21,6 @@ need to evaluate it in a module in which the generated function is needed. - `errorcode`: Executed if `cs < 0` after `loopcode` (default error message) See the source code of this function to see how the generated code looks like -``` """ function generate_reader( funcname::Symbol, diff --git a/test/runtests.jl b/test/runtests.jl index 56ccebef..bc4d7318 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -37,6 +37,8 @@ end @testset "RegExp" begin @test_throws ArgumentError("invalid escape sequence: \\o") Automa.RegExp.parse("\\o") + @test ('+' * re"abc") isa RE + @test_throws Exception Automa.RegExp.parse("+") end @testset "DOT" begin From fe0feb1e3a2c41e5108b722dfbc99bc525e08749 Mon Sep 17 00:00:00 2001 From: Jakob Nybo Nissen Date: Wed, 19 Jul 2023 09:31:09 +0200 Subject: [PATCH 64/64] Fix Project --- Project.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Project.toml b/Project.toml index f0c2cc06..226b225a 100644 --- a/Project.toml +++ b/Project.toml @@ -8,7 +8,6 @@ PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" TranscodingStreams = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" [compat] -ScanByte = "0.4.0" julia = "1.6" PrecompileTools = "1" TranscodingStreams = "0.9"