From f94a37b02a0cf71254fb60cb6896fad1bb866c33 Mon Sep 17 00:00:00 2001 From: Hans Svensson Date: Thu, 7 Sep 2023 22:20:05 +0200 Subject: [PATCH 1/6] Add aeso_utils:canonical_dir/1 --- src/aeso_compiler.erl | 2 +- src/aeso_utils.erl | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/aeso_compiler.erl b/src/aeso_compiler.erl index 1ff6ec3..54ed8f3 100644 --- a/src/aeso_compiler.erl +++ b/src/aeso_compiler.erl @@ -99,7 +99,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()]}. diff --git a/src/aeso_utils.erl b/src/aeso_utils.erl index e86f9ca..dcf9b7d 100644 --- a/src/aeso_utils.erl +++ b/src/aeso_utils.erl @@ -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). -- 2.30.2 From dee5dd027bed0fa8d5507bd517c743e0c921d8ac Mon Sep 17 00:00:00 2001 From: Hans Svensson Date: Thu, 7 Sep 2023 22:20:33 +0200 Subject: [PATCH 2/6] Add current file directory when resolving includes --- src/aeso_parser.erl | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/aeso_parser.erl b/src/aeso_parser.erl index d1d92cb..5f37cd9 100644 --- a/src/aeso_parser.erl +++ b/src/aeso_parser.erl @@ -757,7 +757,9 @@ stdlib_options() -> end. 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 + Opts1 = include_current_file_dir(Opts, Ann), + case {read_file(File, Opts1), read_file(File, stdlib_options())} of {{ok, Bin}, {ok, _}} -> case filename:basename(File) == File of true -> { error @@ -774,6 +776,19 @@ get_include_code(File, Ann, Opts) -> {error, {ann_pos(Ann), include_error, File}} end. +include_current_file_dir(Opts, Ann) -> + case {proplists:get_value(file, Ann, undefined), + proplists:get_value(include, Opts, undefined)} of + {undefined, _} -> Opts; + {FromFile, {file_system, Paths}} -> + BaseDir = aeso_utils:canonical_dir(filename:dirname(FromFile)), + case lists:member(BaseDir, Paths) of + false -> [{include, {file_system, [BaseDir | 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); -- 2.30.2 From 46cdf80661a141bed0cb6a066fe62d37d68267b8 Mon Sep 17 00:00:00 2001 From: Hans Svensson Date: Thu, 7 Sep 2023 22:23:06 +0200 Subject: [PATCH 3/6] Add CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ce4b06..4c91bc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ 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. ### Removed ### Fixed -- 2.30.2 From 628046c007de6cff3240f40190d5238abd59d247 Mon Sep 17 00:00:00 2001 From: Hans Svensson Date: Fri, 8 Sep 2023 16:27:09 +0200 Subject: [PATCH 4/6] Add documentation --- CHANGELOG.md | 3 ++- docs/sophia_features.md | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c91bc2..3d90868 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +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. +- 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 diff --git a/docs/sophia_features.md b/docs/sophia_features.md index e8170c0..3025f82 100644 --- a/docs/sophia_features.md +++ b/docs/sophia_features.md @@ -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 -- 2.30.2 From 3ce4e7360ce352f9fb778facbb54fc9f1d3df537 Mon Sep 17 00:00:00 2001 From: Hans Svensson Date: Fri, 8 Sep 2023 16:27:49 +0200 Subject: [PATCH 5/6] Add a test case --- test/aeso_compiler_tests.erl | 1 + test/contracts/dir1/bar.aes | 4 ++++ test/contracts/dir2/baz.aes | 3 +++ test/contracts/relative_include.aes | 3 +++ 4 files changed, 11 insertions(+) create mode 100644 test/contracts/dir1/bar.aes create mode 100644 test/contracts/dir2/baz.aes create mode 100644 test/contracts/relative_include.aes diff --git a/test/aeso_compiler_tests.erl b/test/aeso_compiler_tests.erl index 26c5d64..b0f2008 100644 --- a/test/aeso_compiler_tests.erl +++ b/test/aeso_compiler_tests.erl @@ -161,6 +161,7 @@ compilable_contracts() -> "state_handling", "events", "include", + "relative_include", "basic_auth", "basic_auth_tx", "bitcoin_auth", diff --git a/test/contracts/dir1/bar.aes b/test/contracts/dir1/bar.aes new file mode 100644 index 0000000..abc4aed --- /dev/null +++ b/test/contracts/dir1/bar.aes @@ -0,0 +1,4 @@ +include "../dir2/baz.aes" +namespace D = + function g() = E.h() + diff --git a/test/contracts/dir2/baz.aes b/test/contracts/dir2/baz.aes new file mode 100644 index 0000000..e14962f --- /dev/null +++ b/test/contracts/dir2/baz.aes @@ -0,0 +1,3 @@ +namespace E = + function h() = 42 + diff --git a/test/contracts/relative_include.aes b/test/contracts/relative_include.aes new file mode 100644 index 0000000..e64ecb1 --- /dev/null +++ b/test/contracts/relative_include.aes @@ -0,0 +1,3 @@ +include "./dir1/bar.aes" +contract C = + entrypoint f() = D.g() -- 2.30.2 From 7f36e980dd44ee68158031a295a1326d6f0b01dc Mon Sep 17 00:00:00 2001 From: Hans Svensson Date: Fri, 8 Sep 2023 16:30:46 +0200 Subject: [PATCH 6/6] Properly keep track of src_dir --- src/aeso_compiler.erl | 5 ++++- src/aeso_parse_lib.erl | 9 +++++++- src/aeso_parser.erl | 47 +++++++++++++++++++++++++----------------- 3 files changed, 40 insertions(+), 21 deletions(-) diff --git a/src/aeso_compiler.erl b/src/aeso_compiler.erl index 54ed8f3..5968081 100644 --- a/src/aeso_compiler.erl +++ b/src/aeso_compiler.erl @@ -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()]. @@ -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)]} diff --git a/src/aeso_parse_lib.erl b/src/aeso_parse_lib.erl index 4c50527..8246ef6 100644 --- a/src/aeso_parse_lib.erl +++ b/src/aeso_parse_lib.erl @@ -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 ------------------------------------------------------------------ @@ -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. diff --git a/src/aeso_parser.erl b/src/aeso_parser.erl index 5f37cd9..3c03bf0 100644 --- a/src/aeso_parser.erl +++ b/src/aeso_parser.erl @@ -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(). @@ -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); @@ -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} ]. @@ -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 -> @@ -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, _} -> @@ -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 @@ -741,7 +744,7 @@ 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 _:_ -> @@ -749,6 +752,13 @@ read_file(File, Opts) -> 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 @@ -760,30 +770,29 @@ get_include_code(File, Ann, Opts) -> %% 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, Bin}, {ok, _}} -> + {{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(file, Ann, undefined), + case {proplists:get_value(dir, Ann, undefined), proplists:get_value(include, Opts, undefined)} of {undefined, _} -> Opts; - {FromFile, {file_system, Paths}} -> - BaseDir = aeso_utils:canonical_dir(filename:dirname(FromFile)), - case lists:member(BaseDir, Paths) of - false -> [{include, {file_system, [BaseDir | Paths]}} | Opts]; + {CurrDir, {file_system, Paths}} -> + case lists:member(CurrDir, Paths) of + false -> [{include, {file_system, [CurrDir | Paths]}} | Opts]; true -> Opts end; {_, _} -> Opts -- 2.30.2