Skip to content

Module system#321

Open
Y-Nak wants to merge 37 commits intoargotorg:mainfrom
Y-Nak:module-system
Open

Module system#321
Y-Nak wants to merge 37 commits intoargotorg:mainfrom
Y-Nak:module-system

Conversation

@Y-Nak
Copy link
Member

@Y-Nak Y-Nak commented Feb 24, 2026

This PR is a PoC for the new module/namespace system and focuses on language behavior and import/export semantics.

The current implementation keeps a transitional flattening bridge so existing passes continue to work. If this direction is accepted, I'll start refactoring to remove that bridge by introducing module-aware lookup in name resolution/type checking and then cleaning up related paths.
TODOs:

  • External library support
  • Solidify the concept for the project root

Module and Namespace System

1. Entry File and Import Roots

  • The entry file is the path passed to -f / --file.
  • Import search roots are built in this order:
    1. takeDirectory(entryFile)
    2. directories from --include (colon-separated, default: std)
  • Imports are resolved by searching roots in order and picking the first existing file.

2. Module Identity and File Mapping

  • Module names are file-system driven.
  • import foo; resolves to foo.solc.
  • import foo.bar; resolves to foo/bar.solc.
  • Loaded module identity is the canonicalized file path.
  • A canonical file is loaded once, even if reached through multiple imports.

3. Import Syntax and Semantics

Supported forms:

import M;
import M as A;
import M.{X, Y};
import M.{*};

Behavior:

  • import M; and import M as A; add qualifier-based access only.
  • import M.{X, Y}; imports selected exported names into unqualified scope.
  • import M.{*}; imports all exported names into unqualified scope.
  • There is no open-import semantics.

Validation rules:

  • Duplicate qualifier names are rejected (import A as M; import B as M;).
  • Duplicate names inside one selective import are rejected.
  • Unknown selected names are rejected.
  • Ambiguous selected imports across modules are rejected.
  • Import cycles are rejected, with the cycle chain in the error.

Data constructor selection detail:

  • Selective import of a constructor (for example {True}) brings its parent data type declaration too, but only with selected constructors.

4. Export Syntax and Visibility

Supported forms:

export {X, Y};
export {*};

Current enforcement:

  • Imported modules must declare exactly one export declaration.
  • Entry module does not need an export declaration.
  • Multiple export declarations are rejected.
  • Duplicate names in an export list are rejected.
  • Unknown names in an export list are rejected.
  • export {*}; exports all importable top-level declarations (except pragma/export declarations).

5. Namespaces and Name Resolution

Duplicate checking is enforced separately for:

  • type namespace (contracts, data types, type synonyms)
  • class namespace
  • term namespace (functions, constructors, values)

Unqualified lookup order:

  1. Local lexical scope
  2. Current module top-level declarations
  3. Names introduced by import M.{...} / import M.{*}
  4. Otherwise unresolved

Module qualification:

  • Imported qualifiers are added to the resolver environment.
  • For nested module paths (for example import foo.bar;), prefix qualifiers are also registered for qualified access paths.

6. Constructor Model

Current constructor model is type-qualified constructors, with dot shorthand support.

  • Canonical constructor names are type-qualified (Bool.True, Option.Some).
  • Module-qualified constructor access is supported (mod.Bool.True, alias.Bool.True).
  • Unqualified constructors are generally rejected when only qualified constructors exist.

Dot shorthand:

.Some(1)
.None
  • Expression shorthand requires expected constructor type context.
  • Pattern shorthand requires expected scrutinee type context.
  • Missing expected type, no match, or ambiguous match is an error.

7. Pragma Scope

  • Pragmas are module-local.
  • Pragmas do not propagate to importing modules.
  • Pragmas are not transitive through imports.

Copy link
Collaborator

@mbenke mbenke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alas, this PR breaks the contract tests (ERC20 et consortes):

$ ./run_contests.sh
Processing: test/examples/dispatch/basic.json
Compiling to Hull...
Configuration is affected by the following files:
- cabal.project.local
Emitting hull for contract C
Writing to output1.hull
Generating Yul...
Configuration is affected by the following files:
- cabal.project.local
yule: /home/ben/work/solcore/build/output1.hull:345:34:
    |
