Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8386918
Fix co_varnames layout, intern string constants, fold sets for member…
tamnd Jun 19, 2026
89846ae
Drive test_dis.py to CPython parity (spec 1720 codegen panel)
tamnd Jun 19, 2026
e3f0159
Drive test_format.py to CPython parity (spec 1720 codegen panel)
tamnd Jun 19, 2026
b9cbd1d
Fix traceback frames lost across exec()/eval() and surrogate source e…
tamnd Jun 19, 2026
dc0c2c0
Constant-slice codegen and comprehension assignment-idiom parity
tamnd Jun 20, 2026
2be8dba
Settable frame.f_lineno for the debugger line jump
tamnd Jun 20, 2026
128e05e
Bound the call/dict stack: chunk large literals through MAP_ADD and C…
tamnd Jun 20, 2026
b60a645
Route compile() AST validation through the right exception type and r…
tamnd Jun 20, 2026
9dd11d0
Accept buffer-protocol source and surface null bytes ahead of UTF-8 f…
tamnd Jun 20, 2026
a02c8ae
Reverse-bridge TypeAlias and PEP 695 type parameters
tamnd Jun 20, 2026
3b678e0
Compare nested code-object consts via code_richcompare not DeepEqual
tamnd Jun 20, 2026
fc7e8b3
Validate AST compile mode and require located-node positions
tamnd Jun 20, 2026
f49de35
Locate attribute, lambda-return and comprehension opcodes like CPytho…
tamnd Jun 20, 2026
1270db0
compile: emit pseudo JUMP_IF_FALSE/TRUE in boolop so jump threading f…
tamnd Jun 20, 2026
2c59ba5
compile: merge equal constants across a compile unit via a const cache
tamnd Jun 20, 2026
4217b79
parser: reject trailing statements in single-input mode
tamnd Jun 21, 2026
191efc4
compile: keep PEP 626 line numbers off synthetic jumps and exits
tamnd Jun 21, 2026
7c8554a
compile: suppress duplicate SyntaxWarning from finally exception path
tamnd Jun 21, 2026
31aee1d
objects: return fresh references from cached code attributes
tamnd Jun 21, 2026
7f1ee22
compile: format-aware scope for type-param bounds and defaults
tamnd Jun 21, 2026
1b18622
frame: bounds-guard GC traversal of partially-built frames
tamnd Jun 21, 2026
9150216
compile: splice module __annotate__ at ANNOTATIONS_PLACEHOLDER and em…
tamnd Jun 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions ast/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,27 @@ import (
"fmt"
"math/big"
"reflect"
"strings"
)

// WrapValidationError tags a Validate failure with the Python exception
// type it must surface as. Most validation failures are ValueError; a
// handful (NamedExpr / AnnAssign / TypeAlias targets, Constant type) are
// TypeError and already carry that prefix in their message, so they pass
// straight through. Callers that turn a Validate error into a Python
// exception route it through here instead of hardcoding ValueError.
//
// CPython: Python/ast.c PyErr_SetString(PyExc_TypeError/ValueError, ...)
func WrapValidationError(err error) error {
msg := err.Error()
for _, p := range []string{"TypeError: ", "SystemError: "} {
if strings.HasPrefix(msg, p) {
return errors.New(msg)
}
}
return errors.New("ValueError: " + msg)
}

