Skip to content

Commit

Permalink
--wip--
Browse files Browse the repository at this point in the history
  • Loading branch information
TotalVerb committed Apr 29, 2017
1 parent 388012a commit 3c18110
Show file tree
Hide file tree
Showing 7 changed files with 358 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/Lint.jl
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ end
# needed for BROADCAST
include("compat.jl")
using .LintCompat
include("RequiredKeywordArguments.jl")
include("SemanticASTs/SemanticASTs.jl")
include("exprutils.jl")
using .ExpressionUtils

Expand Down
31 changes: 31 additions & 0 deletions src/RequiredKeywordArguments.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module RequiredKeywordArguments

using Base.Meta
export @required

"""
@required(arg)
@required(arg::Type)
Indicates that the given keyword argument is required; that is, an
`ArgumentError` will be thrown if it is not provided.
"""
macro required(ex)
target = if isexpr(ex, :(::))
ex.args[1]
elseif isa(ex, Symbol)
ex
else
throw(ArgumentError("@required($ex) does not match expected format"))
end

# TODO: fix when 0.5 support dropped
# This is the right code, but it doesn't work on 0.5, so we make do with
# the wronger version.
# Expr(:kw, esc(ex),
# :(throw(ArgumentError($"keyword argument $target is required"))))
esc(Expr(:kw, ex,
:(throw(ArgumentError($"keyword argument $target is required")))))
end

