Skip to content

Python transpiler: fanc py command#84

Open
trevadelman wants to merge 6 commits intofantom-lang:masterfrom
trevadelman:python-transpiler
Open

Python transpiler: fanc py command#84
trevadelman wants to merge 6 commits intofantom-lang:masterfrom
trevadelman:python-transpiler

Conversation

@trevadelman
Copy link
Contributor

Summary

Add Python transpiler to the fanc tool -- generates Python 3.12+ code from Fantom source via fanc py <pod>.

Context

This is PR 1 of 6 for adding Python transpilation support to Fantom. Subsequent PRs add the hand-written runtime (sys, concurrent, util), test framework, and additional pod support (inet, crypto).

Changes

New files (8):

  • src/fanc/fan/py/PythonCmd.fan -- Entry point (fanc py <pod>)
  • src/fanc/fan/py/PyTypePrinter.fan -- Class/type generation
  • src/fanc/fan/py/PyExprPrinter.fan -- Expression generation
  • src/fanc/fan/py/PyStmtPrinter.fan -- Statement generation
  • src/fanc/fan/py/PyPrinter.fan -- Base printer with indentation and output management
  • src/fanc/fan/py/PyUtil.fan -- Utilities, operator maps, naming conventions
  • src/fanc/fan/py/design.md -- Technical reference for the Fantom-to-Python mapping
  • src/fanc/fan/py/quickstart.md -- Build and test guide

Modified files (3):

  • src/build/fan/BuildPod.fan -- Add pyDirs field (mirrors jsDirs), pod.native.py meta, ci.pyFiles = pyDirs
  • src/compiler/fan/CompilerInput.fan -- Add pyFiles field (mirrors jsFiles)
  • src/fanc/build.fan -- Add fan/py/ to srcDirs

Dependencies

  • Depends on: None
  • Required by: PRs 2-6 (sys runtime, concurrent, util/test framework, inet, crypto)

Testing

# Build the transpiler
bin/fan src/fanc/build.fan

# Transpile a pod (generates Python in gen/py/)
bin/fanc py sys

The transpiler generates code but cannot run it yet -- the hand-written sys runtime (PR 2) and test framework (PR 4) are needed for execution.

Design Decisions

  • Follows the Java transpiler pattern: PythonCmd extends TranspileCmd, same as JavaCmd. Uses PyTypePrinter/PyExprPrinter/PyStmtPrinter hierarchy mirroring the Java transpiler.
  • snake_case naming: Fantom's camelCase methods become Python's snake_case. A compile-time map handles the conversion.
  • Lazy __init__.py loaders: Avoids circular imports by deferring module loads until first access.
  • Func.make_closure(): Closures are wrapped to support Fantom's captured variable semantics.
  • ObjUtil dispatch: Cross-type operations (compare, equals, hash) route through a utility module matching the JS transpiler's approach.
  • pyDirs in BuildPod: Mirrors jsDirs -- pods with a py/ directory contain hand-written Python natives that override transpiled output.

See design.md for the complete Fantom-to-Python mapping reference.

Add Python transpiler to the fanc tool as a TranspileCmd, following the same
pattern as the existing Java transpiler command. Generates Python 3.12+ code
from Fantom source via 'fanc py <pod>'.

The transpiler maps Fantom constructs to idiomatic Python: snake_case naming,
combined getter/setter fields, Func.make_closure() for closures, ObjUtil
dispatch for cross-type operations, and lazy __init__.py module loaders to
avoid circular imports. See design.md for the full Fantom-to-Python mapping.

New files:
  src/fanc/fan/py/PythonCmd.fan     - Entry point (fanc py <pod>)
  src/fanc/fan/py/PyTypePrinter.fan - Class/type generation
  src/fanc/fan/py/PyExprPrinter.fan - Expression generation
  src/fanc/fan/py/PyStmtPrinter.fan - Statement generation
  src/fanc/fan/py/PyPrinter.fan     - Base printer
  src/fanc/fan/py/PyUtil.fan        - Utilities and operator maps
  src/fanc/fan/py/design.md         - Technical reference
  src/fanc/fan/py/quickstart.md     - Build and test guide

Modified files:
  src/build/fan/BuildPod.fan         - Add pyDirs field (mirrors jsDirs)
  src/compiler/fan/CompilerInput.fan - Add pyFiles field (mirrors jsFiles)
  src/fanc/build.fan                 - Add py/ to srcDirs
The combined getter/setter pattern used _val_=None as the default,
making it impossible to set a nullable field to null. Now uses a
module-level _UNSET = object() sentinel so field(None) correctly
enters the setter path.

Also fixes return type hints on field accessors to always use
Optional[T] since the setter path returns None.

Addresses review feedback from Matthew.
…ables

The transpiler now detects Wrap$ synthetic classes (the Fantom compiler's
signal for closure-captured mutable variables) and emits Python's nonlocal
keyword instead of ObjUtil.cvar() wrappers. This produces cleaner, more
idiomatic Python output.

PyPrinter.fan     - nonlocalVars map tracks wrapper-to-original-name mappings
PyStmtPrinter.fan - detectAndRecordNonlocal() skips Wrap$.make() lines,
                    prescanNonlocal() pre-scans method bodies, writeClosure()
                    emits nonlocal declarations
PyExprPrinter.fan - isWrapValAccess() intercepts Wrap$.val field access and
                    outputs plain variable names instead of wrapper._val

Result: 214 generated files now use nonlocal, zero ObjUtil.cvar in output.

Addresses review feedback from Matthew.
Move generated _UNSET sentinel from per-module 'object()' to an import
from sys::ObjUtil, ensuring a single global identity for cross-pod field
inheritance. The corresponding _UNSET definition in ObjUtil.py lands in
PR 2 (sys runtime).

Field accessor setter branches now return _val_ so both code paths match
the return type. Type hints reflect Fantom's actual type: non-nullable
Str -> 'str', nullable Str? -> 'Optional[str]'.

Addresses review feedback from Brian (Issues 1.5, 1.6).
Collapse two loops over pod.typeDefs in genPod() into one pass.
Replace ~40 printLine calls in writeLazyLoader() with a Str raw
string template using {{POD}} and {{TYPES_DICT}} placeholders.

Addresses review feedback from Matthew (Issues 1.7, 1.8).
Address all 10 points from code review:

1. mapLiteral/listLiteral: Cast to MapType/ListType directly. Removed
   try/catch control flow and dynamic dispatch (->). Use ns-level types
   instead of pod.resolveType().

2. DRY sysPrefix: Extracted sysPrefix() helper, replacing 15 inline
   occurrences of the curPod != sys check.

3. checkInCtor/enterCtor/exitCtor: Scoped to sys::Func parent type
   check instead of bare name matching.

4. call() broken up: From ~120 lines to 15-line dispatcher delegating
   to callSafe, callDynamic, callFunc, callPrivate, callNormal.

5. isObjUtilMethod: m.parent.isObj routes ALL Obj methods through
   ObjUtil (no name checks). Matches ES compiler pattern.

6. isPrimitiveType: Err/Func confirmed NOT primitives (matches ES
   compiler pmap). Num routed via ObjUtil.

7. primitiveMap: Single Str:Str hashmap replaces parallel if-chains
   in isPrimitiveType and primitiveClassName.

8. usesMethodStyleSetters: Removed dead code.

9. TODO defaults: throw UnsupportedErr instead of outputting comments
   or None for unimplemented paths.

10. CType methods: Replaced string comparisons (targetSig == sys::Str)
    with CType methods (isStr, isObj, isFloat, isNum, isDecimal, isRange).

Additional DRY improvements:
- writeTypeRef(): Extracted pod-qualified type reference (4 duplicates)
- writeArgs(): Extracted comma-separated arg writing (6+ duplicates)
- compoundAssign(): Extracted from doShortcutBinaryOp
- objUtilOp(): Extracted from divOp/modOp
- incDec(): Merged identical increment/decrement methods
- Eliminated dead cmp() method (redundant with comparison())

PyTypePrinter: fieldInit now writes type defaults only, matching ES
compiler pattern. Actual initialization handled by instance$init.

Net reduction: ~325 lines removed (15% smaller).

Addresses review feedback from Brian.
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.

1 participant