This is a very impressive and ambitious project. Building a new language, even one that transpiles to another, is a significant undertaking. The code is well-structured, the documentation is excellent, and the test coverage at the integration level is fantastic.
Here is a detailed review, focusing on the "glaring issues" you asked for, as well as potential areas for improvement.
The overall design is sound and clever. By compiling Dasy Lisp syntax into a Vyper AST and then handing off to the Vyper compiler, you gain several major advantages:
- Security & Optimization: You are piggy-backing on the extensive security analysis and optimization work done by the Vyper team. This is a huge win and the right architectural choice.
- EVM Compatibility: You're guaranteed to produce correct and efficient EVM bytecode as long as your AST generation is correct.
- Developer Experience: You can focus on providing a powerful, macro-driven Lisp experience without needing to reinvent the entire compiler backend.
The use of Hy for macros and some parser utilities is a natural and excellent fit for the project.
These are the most significant issues that could lead to bugs, security vulnerabilities, or unpredictable behavior.
This is the most critical issue in the codebase.
File: dasy/dasy/builtin/macros.hy
Macros: interface! and include!
(defmacro interface! [filename]
(import dasy) ;; <-- Problem 1: Imports the whole package
(import os)
;; Problem 2: Path is relative to Current Working Directory, not the source file
(let [path (+ (.getcwd os) "/" filename)
data (.compile-file dasy path) ;; <-- Problem 3: Re-enters the compiler
interface-str (.get-external-interface dasy data)]
(.read dasy interface-str)))Analysis:
- Circular Dependency: A macro, which is executed during compilation, imports the
dasypackage. Thedasypackage itself initializes the parser and compiler. This creates a highly coupled and potentially circular dependency. - Recursive Compilation: The macro calls
dasy.compile_file(). This means the compiler is calling itself to compile another file just to get its interface. While this is a powerful feature, it's fraught with peril:- What happens if
examples/test_interface.vyhas a syntax error? The main compilation will fail with a potentially confusing traceback originating from the macro expansion. - What if
test_interface.vyitself uses aninterface!macro? This could lead to deep, hard-to-debug recursion.
- What happens if
- Path Resolution: The use of
os.getcwd()is incorrect. It makes compilation dependent on where the user runs thedasycommand from, not where the source file is located. If a user tries to compiledasy/examples/interface.dasyfrom the root directory,os.getcwd()will be/path/to/dasy, andpathwill be/path/to/dasy/examples/test_interface.vy, which is correct. But if theycd dasyand rundasy ../examples/interface.dasy, it will fail.
Recommendation:
- Decouple Macro Helpers: Create a separate, minimal module for file reading and compilation that macros can import without pulling in the entire
dasypackage. This module should not depend on the main parser loop. - Fix Path Resolution: The parser or macro expansion context needs to know the path of the file currently being compiled. Hy's macro system can provide this. The path to the included/interfaced file should be resolved relative to the current source file's path, not the CWD.
pathlibis your friend here. - Avoid Global State (see next point): This will make recursive compilation safer.
File: dasy/dasy/parser/parse.py
Globals: SRC, CONSTS
# ...
SRC = ""
CONSTS = {}
def parse_node(node):
# ... uses CONSTS ...
def parse_src(src: str):
global SRC
SRC = src
# ...
# parse_node is called, which eventually calls add_src_map
# add_src_map uses the global SRC
return add_src_map(SRC, node, ast_node)Analysis:
The parser is not re-entrant. SRC is a global variable holding the source code for the current file. CONSTS is a global dictionary holding constants.
- If you ever wanted to support parallel compilation in the same process, this would fail catastrophically.
- It makes the recursive compilation in the
interface!macro even more dangerous. When the compiler is re-entered, this global state could be overwritten, leading to incorrect source mapping or constant resolution for the original file.
Recommendation:
- Refactor
parse_srcandparse_nodeto accept aContextobject or passsrcandconstsdown the call stack as arguments. This makes the parser pure and re-entrant.
# Suggested change
def parse_src(src: str):
# No more global SRC
mod_node = ...
settings = {}
consts = {} # Local to this compilation
for element in dasy_read_many(src):
# Pass context down
ast = parse_node(element, src, consts)
# ...
if cmd_str == "defconst":
consts[str(expr[1])] = expr[2] # Mutate local dict
return None
# ...
def parse_node(node, src, consts):
# ...
ast_node = ...
# ...
return add_src_map(src, node, ast_node) # Pass src explicitlyThese are less critical but would improve code quality, maintainability, and robustness.
Files: dasy/dasy/parser/comparisons.py and dasy/dasy/parser/ops.py
Both files define an almost identical chain_comps function. It appears dasy/parser/ops.py is the one being used. The dasy/parser/comparisons.py file seems redundant or a leftover from a refactor and could likely be removed to avoid confusion.
The codebase frequently uses raise Exception(...). It would be better to define a hierarchy of custom exceptions.
class DasyError(Exception):
pass
class DasySyntaxError(DasyError):
# Could include line/column info
pass
class DasyCompilerError(DasyError):
passThis allows consumers of dasy as a library (like an IDE extension or build tool) to catch specific errors and provide better feedback to the user.
File: dasy/dasy/parser/parse.py in parse_expr
The current dispatch mechanism is a large if/elif chain over strings.
def parse_expr(expr):
# ...
if is_op(cmd_str): # ...
if cmd_str in nodes.handlers: # ...
node_fn = f"parse_{cmd_str}"
for ns in [nodes, core, macros, functions]:
if hasattr(ns, node_fn):
return getattr(ns, node_fn)(expr)
# ...This works, but it can be hard to trace and extend. A dictionary-based dispatch table is a common and often cleaner alternative.
# A potential alternative structure
DISPATCH_TABLE = {
"defn": core.parse_defn,
"defvar": core.parse_defvar,
# ... map all core forms
}
def parse_expr(expr):
cmd_str = ALIASES.get(str(expr[0]), str(expr[0]))
# ...
if handler := DISPATCH_TABLE.get(cmd_str):
return handler(expr)
# ... handle ops, macros, calls etc.The logic in parse.py:parse_call and parse.py:parse_expr to handle method calls like (.append self/nums 1) (which becomes ((. self nums) append 1)) and especially the (. None method) pattern is clever but complex and a bit "magical". The doto macro is a much cleaner and more idiomatic Lisp pattern for this. It might be worth considering simplifying the core syntax and encouraging doto for these use cases.
It's important to highlight what the project does well, and it does a lot.
- Excellent Documentation: The
README.org,docs.org, anddasybyexample.orgare fantastic. Providing clear documentation and a "by example" guide is crucial for any new language, and you have nailed this. - Comprehensive Integration Tests:
tests/test_dasy.pyis a model for how to test a compiler. It covers a vast range of language features by compiling and executing example files. The use ofboais perfect for this. - Smart Language Features:
- The
DasyReaderfor handling0xliterals is a small but very smart detail that improves usability. - The built-in macros like
cond,condp,doto, and the field accessors (set-in,get-at) provide significant ergonomic improvements over raw Vyper AST construction. - The automatic chaining of binary operators (
(+ 1 2 3)) is a great Lisp-y feature.
- The
- Clean Code Structure: The project is well-organized into logical modules (
parser,compiler,builtin), which makes it easy to navigate.
You have a very solid foundation for an exciting project. The architecture is smart, the documentation is superb, and the feature set is already rich.
To move from "experimental pre-alpha" to a more stable state, I would strongly recommend focusing on these two action items in order:
- Refactor the
interface!andinclude!macros to eliminate recursive compilation and fix path handling. - Remove global state from the parser to make it re-entrant and robust.
After addressing these critical issues, the project will be in a much more stable and secure position for further development. Fantastic work so far!
- Problem:
interface!andinclude!usedos.getcwd()making compilation dependent on working directory - Solution:
- Created
ParseContextclass to carry source file path through compilation - Macros now resolve paths relative to
context.base_dir(source file's directory) - Tested and verified compilation works from any directory
- Created
- Problem: Global
SRCandCONSTSvariables made parser non-reentrant - Solution:
- Created
ParseContextobject to hold source code, constants, and file path - Refactored all parser functions to accept context parameter
- Added backwards compatibility layer (
parse_node_legacy) for gradual migration - Parser is now thread-safe and reentrant
- Created
- Problem:
interface!macro could cause infinite recursion by importing itself - Solution:
- Created
compile_for_interface()function for minimal compilation - Added circular dependency detection with clear error messages
- Used thread-local storage to track compilation and include stacks
CircularDependencyErrorprovides helpful debugging information
- Created
- Problem:
chain_compsfunction duplicated in bothcomparisons.pyandops.py - Solution:
- Removed unused
comparisons.pyfile entirely ops.pyis the canonical location for comparison operations- All tests pass after removal
- Removed unused
- Problem: Generic
ExceptionandValueErrorused throughout - Solution:
- Created
dasy/exceptions.pywith specialized exception classes - Exception hierarchy:
DasyException(base) with subclasses for syntax, type, compilation errors etc. - Replaced all generic exceptions with appropriate specific types
- Better error messages and easier debugging for users
- Created
None - All high priority items completed!
- Complex method call syntax - Consider simplifying the
(. None method)pattern
- Refactor parser dispatch - Replace if/elif chain with dictionary-based dispatch
The critical architectural issues have been resolved. The codebase is now much more robust with:
- Proper path handling independent of working directory
- Thread-safe, reentrant parser without global state
- Protection against circular dependencies with helpful error messages
All 39 tests pass. The foundation is solid for further improvements