end
80 changes: 80 additions & 0 deletions src/SemanticASTs/SemanticASTs.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""
Lint.SemanticASTs
This module provides types for an annotated abstract syntax tree with nodes
based off semantic information.
"""
module SemanticASTs

using ..RequiredKeywordArguments

using Base.Meta
using Compat
import Base.show

# for code readability purposes
const AST = Any

export SemanticAST, FunctionName, InstanceFunctionName, TypeFunctionName,
Signature, Quantification, Lambda, MethodDefinition, Unannotated,
annotateⁿ, rawastⁿ

"""
SemanticAST
An annotated expression. An expression is something which can be meaningfully
evaluated into a value in a particular context. For example, `:(1 + 1)` and
`:foo` are expressions. In Julia, even `type Foo end` is an expression, because
it evaluates to `nothing` in the global context.
"""
@compat abstract type SemanticAST end

rawastⁿ(ast::SemanticAST) = Nullable(rawast(ast))
nameⁿ(ast::SemanticAST) = Nullable(name(ast))

include("functionname.jl")
include("functiondef.jl")

immutable Unannotated <: SemanticAST
_ast :: AST
end
rawast(ast::Unannotated) = ast._ast

function show(io::IO, ast::Unannotated)
print(io, "Unannotated(")
show(io, get(rawastⁿ(ast)))
print(io, ")")
end

"""
SemanticAST(x::Expr)
```jldoctest
julia> SemanticAST(:(f(x) = x))
MethodDefinition(
to=InstanceFunctionName(func=Unannotated(:f)),
method=Lambda(signature=Signature(:((x,))))
)
julia> functionname(:(function (::Type{Foo})(); Foo(1); end))
:Foo
julia> functionname(:(function (p::Polynomial)(); p(0); end))
:(::Polynomial)
julia> functionname(:(macro foo(); end))
Symbol("@foo")
```
"""
function SemanticAST(ex::Expr)
if isexpr(ex, :function)
# parse as function
elseif isexpr(ex, :macro)
else
Unannotated(ex)
end
end

SemanticAST(ex) = Unannotated(ex)

end
153 changes: 153 additions & 0 deletions src/SemanticASTs/functiondef.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
"""
Signature
The signature of a function.
"""
immutable Signature <: SemanticAST
_ast :: AST
end
rawast(ast::Signature) = ast._ast

function show(io::IO, ast::Signature)
print(io, "Signature(")
show(io, rawast(ast))
print(io, ")")
end

"""
Quantification
A parameter introduced by `where` in a method definition, including any bounds;
for example, the `T <: Number` in `f(x::T, y::T) where T <: Number`.
"""
immutable Quantification <: SemanticAST
_ast :: Nullable{AST}
name :: Symbol
lowerbound :: Nullable{SemanticAST}
upperbound :: Nullable{SemanticAST}
end
name(ast::Quantification) = ast.name
lowerboundⁿ(ast::Quantification) = ast.lowerbound
upperboundⁿ(ast::Quantification) = ast.upperbound

Quantification(ast=Nullable();
@required(name::Symbol),
lowerbound=Nullable(),
upperbound=Nullable()
) = Quantification(ast, name, lowerbound, upperbound)

function show(io::IO, ast::Quantification)
print(io, "Quantification(name=")
show(io, name(ast))
if !isnull(lowerboundⁿ(ast))
print(io, ", lowerbound=", lowerboundⁿ(ast))
end
if !isnull(upperboundⁿ(ast))
print(io, ", upperbound=", upperboundⁿ(ast))
end
print(io, ")")
end

function annotateⁿ(::Type{Quantification}, ast::AST)::Nullable
name = if isa(ast, Symbol)
Quantification(name=ast)
elseif isexpr(ast, :comparison) && length(ast.args) == 5 &&
ast.args[2] == ast.args[4] == :(<:) && isa(ast.args[3], Symbol)
Quantification(name=ast.args[3],
lowerbound=SemanticAST(ast.args[1]),
upperbound=SemanticAST(ast.args[5]))
elseif isexpr(ast, :(<:)) && isa(ast.args[1], Symbol)
Quantification(name=ast.args[1], upperbound=SemanticAST(ast.args[2]))
elseif isexpr(ast, :(>:)) && isa(ast.args[1], Symbol)
Quantification(name=ast.args[1], lowerbound=SemanticAST(ast.args[2]))
else
Nullable()
end
end

"""
FunctionDefinition
A definition of a named or anonymous function.
"""
@compat abstract type FunctionDefinition <: SemanticAST end

"""
Lambda
An anonymous function, including the signature, body, any static parameters,
and return type declaration if applicable.
"""
immutable Lambda <: FunctionDefinition
_ast :: Nullable{AST}
sparams :: Vector{Quantification}
signature :: Signature
returntype :: Nullable{SemanticAST}
code :: SemanticAST
end
rawastⁿ(ast::Lambda) = ast._ast
sparams(ast::Lambda) = ast.sparams
signature(ast::Lambda) = ast.signature
returntypeⁿ(ast::Lambda) = ast.returntype
code(ast::Lambda) = ast.code

Lambda(ast=Nullable();
sparams=Quantification[],
@required(signature::Signature),
returntype=Nullable(),
@required(code::SemanticAST)
) = Lambda(ast, sparams, signature, returntype, code)

function show(io::IO, ast::Lambda)
print(io, "Lambda(")
if !isempty(sparams(ast))
print("sparams=", sparams(ast), ", ")
end
print("signature=", signature(ast), ", ")
if !isnull(returntypeⁿ(ast))
print("returntype=", get(returntype(ast)), ", ")
end
print("code=", code(ast), ")")
end

"""
MethodDefinition
A definition of a method, including the function name, signature, body, any
static parameters, and return type declaration if applicable.
"""
immutable MethodDefinition <: FunctionDefinition
_ast :: Nullable{AST}
sparams :: Vector{Quantification}
func :: FunctionName
signature :: Signature
returntype :: Nullable{SemanticAST}
code :: SemanticAST
end
rawastⁿ(ast::MethodDefinition) = ast._ast
sparams(ast::MethodDefinition) = ast.sparams
func(ast::MethodDefinition) = ast.func
signature(ast::MethodDefinition) = ast.signature
returntypeⁿ(ast::MethodDefinition) = ast.returntype
code(ast::MethodDefinition) = ast.code

MethodDefinition(ast=Nullable();
sparams=Quantification[],
@required(func::FunctionName),
@required(signature::Signature),
returntype=Nullable(),
@required(code::SemanticAST)
) = MethodDefinition(ast, sparams, func, signature, returntype, code)

function show(io::IO, ast::MethodDefinition)
print(io, "MethodDefinition(")
if !isempty(sparams(ast))
print("sparams=", sparams(ast), ", ")
end
print("func=", func(ast), ", ")
print("signature=", signature(ast), ", ")
if !isnull(returntypeⁿ(ast))
print("returntype=", get(returntype(ast)), ", ")
end
print("code=", code(ast), ")")
end
78 changes: 78 additions & 0 deletions src/SemanticASTs/functionname.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""
FunctionName
A function name is something that methods can validly be added to.
"""
@compat abstract type FunctionName <: SemanticAST end

