Skip to content

Commit

Permalink
Improve resolution of relative includes (#489)
Browse files Browse the repository at this point in the history
* Add aeso_utils:canonical_dir/1

* Add current file directory when resolving includes

* Add CHANGELOG

* Add documentation

* Add a test case

* Properly keep track of src_dir
  • Loading branch information
hanssv authored Sep 14, 2023
1 parent 33229c3 commit 03d6dd6
Show file tree
Hide file tree
Showing 10 changed files with 96 additions and 19 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
### Changed
- Improve how includes with relative paths are resolved during parsing/compilation. Relative
include paths are now always relative to the file containing the `include` statement.
### Removed
### Fixed

Expand Down
18 changes: 18 additions & 0 deletions docs/sophia_features.md
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,24 @@ the file, except that error messages will refer to the original source
locations. The language will try to include each file at most one time automatically,
so even cyclic includes should be working without any special tinkering.

### Include files using relative paths

When including code from another file using the `include` statement, the path
is relative to _the file that includes it_. Consider the following file tree:
```
c1.aes
c3.aes
dir1/c2.aes
dir1/c3.aes
```

If `c1.aes` contains `include "c3.aes"` it will include the top level `c3.aes`,
while if `c2.aes` contained the same line it would as expected include
`dir1/c3.aes`.

Note: Prior to v7.5.0, it would consider the include path relative to _the main
contract file_ (or any explicitly set include path).

## Standard library

Sophia offers [standard library](sophia_stdlib.md) which exposes some
Expand Down
7 changes: 5 additions & 2 deletions src/aeso_compiler.erl
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
| {include, {file_system, [string()]} |
{explicit_files, #{string() => binary()}}}
| {src_file, string()}
| {src_dir, string()}
| {aci, aeso_aci:aci_type()}.
-type options() :: [option()].

Expand Down Expand Up @@ -87,7 +88,9 @@ file(Filename) ->
file(File, Options0) ->
Options = add_include_path(File, Options0),
case read_contract(File) of
{ok, Bin} -> from_string(Bin, [{src_file, File} | Options]);
{ok, Bin} ->
SrcDir = aeso_utils:canonical_dir(filename:dirname(File)),
from_string(Bin, [{src_file, File}, {src_dir, SrcDir} | Options]);
{error, Error} ->
Msg = lists:flatten([File,": ",file:format_error(Error)]),
{error, [aeso_errors:new(file_error, Msg)]}
Expand All @@ -99,7 +102,7 @@ add_include_path(File, Options) ->
false ->
Dir = filename:dirname(File),
{ok, Cwd} = file:get_cwd(),
[{include, {file_system, [Cwd, Dir]}} | Options]
[{include, {file_system, [Cwd, aeso_utils:canonical_dir(Dir)]}} | Options]
end.

-spec from_string(binary() | string(), options()) -> {ok, map()} | {error, [aeso_errors:error()]}.
Expand Down
9 changes: 8 additions & 1 deletion src/aeso_parse_lib.erl
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
many/1, many1/1, sep/2, sep1/2,
infixl/2, infixr/2]).

-export([current_file/0, set_current_file/1,
-export([current_file/0, set_current_file/1, current_dir/0, set_current_dir/1,
current_include_type/0, set_current_include_type/1]).

%% -- Types ------------------------------------------------------------------
Expand Down Expand Up @@ -480,6 +480,13 @@ current_file() ->
set_current_file(File) ->
put('$current_file', File).

%% Current source directory
current_dir() ->
get('$current_dir').

set_current_dir(File) ->
put('$current_dir', File).

add_current_file({L, C}) -> {current_file(), L, C};
add_current_file(Pos) -> Pos.

54 changes: 39 additions & 15 deletions src/aeso_parser.erl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

-include("aeso_parse_lib.hrl").
-import(aeso_parse_lib, [current_file/0, set_current_file/1,
current_dir/0, set_current_dir/1,
current_include_type/0, set_current_include_type/1]).

-type parse_result() :: aeso_syntax:ast() | {aeso_syntax:ast(), sets:set(include_hash())} | none().
Expand Down Expand Up @@ -58,6 +59,7 @@ run_parser(P, Inp, Opts) ->

parse_and_scan(P, S, Opts) ->
set_current_file(proplists:get_value(src_file, Opts, no_file)),
set_current_dir(proplists:get_value(src_dir, Opts, no_file)),
set_current_include_type(proplists:get_value(include_type, Opts, none)),
case aeso_scan:scan(S) of
{ok, Tokens} -> aeso_parse_lib:parse(P, Tokens);
Expand Down Expand Up @@ -556,6 +558,7 @@ bracket_list(P) -> brackets(comma_sep(P)).
-spec pos_ann(ann_line(), ann_col()) -> ann().
pos_ann(Line, Col) ->
[ {file, current_file()}
, {dir, current_dir()}
, {include_type, current_include_type()}
, {line, Line}
, {col, Col} ].
Expand Down Expand Up @@ -696,7 +699,7 @@ expand_includes([], Included, Acc, Opts) ->
end;
expand_includes([{include, Ann, {string, _SAnn, File}} | AST], Included, Acc, Opts) ->
case get_include_code(File, Ann, Opts) of
{ok, Code} ->
{ok, AbsDir, Code} ->
Hashed = hash_include(File, Code),
case sets:is_element(Hashed, Included) of
false ->
Expand All @@ -706,9 +709,10 @@ expand_includes([{include, Ann, {string, _SAnn, File}} | AST], Included, Acc, Op
_ -> indirect
end,
Opts1 = lists:keystore(src_file, 1, Opts, {src_file, File}),
Opts2 = lists:keystore(include_type, 1, Opts1, {include_type, IncludeType}),
Opts2 = lists:keystore(src_dir, 1, Opts1, {src_dir, AbsDir}),
Opts3 = lists:keystore(include_type, 1, Opts2, {include_type, IncludeType}),
Included1 = sets:add_element(Hashed, Included),
case parse_and_scan(file(), Code, Opts2) of
case parse_and_scan(file(), Code, Opts3) of
{ok, AST1} ->
expand_includes(AST1 ++ AST, Included1, Acc, Opts);
Err = {error, _} ->
Expand All @@ -726,13 +730,12 @@ expand_includes([E | AST], Included, Acc, Opts) ->
read_file(File, Opts) ->
case proplists:get_value(include, Opts, {explicit_files, #{}}) of
{file_system, Paths} ->
CandidateNames = [ filename:join(Dir, File) || Dir <- Paths ],
lists:foldr(fun(F, {error, _}) -> file:read_file(F);
(_F, OK) -> OK end, {error, not_found}, CandidateNames);
lists:foldr(fun(Path, {error, _}) -> read_file_(Path, File);
(_Path, OK) -> OK end, {error, not_found}, Paths);
{explicit_files, Files} ->
case maps:get(binary_to_list(File), Files, not_found) of
not_found -> {error, not_found};
Src -> {ok, Src}
Src -> {ok, File, Src}
end;
escript ->
try
Expand All @@ -741,14 +744,21 @@ read_file(File, Opts) ->
Archive = proplists:get_value(archive, Sections),
FileName = binary_to_list(filename:join([aesophia, priv, stdlib, File])),
case zip:extract(Archive, [{file_list, [FileName]}, memory]) of
{ok, [{_, Src}]} -> {ok, Src};
{ok, [{_, Src}]} -> {ok, escript, Src};
_ -> {error, not_found}
end
catch _:_ ->
{error, not_found}
end
end.

read_file_(Path, File) ->
AbsFile = filename:join(Path, File),
case file:read_file(AbsFile) of
{ok, Bin} -> {ok, aeso_utils:canonical_dir(filename:dirname(AbsFile)), Bin};
Err -> Err
end.

stdlib_options() ->
StdLibDir = aeso_stdlib:stdlib_include_path(),
case filelib:is_dir(StdLibDir) of
Expand All @@ -757,23 +767,37 @@ stdlib_options() ->
end.

get_include_code(File, Ann, Opts) ->
case {read_file(File, Opts), read_file(File, stdlib_options())} of
{{ok, Bin}, {ok, _}} ->
%% Temporarily extend include paths with the directory of the current file
Opts1 = include_current_file_dir(Opts, Ann),
case {read_file(File, Opts1), read_file(File, stdlib_options())} of
{{ok, Dir, Bin}, {ok, _}} ->
case filename:basename(File) == File of
true -> { error
, fail( ann_pos(Ann)
, "Illegal redefinition of standard library " ++ binary_to_list(File))};
%% If a path is provided then the stdlib takes lower priority
false -> {ok, binary_to_list(Bin)}
false -> {ok, Dir, binary_to_list(Bin)}
end;
{_, {ok, Bin}} ->
{ok, binary_to_list(Bin)};
{{ok, Bin}, _} ->
{ok, binary_to_list(Bin)};
{_, {ok, _, Bin}} ->
{ok, stdlib, binary_to_list(Bin)};
{{ok, Dir, Bin}, _} ->
{ok, Dir, binary_to_list(Bin)};
{_, _} ->
{error, {ann_pos(Ann), include_error, File}}
end.

include_current_file_dir(Opts, Ann) ->
case {proplists:get_value(dir, Ann, undefined),
proplists:get_value(include, Opts, undefined)} of
{undefined, _} -> Opts;
{CurrDir, {file_system, Paths}} ->
case lists:member(CurrDir, Paths) of
false -> [{include, {file_system, [CurrDir | Paths]}} | Opts];
true -> Opts
end;
{_, _} -> Opts
end.

-spec hash_include(string() | binary(), string()) -> include_hash().
hash_include(File, Code) when is_binary(File) ->
hash_include(binary_to_list(File), Code);
Expand Down
14 changes: 13 additions & 1 deletion src/aeso_utils.erl
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,22 @@
%%%-------------------------------------------------------------------
-module(aeso_utils).

-export([scc/1]).
-export([scc/1, canonical_dir/1]).

-export_type([graph/1]).

%% -- Simplistic canonical directory
%% Note: no attempts to be 100% complete

canonical_dir(Dir) ->
{ok, Cwd} = file:get_cwd(),
AbsName = filename:absname(Dir),
RelAbsName = filename:join(tl(filename:split(AbsName))),
case filelib:safe_relative_path(RelAbsName, Cwd) of
unsafe -> AbsName;
Simplified -> filename:absname(Simplified, "")
end.

%% -- Topological sort

-type graph(Node) :: #{Node => [Node]}. %% List of incoming edges (dependencies).
Expand Down
1 change: 1 addition & 0 deletions test/aeso_compiler_tests.erl
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ compilable_contracts() ->
"state_handling",
"events",
"include",
"relative_include",
"basic_auth",
"basic_auth_tx",
"bitcoin_auth",
Expand Down
4 changes: 4 additions & 0 deletions test/contracts/dir1/bar.aes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
include "../dir2/baz.aes"
namespace D =
function g() = E.h()

3 changes: 3 additions & 0 deletions test/contracts/dir2/baz.aes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace E =
function h() = 42

3 changes: 3 additions & 0 deletions test/contracts/relative_include.aes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
include "./dir1/bar.aes"
contract C =
entrypoint f() = D.g()

0 comments on commit 03d6dd6

Please sign in to comment.