345 |         return dispatch_$impl$std.$impl$abi_decode$calldataLbytesJ_pairLuint256_pairLuint256_uint256JJ_CalldataWordReader_pairLuint256_pairLuint256_uint256JJ(decodable, pty, prdr)
    |                                  ^
unexpected '.'
expecting "assembly", "false", "fst", "function", "if", "in", "inl", "inr", "let", "match", "return", "revert", "snd", "true", '$', '(', '_', '{', '}', alphanumeric character, integer, or letter

CallStack (from HasCallStack):
  error, called at src/Common/LightYear.hs:26:15 in sol-core-0.0.0.0-inplace:Common.LightYear
  runMyParser', called at src/Common/LightYear.hs:21:22 in sol-core-0.0.0.0-inplace:Common.LightYear
  runMyParser, called at src/Language/Hull/Parser.hs:30:24 in sol-core-0.0.0.0-inplace:Language.Hull.Parser
Error: yule generation failed
Processing: test/examples/dispatch/neg.json
Compiling to Hull...
Configuration is affected by the following files:
- cabal.project.local
Module validation failed for /home/ben/work/solcore/test/examples/dispatch/neg.solc:
Undefined type constructor:
uint256

 - in:function negPair () -> uint256 {
   return uint256(fromB(pairfst(Neg.neg(Pair(B.F, B.T))))) ;
}
 - in:function negPair () -> uint256 {
   return uint256(fromB(pairfst(Neg.neg(Pair(B.F, B.T))))) ;
}
 - in:contract NegPair {
   constructor () {
   }
   function negPair () -> uint256 {
      return uint256(fromB(pairfst(Neg.neg(Pair(B.F, B.T))))) ;
   }
}
 - in:contract NegPair {
   constructor () {
   }
   function negPair () -> uint256 {
      return uint256(fromB(pairfst(Neg.neg(Pair(B.F, B.T))))) ;
   }
}
Error: sol-core compilation failed
Processing: test/examples/dispatch/miniERC20.json
Compiling to Hull...
Configuration is affected by the following files:
- cabal.project.local
Emitting hull for contract MiniERC20
Writing to output1.hull
Generating Yul...
Configuration is affected by the following files:
- cabal.project.local
yule: /home/ben/work/solcore/build/output1.hull:1294:34:
     |
1294 |         return dispatch_$impl$std.$impl$abi_decode$calldataLbytesJ_address_CalldataWordReader_address(decodable, pty, prdr)
     |                                  ^
unexpected '.'
expecting "assembly", "false", "fst", "function", "if", "in", "inl", "inr", "let", "match", "return", "revert", "snd", "true", '$', '(', '_', '{', '}', alphanumeric character, integer, or letter

CallStack (from HasCallStack):
  error, called at src/Common/LightYear.hs:26:15 in sol-core-0.0.0.0-inplace:Common.LightYear
  runMyParser', called at src/Common/LightYear.hs:21:22 in sol-core-0.0.0.0-inplace:Common.LightYear
  runMyParser, called at src/Language/Hull/Parser.hs:30:24 in sol-core-0.0.0.0-inplace:Language.Hull.Parser
Error: yule generation failed
Processing: test/examples/dispatch/Revert.json
Compiling to Hull...
Configuration is affected by the following files:
- cabal.project.local
Emitting hull for contract Foo
Writing to output1.hull
Generating Yul...
Configuration is affected by the following files:
- cabal.project.local
yule: /home/ben/work/solcore/build/output1.hull:200:34:
    |
200 |         return dispatch_$impl$std.$impl$abi_encode$uint256(val)
    |                                  ^
unexpected '.'
expecting "assembly", "false", "fst", "function", "if", "in", "inl", "inr", "let", "match", "return", "revert", "snd", "true", '$', '(', '_', '{', '}', alphanumeric character, integer, or letter