"""
InstanceFunctionName
A function name for a particular instance, as in `f(x)` or `Base.show(x, y)`.
The instance here should represent a singleton, or a simple name in the case of
a closure.
"""
immutable InstanceFunctionName <: FunctionName
_ast :: Nullable{AST}
instance :: SemanticAST
end
rawastⁿ(ast::InstanceFunctionName) = ast._ast
instance(ast::InstanceFunctionName) = ast.instance

InstanceFunctionName(ast=Nullable();
@required(instance::SemanticAST)
) = InstanceFunctionName(ast, instance)

function show(io::IO, ast::InstanceFunctionName)
print(io, "InstanceFunctionName(instance=", instance(ast), ")")
end

"""
TypeFunctionName
A function name for a particular type, as in `(::Polynomial)(x)`. Optionally,
the function name is given a name, like in `(p::Polynomial)(x)`.
"""
immutable TypeFunctionName <: FunctionName
_ast :: Nullable{AST}
name :: Nullable{Symbol}
paramtype :: SemanticAST
end
rawastⁿ(ast::TypeFunctionName) = ast._ast
nameⁿ(ast::TypeFunctionName) = ast.name
paramtype(ast::TypeFunctionName) = ast.paramtype

TypeFunctionName(ast=Nullable();
name=Nullable(),
@required(paramtype::SemanticAST)
) = TypeFunctionName(ast, name, paramtype)

function show(io::IO, ast::TypeFunctionName)
print(io, "TypeFunctionName(")
if !isnull(nameⁿ(ast))
print(io, "name=")
show(io, get(nameⁿ(ast)))
print(io, ", ")
end
print(io, "paramtype=", paramtype(ast), ")")
end

"""
annotateⁿ(FunctionName, ast)
Return, wrapped in a `Nullable`, the given `ast` with extra semantic
information computed given that `ast` represents the function name part of a
method definition. If it cannot be understood that way, return `Nullable()`.
"""
function annotateⁿ(::Type{FunctionName}, ast::AST)::Nullable
if isexpr(ast, :(::))
if length(ast.args) == 1
return TypeFunctionName(ast; paramtype=Unannotated(ast.args[1]))
elseif length(ast.args) == 2 && isa(ast.args[1], Symbol)
return TypeFunctionName(ast; name=ast.args[1], paramtype=Unannotated(ast.args[2]))
end
elseif isa(ast, Symbol) || isexpr(ast, :(.)) || isexpr(ast, :curly)
return InstanceFunctionName(ast; instance=Unannotated(ast))
end
Nullable()
end
13 changes: 13 additions & 0 deletions test/requiredargs.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Lint.RequiredKeywordArguments
using Base.Test

@testset "Required Arguments" begin
foo1(; @required(bar)) = bar
@test foo1(bar=42) == 42
@test_throws ArgumentError foo1()

foo2(; @required(bar::Integer)) = bar
@test foo2(bar=42) == 42
@test_throws TypeError foo2(bar="x")
@test_throws ArgumentError foo2()
end
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ messageset(msgs) = Set(x.code for x in msgs)

include("exprutils.jl")
include("statictype.jl")
include("requiredargs.jl")

@testset "Lint Messages" begin
include("messages.jl")
Expand Down

0 comments on commit 3c18110

Please sign in to comment.