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

Add mat"" custom string literal #29

Merged
merged 3 commits into from
Apr 16, 2015
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
51 changes: 33 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,41 +224,56 @@ This example will create a MAT file called ``test.mat``, which contains six MATL

#### Basic Use

To evaluate expressions in MATLAB, one may open a MATLAB engine session and communicate with it.
To evaluate expressions in MATLAB, one may open a MATLAB engine session and communicate with it. There are three ways to call MATLAB from Julia:

Below is a simple example that illustrates how one can use MATLAB from within Julia:
- The `mat""` custom string literal allows you to write MATLAB syntax inside Julia and use Julia variables directly from MATLAB via interpolation
- The `@matlab` macro, in combination with `@mput` and `@mget`, translates Julia syntax to MATLAB
- The `mxcall` function calls a given MATLAB function and returns the result

*Note:* There can be multiple (reasonable) ways to convert a MATLAB variable to Julia array. For example, MATLAB represents a scalar using a 1-by-1 matrix. Here we have two choices in terms of converting such a matrix back to Julia: (1) convert to a scalar number, or (2) convert to a matrix of size 1-by-1.

##### The `mat""` custom string literal

Text inside the `mat""` custom string literal is in MATLAB syntax. Variables from Julia can be "interpolated" into MATLAB code by prefixing them with a dollar sign as you would interpolate them into an ordinary string.

```julia
using MATLAB

restart_default_msession() # Open a default MATLAB session

x = linspace(-10., 10., 500)
mat"plot($x, sin($x))" # evaluate a MATLAB function

@mput x # put x to MATLAB's workspace
@matlab plot(x, sin(x)) # evaluate a MATLAB function

close_default_msession() # close the default session (optional)
y = linspace(2., 3., 500)
mat"""
$u = $x + $y
$v = $x - $y
"""
@show u v # u and v are accessible from Julia
```

You can put multiple variable and evaluate multiple statement by calling ``@mput`` and ``@matlab`` once:
As with ordinary string literals, you can also interpolate whole Julia expressions, e.g. `mat"$(x[1]) = $(x[2]) + $(binomial(5, 2))`.

##### The `@matlab` macro

The example above can also be written using the `@matlab` macro in combination with `@mput` and `@mget`.

```julia
using MATLAB

x = linspace(-10., 10., 500)
y = linspace(2., 3., 500)
@mput x # put x to MATLAB's workspace
@matlab plot(x, sin(x)) # evaluate a MATLAB function

@mput x y
y = linspace(2., 3., 500)
@mput y
@matlab begin
u = x + y
v = x - y
end
@mget u v
@show u v
```

*Note:* There can be multiple (reasonable) ways to convert a MATLAB variable to Julia array. For example, MATLAB represents a scalar using a 1-by-1 matrix. Here we have two choice in terms of converting such a matrix back to Julia: (1) convert to a scalar number, or (2) convert to a matrix of size 1-by-1.

Here, ``get_mvariable`` returns an instance of ``MxArray``, and the user can make his own choice by calling ``jarray``, ``jvector``, or ``jscalar`` to convert it to a Julia variable.

#### Caveats of @matlab
###### Caveats of @matlab

Note that some MATLAB expressions are not valid Julia expressions. This package provides some ways to work around this in the ``@matlab`` macro:

Expand All @@ -282,9 +297,9 @@ While we try to cover most MATLAB statements, some valid MATLAB statements remai
eval_string("[u, v] = myfun(x, y);")
```

#### mxcall
##### `mxcall`

You may also directly call a MATLAB function on Julia variables
You may also directly call a MATLAB function on Julia variables using `mxcall`:

```julia
x = [-10.:0.1:10.]
Expand Down
3 changes: 2 additions & 1 deletion src/MATLAB.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ module MATLAB
export eval_string, get_mvariable, get_variable, put_variable, put_variables
export variable_names, read_matfile, write_matfile
export mxcall
export @mput, @mget, @matlab
export @mput, @mget, @matlab, @mat_str, @mat_mstr


import Base.eltype, Base.close, Base.size, Base.copy, Base.ndims, Compat.unsafe_convert
Expand All @@ -38,5 +38,6 @@ module MATLAB

include("mstatements.jl")
include("engine.jl")
include("matstr.jl")

end # module
1 change: 1 addition & 0 deletions src/engine.jl
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ function close_default_msession()
global default_msession
if !(default_msession == nothing)
close(default_msession)
default_msession = nothing
end
end

Expand Down
166 changes: 166 additions & 0 deletions src/matstr.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# Syntax for mat"" string interpolation

# A really basic parser intended only to handle checking whether
# a variable is on the left or right hand side of an expression
type DumbParserState
paren_depth::Int
in_string::Bool
end
DumbParserState() = DumbParserState(0, false)

# Returns true if an = is encountered and updates pstate
function dumb_parse!(pstate::DumbParserState, str::String)
paren_depth = pstate.paren_depth
in_string = pstate.in_string
x = '\0'
s = start(str)
while !done(str, s)
lastx = x
(x, s) = next(str, s)
if in_string
if x == '\''
if !done(str, s) && next(str, s)[1] == '\''
(x, s) = next(str, s)
else
in_string = false
end
end
else
if x == '('
paren_depth += 1
elseif x == ')'
paren_depth -= 1
elseif x == '\'' && lastx in ",( \t\0;"
in_string = true
elseif x == '=' && !(lastx in "<>~")
if !done(str, s) && next(str, s)[1] == '='
(x, s) = next(str, s)
else
return true
end
elseif x == '%'
break
end
end
end
pstate.paren_depth = paren_depth
pstate.in_string = in_string
return false
end

