Improve resolution of relative includes #980

Merged
zxq9 merged 6 commits from fix_relative_includes into master 2023-09-14 22:00:30 +09:00
10 changed files with 96 additions and 19 deletions

View File

@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added ### Added
### Changed ### 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 ### Removed
### Fixed ### Fixed

View File

@ -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, 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. 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 ## Standard library
Sophia offers [standard library](sophia_stdlib.md) which exposes some Sophia offers [standard library](sophia_stdlib.md) which exposes some

View File

@ -42,6 +42,7 @@
| {include, {file_system, [string()]} | | {include, {file_system, [string()]} |
{explicit_files, #{string() => binary()}}} {explicit_files, #{string() => binary()}}}
| {src_file, string()} | {src_file, string()}
| {src_dir, string()}
| {aci, aeso_aci:aci_type()}. | {aci, aeso_aci:aci_type()}.
-type options() :: [option()]. -type options() :: [option()].
@ -87,7 +88,9 @@ file(Filename) ->
file(File, Options0) -> file(File, Options0) ->
Options = add_include_path(File, Options0), Options = add_include_path(File, Options0),
case read_contract(File) of 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} -> {error, Error} ->
Msg = lists:flatten([File,": ",file:format_error(Error)]), Msg = lists:flatten([File,": ",file:format_error(Error)]),
{error, [aeso_errors:new(file_error, Msg)]} {error, [aeso_errors:new(file_error, Msg)]}
@ -99,7 +102,7 @@ add_include_path(File, Options) ->
false -> false ->
Dir = filename:dirname(File), Dir = filename:dirname(File),
{ok, Cwd} = file:get_cwd(), {ok, Cwd} = file:get_cwd(),
[{include, {file_system, [Cwd, Dir]}} | Options] [{include, {file_system, [Cwd, aeso_utils:canonical_dir(Dir)]}} | Options]
end. end.
-spec from_string(binary() | string(), options()) -> {ok, map()} | {error, [aeso_errors:error()]}. -spec from_string(binary() | string(), options()) -> {ok, map()} | {error, [aeso_errors:error()]}.

View File

@ -15,7 +15,7 @@
many/1, many1/1, sep/2, sep1/2, many/1, many1/1, sep/2, sep1/2,
infixl/2, infixr/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]). current_include_type/0, set_current_include_type/1]).
%% -- Types ------------------------------------------------------------------ %% -- Types ------------------------------------------------------------------
@ -480,6 +480,13 @@ current_file() ->
set_current_file(File) -> set_current_file(File) ->
put('$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({L, C}) -> {current_file(), L, C};
add_current_file(Pos) -> Pos. add_current_file(Pos) -> Pos.

View File

@ -19,6 +19,7 @@
-include("aeso_parse_lib.hrl"). -include("aeso_parse_lib.hrl").
-import(aeso_parse_lib, [current_file/0, set_current_file/1, -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]). current_include_type/0, set_current_include_type/1]).
-type parse_result() :: aeso_syntax:ast() | {aeso_syntax:ast(), sets:set(include_hash())} | none(). -type parse_result() :: aeso_syntax:ast() | {aeso_syntax:ast(), sets:set(include_hash())} | none().
@ -58,6 +59,7 @@ run_parser(P, Inp, Opts) ->
parse_and_scan(P, S, Opts) -> parse_and_scan(P, S, Opts) ->
set_current_file(proplists:get_value(src_file, Opts, no_file)), 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)), set_current_include_type(proplists:get_value(include_type, Opts, none)),
case aeso_scan:scan(S) of case aeso_scan:scan(S) of
{ok, Tokens} -> aeso_parse_lib:parse(P, Tokens); {ok, Tokens} -> aeso_parse_lib:parse(P, Tokens);
@ -556,6 +558,7 @@ bracket_list(P) -> brackets(comma_sep(P)).
-spec pos_ann(ann_line(), ann_col()) -> ann(). -spec pos_ann(ann_line(), ann_col()) -> ann().
pos_ann(Line, Col) -> pos_ann(Line, Col) ->
[ {file, current_file()} [ {file, current_file()}
, {dir, current_dir()}
, {include_type, current_include_type()} , {include_type, current_include_type()}
, {line, Line} , {line, Line}
, {col, Col} ]. , {col, Col} ].
@ -696,7 +699,7 @@ expand_includes([], Included, Acc, Opts) ->
end; end;
expand_includes([{include, Ann, {string, _SAnn, File}} | AST], Included, Acc, Opts) -> expand_includes([{include, Ann, {string, _SAnn, File}} | AST], Included, Acc, Opts) ->
case get_include_code(File, Ann, Opts) of case get_include_code(File, Ann, Opts) of
{ok, Code} -> {ok, AbsDir, Code} ->
Hashed = hash_include(File, Code), Hashed = hash_include(File, Code),
case sets:is_element(Hashed, Included) of case sets:is_element(Hashed, Included) of
false -> false ->
@ -706,9 +709,10 @@ expand_includes([{include, Ann, {string, _SAnn, File}} | AST], Included, Acc, Op
_ -> indirect _ -> indirect
end, end,
Opts1 = lists:keystore(src_file, 1, Opts, {src_file, File}), 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), Included1 = sets:add_element(Hashed, Included),
case parse_and_scan(file(), Code, Opts2) of case parse_and_scan(file(), Code, Opts3) of
{ok, AST1} -> {ok, AST1} ->
expand_includes(AST1 ++ AST, Included1, Acc, Opts); expand_includes(AST1 ++ AST, Included1, Acc, Opts);
Err = {error, _} -> Err = {error, _} ->
@ -726,13 +730,12 @@ expand_includes([E | AST], Included, Acc, Opts) ->
read_file(File, Opts) -> read_file(File, Opts) ->
case proplists:get_value(include, Opts, {explicit_files, #{}}) of case proplists:get_value(include, Opts, {explicit_files, #{}}) of
{file_system, Paths} -> {file_system, Paths} ->
CandidateNames = [ filename:join(Dir, File) || Dir <- Paths ], lists:foldr(fun(Path, {error, _}) -> read_file_(Path, File);
lists:foldr(fun(F, {error, _}) -> file:read_file(F); (_Path, OK) -> OK end, {error, not_found}, Paths);
(_F, OK) -> OK end, {error, not_found}, CandidateNames);
{explicit_files, Files} -> {explicit_files, Files} ->
case maps:get(binary_to_list(File), Files, not_found) of case maps:get(binary_to_list(File), Files, not_found) of
not_found -> {error, not_found}; not_found -> {error, not_found};
Src -> {ok, Src} Src -> {ok, File, Src}
end; end;
escript -> escript ->
try try
@ -741,7 +744,7 @@ read_file(File, Opts) ->
Archive = proplists:get_value(archive, Sections), Archive = proplists:get_value(archive, Sections),
FileName = binary_to_list(filename:join([aesophia, priv, stdlib, File])), FileName = binary_to_list(filename:join([aesophia, priv, stdlib, File])),
case zip:extract(Archive, [{file_list, [FileName]}, memory]) of case zip:extract(Archive, [{file_list, [FileName]}, memory]) of
{ok, [{_, Src}]} -> {ok, Src}; {ok, [{_, Src}]} -> {ok, escript, Src};
_ -> {error, not_found} _ -> {error, not_found}
end end
catch _:_ -> catch _:_ ->
@ -749,6 +752,13 @@ read_file(File, Opts) ->
end end
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() -> stdlib_options() ->
StdLibDir = aeso_stdlib:stdlib_include_path(), StdLibDir = aeso_stdlib:stdlib_include_path(),
case filelib:is_dir(StdLibDir) of case filelib:is_dir(StdLibDir) of
@ -757,23 +767,37 @@ stdlib_options() ->
end. end.
get_include_code(File, Ann, Opts) -> get_include_code(File, Ann, Opts) ->
case {read_file(File, Opts), read_file(File, stdlib_options())} of %% Temporarily extend include paths with the directory of the current file
{{ok, Bin}, {ok, _}} -> 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 case filename:basename(File) == File of
true -> { error true -> { error
, fail( ann_pos(Ann) , fail( ann_pos(Ann)
, "Illegal redefinition of standard library " ++ binary_to_list(File))}; , "Illegal redefinition of standard library " ++ binary_to_list(File))};
%% If a path is provided then the stdlib takes lower priority %% 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; end;
{_, {ok, Bin}} -> {_, {ok, _, Bin}} ->
{ok, binary_to_list(Bin)}; {ok, stdlib, binary_to_list(Bin)};
{{ok, Bin}, _} -> {{ok, Dir, Bin}, _} ->
{ok, binary_to_list(Bin)}; {ok, Dir, binary_to_list(Bin)};
{_, _} -> {_, _} ->
{error, {ann_pos(Ann), include_error, File}} {error, {ann_pos(Ann), include_error, File}}
end. 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(). -spec hash_include(string() | binary(), string()) -> include_hash().
hash_include(File, Code) when is_binary(File) -> hash_include(File, Code) when is_binary(File) ->
hash_include(binary_to_list(File), Code); hash_include(binary_to_list(File), Code);

View File

@ -6,10 +6,22 @@
%%%------------------------------------------------------------------- %%%-------------------------------------------------------------------
-module(aeso_utils). -module(aeso_utils).
-export([scc/1]). -export([scc/1, canonical_dir/1]).
-export_type([graph/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 %% -- Topological sort
-type graph(Node) :: #{Node => [Node]}. %% List of incoming edges (dependencies). -type graph(Node) :: #{Node => [Node]}. %% List of incoming edges (dependencies).

View File

@ -161,6 +161,7 @@ compilable_contracts() ->
"state_handling", "state_handling",
"events", "events",
"include", "include",
"relative_include",
"basic_auth", "basic_auth",
"basic_auth_tx", "basic_auth_tx",
"bitcoin_auth", "bitcoin_auth",

View File

@ -0,0 +1,4 @@
include "../dir2/baz.aes"
namespace D =
function g() = E.h()

View File

@ -0,0 +1,3 @@
namespace E =
function h() = 42

View File

@ -0,0 +1,3 @@
include "./dir1/bar.aes"
contract C =
entrypoint f() = D.g()