CallStack (from HasCallStack):
  error, called at src/Common/LightYear.hs:26:15 in sol-core-0.0.0.0-inplace:Common.LightYear
  runMyParser', called at src/Common/LightYear.hs:21:22 in sol-core-0.0.0.0-inplace:Common.LightYear
  runMyParser, called at src/Language/Hull/Parser.hs:30:24 in sol-core-0.0.0.0-inplace:Language.Hull.Parser
Error: yule generation failed

@Y-Nak
Copy link
Member Author

Y-Nak commented Feb 25, 2026

Oops, I forgot to run contract tests. Fixed.

@Y-Nak Y-Nak requested a review from mbenke February 25, 2026 09:31
Copy link
Collaborator

@rodrigogribeiro rodrigogribeiro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work! LGTM.

@mbenke
Copy link
Collaborator

mbenke commented Feb 26, 2026

Pragma handling for imports sometimes leads to some weird errors, e.g.

import std;

function main(){}

crashes with

Instance 
a : Num
does not satisfy the Patterson conditions.

 - in:forall a .
a : Add, a : Sub, a : Bounded, a : Eq, a : Ord, a : Typedef (word) => default instance a : Num

Copy link
Collaborator

@mbenke mbenke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏 Impressive work!

I have left some suggestions and requests for clarification in the comments. My main concerns are:

  1. Problem with shorthand, e.g.
function join(mmx:Option(Option(word))) -> Option(word){
  match (mmx:Option(Option((word)))) {
    | .Some(.Some(x)) => return Option.Some(x);
    | _ => return Option.None;
  }
}

does not compile (cannot resolve nested .Some pattern)
2. pragma handling leading to weird bugs (see #321 (comment))

Some suggestions for future refactoring (probably not worth it right now):

  1. Try to eliminate boilerplate in renameExpFunctionCalls, renameExpTypeRefs, stmtFunctionRefs, expFunctionRefs, etc.
  2. Loader.hs is doing too much. At 1336 lines, it handles:
    graph loading, import/export validation, name flattening for
    validation, name flattening for compilation, function renaming,
    type renaming, expression renaming, pattern renaming, function
    dependency closure analysis, shadowing, and more. The AST
    rewriting functions (lines ~426–1146) are a distinct concern
    from the module graph logic (lines ~27–177). Splitting into at
    least Loader.hs (graph + validation) and ModuleFlattener.hs
    (AST rewriting) would make it more maintainable.

Comment on lines +1 to +2
import std.{*};
import std.{Proxy, Typedef, Sub, uint256, address, bytes4, memory, calldata, string, bytes, ABIAttribs, ABIDecoder, ABIDecode, ABIEncode, CalldataWordReader, keccakLit, abi_decode, abi_encode, get_free_memory};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are both imports needed?

| ExpName (Maybe Exp) Name [Exp] -- function call or constructor
| ExpVar (Maybe Exp) Name -- variables or field access
| ExpDotName Name [Exp] -- contextual constructor shorthand, e.g. .Some(1)
| ExpDotVar Name -- contextual constructor shorthand, e.g. .None
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is ExpDotVar needed (and with a confusing name, from the comment and the parser it seems it does not represent a variable. IOW, why is .None represented as ExpDotVar "None" rather than `ExpDotName "None" []"?

%right 'else'

%expect 0
%expect 1
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This deserves a comment explaining which conflict exists and why the default resolution is correct.

| .Some(v) => return v;
| .None => return 0;
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OTOH, this does not work, despite inserting as much type annotations as possible:

  function join(mmx:Option(Option(word))) -> Option(word){
    match (mmx:Option(Option((word)))) {
      | .Some(.Some(x)) => return Option.Some(x);
      | _ => return Option.None;
    }
  }

the problem is apparently in nested constructors


flattenModuleCompUnit :: ModuleGraph -> FilePath -> Either String CompUnit
flattenModuleCompUnit graph modulePath =
flattenModuleValidationCompUnit graph modulePath
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are two functions needed? What is the difference?


- Canonical constructor names are type-qualified (`Bool.True`, `Option.Some`).
- Module-qualified constructor access is supported (`mod.Bool.True`, `alias.Bool.True`).
- Unqualified constructors are generally rejected when only qualified constructors exist.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this mean?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants