Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented FrAngel on feat/frangel #7

Merged
merged 9 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dev/Project.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
[deps]
Git = "d7ba0133-e1db-5d97-8f8c-041e4b3a1eb2"
JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899"
3 changes: 1 addition & 2 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ jobs:
matrix:
version:
- '1.10'
- '1.8'
- 'pre'
- '1'
os:
- ubuntu-latest
arch:
Expand Down
6 changes: 6 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,11 @@ name = "Garden"
uuid = "22d0bc84-1826-4aaf-b584-c2a6a91114a2"
version = "0.1.0"

[deps]
DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
Herb = "c09c6b7f-4f63-49de-90d9-97a3563c0f4a"

[compat]
DocStringExtensions = "0.9.3"
Herb = "0.4.1"
julia = "^1.8"
6 changes: 5 additions & 1 deletion src/Garden.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
module Garden

greet() = print("Hello World!")
using DocStringExtensions

include("frangel/method.jl")

export FrAngel

end # module Garden
17 changes: 17 additions & 0 deletions src/frangel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# FrAngel

[Publication (Open Access)](https://doi.org/10.1145/3290386)

```
@article{DBLP:journals/pacmpl/ShiSL19,
author = {Kensen Shi and
Jacob Steinhardt and
Percy Liang},
title = {FrAngel: component-based synthesis with control structures},
journal = {Proc. {ACM} Program. Lang.},
volume = {3},
number = {{POPL}},
pages = {73:1--73:29},
year = {2019}
}
```
245 changes: 245 additions & 0 deletions src/frangel/method.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
module FrAngel

using DocStringExtensions
using Herb.HerbCore: AbstractRuleNode, AbstractGrammar, get_rule, isfilled, depth
using Herb.HerbSpecification: AbstractSpecification, Problem
using Herb.HerbSearch: ProgramIterator, evaluate
using Herb.HerbInterpret: SymbolTable
using Herb.HerbConstraints: get_grammar, freeze_state
using Herb.HerbGrammar: ContextSensitiveGrammar, grammar2symboltable, rulenode2expr,
isterminal, iscomplete, add_rule!

@enum SynthResult optimal_program=1 suboptimal_program=2

struct NoProgramFoundError <: Exception
message::String
end

"""
$(TYPEDSIGNATURES)

Synthesize a program using the `grammar` that follows the `spec` following the method from
["FrAngel: component-based synthesis with control structures"](https://doi.org/10.1145/3290386).

!!! note

This implementation includes fragment mining but *excludes angelic conditions*!

Constructs an iterator of type `iterator_type`. Iteratively, collects promising programs, mines fragments from them and adds them to the grammar.
This is then re-started with the updated grammar.

- `frangel_iterations` determines how many iterations of search are run
- `max_iterations` puts a bound on how many programs should be enumerated within one search procedure.
- `max_iteration_time` limits the time the iterator has for each search. Note that this time is only checked whenever a program is iterated and thus may take slightly longer than the given time.
```
"""
function frangel(
iterator_type::Type{T},
grammar::AbstractGrammar,
starting_sym::Symbol,
problem::Problem;
max_iterations::Int = typemax(Int),
frangel_iterations::Int = 3,
max_iteration_time::Int = typemax(Int),
kwargs...
)::Union{AbstractRuleNode, Nothing} where {T <: ProgramIterator}
# FrAngel config arguments

for _ in 1:frangel_iterations
# Gets an iterator with some limit (the low-level budget)
iterator = iterator_type(grammar, starting_sym; kwargs...)

# Run a budgeted search
promising_programs, result_flag = get_promising_programs(
iterator, problem; max_time = max_iteration_time,
max_enumerations = max_iterations)

if result_flag == optimal_program
return only(promising_programs) # returns the only element
end

# Throw an error if no programs were found.
if length(promising_programs) == 0
throw(NoProgramFoundError("No promising program found for the given specification. Try exploring more programs."))
end

# Extract fragments
fragments = mine_fragments(grammar, promising_programs)

# Select fragments that should be added to the gramamr
selected_fragments = select_fragments(fragments)

# Modify grammar
modify_grammar_frangel!(selected_fragments, grammar)
end

@warn "No solution found. Within $frangel_iterations iterations."
return nothing
end

"""
$(TYPEDSIGNATURES)

Decide whether to keep a program, or discard it, based on the specification.
Returns a score where score=1 yields the program immediately.
"""
function decide_frangel(
program::AbstractRuleNode,
problem::Problem,
grammar::ContextSensitiveGrammar,
symboltable::SymbolTable
)
expr = rulenode2expr(program, grammar)
score = evaluate(problem, expr, symboltable, shortcircuit = false)
return score
end

"""
$(TYPEDSIGNATURES)

Modify the grammar based on the fragments mined from the programs kept during the `decide` step.
This function adds "Fragment_{type}" to the grammar to denote added fragments.
"""
function modify_grammar_frangel!(
fragments::AbstractVector{<:AbstractRuleNode},
grammar::AbstractGrammar;
max_fragment_rules::Int = typemax(Int)
)
for f in fragments
ind = get_rule(f)
type = grammar.types[ind]
frag_type = Symbol("Fragment_", type)

# Add fragment_{type} to the grammar if not present yet
if !haskey(grammar.bytype, frag_type)
#@TODO substitute fragment rules
add_rule!(grammar, Meta.parse("$type = $frag_type"))
end

expr = rulenode2expr(f, grammar)
add_rule!(grammar, Meta.parse("$frag_type = $expr"))
end
end

"""
$(TYPEDSIGNATURES)

Selects the smallest (fewest number of nodes) fragments from the set of mined fragments.
`num_programs` determines how many programs should be selected.
"""
function select_smallest_fragments(
fragments::Set{AbstractRuleNode};
num_programs::Int = 3
)::AbstractVector{<:AbstractRuleNode}
sorted_nodes = sort(collect(fragments), by = x -> length(x))

# Select the top 3 elements
return sorted_nodes[1:min(num_programs, length(sorted_nodes))]
end

"""
$(TYPEDSIGNATURES)

Selects the shallowest (smallest depth) fragments from the set of mined fragments.
`num_programs` determines how many programs should be selected.
"""
function select_shallowest_fragments(
fragments::Set{AbstractRuleNode};
num_programs::Int = 3
)::AbstractVector{<:AbstractRuleNode}
sorted_nodes = sort(collect(fragments), by = x -> depth(x))

# Select the top 3 elements
return sorted_nodes[1:min(num_programs, length(sorted_nodes))]
end

function select_fragments(fragments::Set{AbstractRuleNode})
select_smallest_fragments(fragments; num_programs = 3)
end

"""
$(TYPEDSIGNATURES)

Iterates over the solutions to find partial or full solutions.
Takes an iterator to enumerate programs. Quits when `max_time` or `max_enumerations` is reached.
If the program solves the problem, it is returned with the `optimal_program` flag.
If a program solves some of the problem (e.g. some but not all examples) it is added to the list of `promising_programs`.
The set of promising programs is returned eventually.
"""
function get_promising_programs(
iterator::ProgramIterator,
problem::Problem;
max_time = typemax(Int),
max_enumerations = typemax(Int),
mod::Module = Main
)::Tuple{Set{AbstractRuleNode}, SynthResult}
start_time = time()
grammar = get_grammar(iterator.solver)
symboltable::SymbolTable = grammar2symboltable(grammar, mod)

promising_programs = Set{AbstractRuleNode}()

for (i, candidate_program) in enumerate(iterator)
score = decide_frangel(candidate_program, problem, grammar, symboltable)

if score == 1
push!(promising_programs, freeze_state(candidate_program))
return (promising_programs, optimal_program)
elseif score > 0
push!(promising_programs, freeze_state(candidate_program))
end

# Check stopping criteria
if i > max_enumerations || time() - start_time > max_time
break
end
end

return (promising_programs, suboptimal_program)
end

"""
$(TYPEDSIGNATURES)

Finds all the fragments from the `program` defined over the `grammar`.

The result is a set of the distinct program fragments, generated recursively by iterating over all children. A fragment is any complete subprogram of the original program.

"""
function mine_fragments(
grammar::AbstractGrammar, program::AbstractRuleNode)::Set{AbstractRuleNode}
fragments = Set{AbstractRuleNode}()
# Push terminals as they are
if isfilled(program) && !isterminal(grammar, program)
# Only complete programs count are considered
if iscomplete(grammar, program)
push!(fragments, program)
end
for child in program.children
fragments = union(fragments, mine_fragments(grammar, child))
end
end

return fragments
end

"""
$(TYPEDSIGNATURES)

Finds fragments (subprograms) of each program in `programs`.
"""
function mine_fragments(
grammar::AbstractGrammar, programs::Set{<:AbstractRuleNode})::Set{AbstractRuleNode}
fragments = reduce(union, mine_fragments(grammar, p) for p in programs)
fragments = setdiff(fragments, programs) # Don't include the programs themselves in the set of fragments

return fragments
end

export
frangel,
decide_frangel,
modify_grammar_frangel!,
get_promising_programs

end
11 changes: 11 additions & 0 deletions src/frangel/ref.bib
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@article{DBLP:journals/pacmpl/ShiSL19,
author = {Kensen Shi and
Jacob Steinhardt and
Percy Liang},
title = {FrAngel: component-based synthesis with control structures},
journal = {Proc. {ACM} Program. Lang.},
volume = {3},
number = {{POPL}},
pages = {73:1--73:29},
year = {2019}
}
1 change: 1 addition & 0 deletions test/Project.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[deps]
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
Herb = "c09c6b7f-4f63-49de-90d9-97a3563c0f4a"
JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
6 changes: 5 additions & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,9 @@ using JET
@testset "Code linting (JET.jl)" begin
JET.test_package(Garden; target_defined_modules = true)
end
# Write your tests here.
for (root, dirs, files) in walkdir(@__DIR__)
for file in filter(contains(r"test_"), files)
include(file)
end
end
end
Loading