# Check if a given variable is assigned, used, or both. Returns the#
# assignment and use status
function check_assignment(interp, i)
# Go back to the last newline
before = String[]
for j = i-1:-1:1
if isa(interp[j], String)
sp = split(interp[j], "\n")
unshift!(before, sp[end])
for k = length(sp)-1:-1:1
match(r"\.\.\.[ \t]*\r?$", sp[k]) == nothing && @goto done_before
unshift!(before, sp[k])
end
end
end
@label done_before

# Check if this reference is inside parens at the start, or on the rhs of an assignment
pstate = DumbParserState()
(dumb_parse!(pstate, join(before)) || pstate.paren_depth > 1) && return (false, true)

# Go until the next newline or comment
after = String[]
both_sides = false
for j = i+1:length(interp)
if isa(interp[j], String)
sp = split(interp[j], "\n")
push!(after, sp[1])
for k = 2:length(sp)
match(r"\.\.\.[ \t]*\r?$", sp[k-1]) == nothing && @goto done_after
push!(after, sp[k])
end
elseif interp[j] == interp[i]
both_sides = true
end
end
@label done_after

assigned = dumb_parse!(pstate, join(after))
used = !assigned || both_sides || (i < length(interp) && match(r"^[ \t]*\(", interp[i+1]) != nothing)
return (assigned, used)
end

function do_mat_str(ex)
# Hack to do interpolation
interp = parse(string("\"\"\"", replace(ex, "\"\"\"", "\\\"\"\""), "\"\"\""))
@assert interp.head == :macrocall
interp = interp.args[2:end]

# Handle interpolated variables
putblock = Expr(:block)
getblock = Expr(:block)
usedvars = Set{Symbol}()
assignedvars = Set{Symbol}()
varmap = Dict{Symbol,Symbol}()
for i = 1:length(interp)
if !isa(interp[i], String)
# Don't put the same symbol to MATLAB twice
if haskey(varmap, interp[i])
var = varmap[interp[i]]
else
var = symbol(string("matlab_jl_", i))
if isa(interp[i], Symbol)
varmap[interp[i]] = var
end
end

# Try to determine if variable is being used in an assignment
(assigned, used) = check_assignment(interp, i)

if used && !(var in usedvars)
push!(usedvars, var)
(var in assignedvars) || push!(putblock.args, :(put_variable($(Meta.quot(var)), $(esc(interp[i])))))
end
if assigned && !(var in assignedvars)
push!(assignedvars, var)
push!(getblock.args, Expr(:(=), esc(interp[i]), :(get_variable($(Meta.quot(var))))))
end

interp[i] = var
end
end

# Clear `ans` and set `matlab_jl_has_ans` before we run the code
unshift!(interp, "clear ans;\nmatlab_jl_has_ans = 0;\n")

# Add a semicolon to the end of the last statement to suppress output
interp[end] = rstrip(interp[end])
push!(interp, ";")

# Figure out if `ans` exists in code to avoid an error if it doesn't
push!(interp, "\nmatlab_jl_has_ans = exist('ans', 'var');")

quote
$(putblock)
eval_string($(join(interp)))
$(getblock)
$(if !isempty(usedvars) || !isempty(assignedvars)
# Clear variables we created
:(eval_string($(string("clear ", join(union(usedvars, assignedvars), " "), ";"))))
end)
if get_variable(:matlab_jl_has_ans) != 0
# Return ans if it was set
get_variable(:ans)
end
end
end

macro mat_str(ex)
do_mat_str(ex)
end

# Only needed for Julia 0.3
macro mat_mstr(ex)
do_mat_str(ex)
end
71 changes: 71 additions & 0 deletions test/matstr.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using MATLAB, Base.Test

@test mat"1" == 1
@test mat"[1, 2, 3]" == [1 2 3]

# Test interpolation
x = 1
@test mat"$x + 1" == 2

ret = mat"$y = $x + 2"
@test ret == nothing
@test y == 3

ret = mat"$y = $(x + 3)"
@test ret == nothing
@test y == 4

x = 5
@test mat"$x == 5"

# Test assignment
x = [1, 2, 3, 4, 5]
ret = mat"$x(1:3) = 1"
@test ret == nothing
@test x == [1, 1, 1, 4, 5]
ret = mat"$(x[1:3]) = 2"
@test ret == nothing
@test x == [2, 2, 2, 4, 5]

# Test a more complicated case with assignments on LHS and RHS
x = 20
mat"""
for i = 1:10
$x = $x + 1;
end
"""

# Test assignment then use
ret = mat"""
$z = 5;
$q = $z;
"""
@test ret == nothing
@test z == 5
@test q == 5

# Test multiple assignment
ret = mat"[$a, $b] = sort([4, 3])"
@test ret == nothing
@test a == [3 4]
@test b == [2 1]

# Test comments
a = 5
@test mat"$a + 1; % = 2" == 6

# Test indexing
c = [1, 2]
@test mat"$c($c == 2)" == 2

# Test line continuations
ret = mat"""
$d ...
= 3
"""
@test ret == nothing
@test d == 3

# Test strings with =
text = "hello = world"
@test mat"strfind($text, 'o = w')" == 5
5 changes: 5 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
include("engine.jl")
include("matfile.jl")
include("matstr.jl")
include("mstatements.jl")
include("mxarray.jl")