// Validate is the gopy port of _PyAST_Validate. It walks mod and
// returns nil if the tree is well-formed, or an error matching
// CPython's ValueError/TypeError text.
Expand Down Expand Up @@ -141,7 +160,7 @@ func validateStmt(s Stmt) error {
case *AnnAssign:
if n.Target != nil {
if _, ok := n.Target.(*Name); !ok && n.Simple != 0 {
return errors.New("AnnAssign with simple non-Name target")
return errors.New("TypeError: AnnAssign with simple non-Name target")
}
}
if err := validateExpr(n.Target, Store); err != nil {
Expand All @@ -156,7 +175,7 @@ func validateStmt(s Stmt) error {
case *TypeAlias:
if n.Name != nil {
if _, ok := n.Name.(*Name); !ok {
return errors.New("TypeAlias with non-Name name")
return errors.New("TypeError: TypeAlias with non-Name name")
}
}
if err := validateExpr(n.Name, Store); err != nil {
Expand Down Expand Up @@ -446,7 +465,7 @@ func validateExpr(e Expr, ctx ExprContext) error {
return validateExprs(n.Values, Load, false)
case *NamedExpr:
if _, ok := n.Target.(*Name); !ok {
return errors.New("NamedExpr target must be a Name")
return errors.New("TypeError: NamedExpr target must be a Name")
}
return validateExpr(n.Value, Load)
case *BinOp:
Expand Down Expand Up @@ -818,7 +837,7 @@ func validateConstant(v any) error {
}
return nil
}
return fmt.Errorf("got an invalid type in Constant: %s", reflect.TypeOf(v))
return fmt.Errorf("TypeError: got an invalid type in Constant: %s", reflect.TypeOf(v))
}

// EllipsisType is the singleton type used to spell Python's `...` as
Expand Down
14 changes: 14 additions & 0 deletions builtins/ast_bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
package builtins

import (
"math/big"

"github.com/tamnd/gopy/ast"
"github.com/tamnd/gopy/imp"
"github.com/tamnd/gopy/objects"
Expand Down Expand Up @@ -788,6 +790,8 @@ func (b *astBridge) convertConstantValue(v any) objects.Object {
switch x := v.(type) {
case int64:
return objects.NewInt(x)
case *big.Int:
return objects.NewIntFromBig(x)
case float64:
return objects.NewFloat(x)
case string:
Expand All @@ -809,6 +813,16 @@ func (b *astBridge) convertConstantValue(v any) objects.Object {
items[i] = b.convertConstantValue(elem)
}
return objects.NewTuple(items)
case ast.FrozenSet:
items := make([]objects.Object, len(x))
for i, elem := range x {
items[i] = b.convertConstantValue(elem)
}
fs, err := objects.NewFrozenset(items)
if err != nil {
return objects.None()
}
return fs
}
return objects.None()
}
Expand Down
114 changes: 103 additions & 11 deletions builtins/ast_bridge_reverse.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ func (r *reverseASTBridge) convertStmt(o objects.Object) ast.Stmt {
if !ok {
return nil
}
pos := r.getPos(inst)
pos := r.getPos(inst, "stmt")
switch inst.Type().Name {
case "Assign":
targets := r.exprList(r.getAttr(inst, "targets"))
Expand All @@ -137,6 +137,17 @@ func (r *reverseASTBridge) convertStmt(o objects.Object) ast.Stmt {
op := r.convertOperator(r.getAttr(inst, "op"))
value := r.convertExpr(r.getAttr(inst, "value"))
return &ast.AugAssign{Target: target, Op: op, Value: value, Pos: pos}
case "AnnAssign":
target := r.convertExpr(r.getAttr(inst, "target"))
annotation := r.convertExpr(r.getAttr(inst, "annotation"))
value := r.optionalExpr(inst, "value")
return &ast.AnnAssign{
Target: target,
Annotation: annotation,
Value: value,
Simple: r.getAttrInt(inst, "simple"),
Pos: pos,
}
case "Return":
val := r.getAttr(inst, "value")
var retVal ast.Expr
Expand Down Expand Up @@ -227,6 +238,11 @@ func (r *reverseASTBridge) convertStmt(o objects.Object) ast.Stmt {
subject := r.convertExpr(r.getAttr(inst, "subject"))
cases := r.convertMatchCases(r.getAttr(inst, "cases"))
return &ast.Match{Subject: subject, Cases: cases, Pos: pos}
case "TypeAlias":
name := r.convertExpr(r.getAttr(inst, "name"))
typeParams := r.convertTypeParams(r.getAttr(inst, "type_params"))
value := r.convertExpr(r.getAttr(inst, "value"))
return &ast.TypeAlias{Name: name, TypeParams: typeParams, Value: value, Pos: pos}
}
// Unknown statement: emit a Pass so the body remains valid.
return &ast.Pass{Pos: pos}
Expand All @@ -243,7 +259,7 @@ func (r *reverseASTBridge) convertExceptHandlers(o objects.Object) ast.Seq[ast.E
if !ok {
continue
}
pos := r.getPos(inst)
pos := r.getPos(inst, "excepthandler")
var typ ast.Expr
if t := r.getAttr(inst, "type"); t != nil && t != objects.None() {
typ = r.convertExpr(t)
Expand Down Expand Up @@ -315,7 +331,7 @@ func (r *reverseASTBridge) convertPattern(o objects.Object) ast.Pattern {
panic(astRecursionSentinel{})
}
defer func() { r.depth++ }()
pos := r.getPos(inst)
pos := r.getPos(inst, "pattern")
switch inst.Type().Name {
case "MatchValue":
value := r.convertExpr(r.getAttr(inst, "value"))
Expand Down Expand Up @@ -425,6 +441,7 @@ func (r *reverseASTBridge) convertFunctionDef(inst *objects.Instance, pos ast.Po
Body: body,
DecoratorList: decorators,
Returns: returns,
TypeParams: r.convertTypeParams(r.getAttr(inst, "type_params")),
Pos: pos,
}
}
Expand All @@ -445,6 +462,7 @@ func (r *reverseASTBridge) convertAsyncFunctionDef(inst *objects.Instance, pos a
Body: body,
DecoratorList: decorators,
Returns: returns,
TypeParams: r.convertTypeParams(r.getAttr(inst, "type_params")),
Pos: pos,
}
}
Expand All @@ -461,10 +479,69 @@ func (r *reverseASTBridge) convertClassDef(inst *objects.Instance, pos ast.Pos)
Keywords: keywords,
Body: body,
DecoratorList: decorators,
TypeParams: r.convertTypeParams(r.getAttr(inst, "type_params")),
Pos: pos,
}
}

// convertTypeParams reverses convertTypeParams in the forward bridge:
// it rebuilds the Go PEP 695 type-parameter nodes (TypeVar / TypeVarTuple
// / ParamSpec) from their _ast instances so a compile()-from-AST request
// carrying generic functions, classes, or type aliases reaches codegen
// (and the validator) with its type_params intact.
//
// CPython: Python/Python-ast.c obj2ast_type_param
func (r *reverseASTBridge) convertTypeParams(o objects.Object) ast.Seq[ast.TypeParam] {
lst, ok := o.(*objects.List)
if !ok {
return nil
}
out := make(ast.Seq[ast.TypeParam], 0, lst.Len())
for i := 0; i < lst.Len(); i++ {
inst, ok := lst.Item(i).(*objects.Instance)
if !ok {
continue
}
pos := r.getPos(inst, "type_param")
name := r.getAttrString(inst, "name")
switch inst.Type().Name {
case "TypeVar":
var bound ast.Expr
if b := r.getAttr(inst, "bound"); b != nil && b != objects.None() {
bound = r.convertExpr(b)
}
out = append(out, &ast.TypeVar{
Name: name,
Bound: bound,
DefaultValue: r.optionalExpr(inst, "default_value"),
Pos: pos,
})
case "TypeVarTuple":
out = append(out, &ast.TypeVarTuple{
Name: name,
DefaultValue: r.optionalExpr(inst, "default_value"),
Pos: pos,
})
case "ParamSpec":
out = append(out, &ast.ParamSpec{
Name: name,
DefaultValue: r.optionalExpr(inst, "default_value"),
Pos: pos,
})
}
}
return out
}

// optionalExpr converts attr name to a Go expr, returning nil when the
// attribute is missing or None.
func (r *reverseASTBridge) optionalExpr(inst *objects.Instance, name string) ast.Expr {
if v := r.getAttr(inst, name); v != nil && v != objects.None() {
return r.convertExpr(v)
}
return nil
}

func (r *reverseASTBridge) convertArguments(o objects.Object) *ast.Arguments {
inst, ok := o.(*objects.Instance)
if !ok {
Expand Down Expand Up @@ -515,7 +592,7 @@ func (r *reverseASTBridge) convertArg(o objects.Object) *ast.Arg {
}
a := &ast.Arg{
Arg: r.getAttrString(inst, "arg"),
Pos: r.getPos(inst),
Pos: r.getPos(inst, "arg"),
}
if ann := r.getAttr(inst, "annotation"); ann != nil && ann != objects.None() {
a.Annotation = r.convertExpr(ann)
Expand Down Expand Up @@ -575,7 +652,7 @@ func (r *reverseASTBridge) convertExpr(o objects.Object) ast.Expr {
panic(astRecursionSentinel{})
}
defer func() { r.depth++ }()
pos := r.getPos(inst)
pos := r.getPos(inst, "expr")
switch inst.Type().Name {
case "Name":
id := r.getAttrIdentifier(inst, "id")
Expand Down Expand Up @@ -749,7 +826,7 @@ func (r *reverseASTBridge) convertKeywords(o objects.Object) ast.Seq[*ast.Keywor
arg = &s
}
val := r.convertExpr(r.getAttr(inst, "value"))
out = append(out, &ast.Keyword{Arg: arg, Value: val, Pos: r.getPos(inst)})
out = append(out, &ast.Keyword{Arg: arg, Value: val, Pos: r.getPos(inst, "keyword")})
}
return out
}
Expand Down Expand Up @@ -786,7 +863,7 @@ func (r *reverseASTBridge) convertAliases(o objects.Object) ast.Seq[*ast.Alias]
s := r.getAttrString(inst, "asname")
asname = &s
}
out = append(out, &ast.Alias{Name: name, Asname: asname, Pos: r.getPos(inst)})
out = append(out, &ast.Alias{Name: name, Asname: asname, Pos: r.getPos(inst, "alias")})
}
return out
}
Expand Down Expand Up @@ -966,8 +1043,12 @@ func (r *reverseASTBridge) convertConstantValue(o objects.Object) any {
case *objects.Unicode:
return v.Value()
case *objects.Int:
i64, _ := v.Int64()
return i64
// Constants that overflow int64 must round-trip as *big.Int, the
// same representation the parser emits, so co_consts compares equal.
if i64, ok := v.Int64(); ok {
return i64
}
return v.BigInt()
case *objects.Float:
return v.Float64()
case *objects.Complex:
Expand Down Expand Up @@ -1060,14 +1141,25 @@ func (r *reverseASTBridge) getAttrPresent(inst *objects.Instance, name string) (
// matching CPython's Python/Python-ast.c:11187 obj2ast_stmt.
//
// CPython: Python/ast.c:1043 validate_stmt LOCATION macro
func (r *reverseASTBridge) getPos(inst *objects.Instance) ast.Pos {
// getPos reads the source-location attributes that every located AST node
// (stmt, expr, excepthandler, pattern, type_param, arg, keyword, alias)
// carries. lineno and col_offset are required: a missing attribute raises
// TypeError("required field \"lineno\" missing from <kind>"), matching the
// obj2ast PyObject_GetOptionalAttr == NULL path. end_lineno / end_col_offset
// default to lineno / col_offset when absent or None.
//
// CPython: Python/Python-ast.c:11149 obj2ast_stmt attribute block
func (r *reverseASTBridge) getPos(inst *objects.Instance, kind string) ast.Pos {
linenoVal, linenoPresent := r.getAttrPresent(inst, "lineno")
if !linenoPresent {
return ast.NoPos
panic(astTypeError{fmt.Sprintf("required field \"lineno\" missing from %s", kind)})
}
if linenoVal == objects.None() {
panic(astValidationError{"invalid integer value: None"})
}
if _, colPresent := r.getAttrPresent(inst, "col_offset"); !colPresent {
panic(astTypeError{fmt.Sprintf("required field \"col_offset\" missing from %s", kind)})
}
lineno := r.getAttrInt(inst, "lineno")
colOffset := r.getAttrInt(inst, "col_offset")

Expand Down
Loading
Loading