From b085bf80adbd7a3cb972a58722c9c88234c2cb87 Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Wed, 22 Apr 2026 13:38:31 +0300 Subject: [PATCH 1/8] compiler: add virtual dispatch Now we can reuse visitor to just visit one node, not recursively. It will be used in following commit to extract serialize from Expression. --- CMakeLists.txt | 1 + include/daScript/ast/ast.h | 2 + include/daScript/ast/ast_expressions.h | 64 ++++++++++++++++++ include/daScript/ast/ast_visitor.h | 10 +++ src/ast/ast_dispatch.cpp | 91 ++++++++++++++++++++++++++ web/CMakeLists.txt | 1 + 6 files changed, 169 insertions(+) create mode 100644 src/ast/ast_dispatch.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0b094e7d9a..f32fa3be9b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -654,6 +654,7 @@ SOURCE_GROUP_FILES("vecmath" VECMATH_SRC) SET(AST_SRC src/ast/ast.cpp +src/ast/ast_dispatch.cpp src/ast/ast_interop.cpp src/ast/ast_tls.cpp src/ast/ast_visitor.cpp diff --git a/include/daScript/ast/ast.h b/include/daScript/ast/ast.h index b7e864ec0e..f6deae79c9 100644 --- a/include/daScript/ast/ast.h +++ b/include/daScript/ast/ast.h @@ -25,6 +25,7 @@ namespace das { struct AstSerializer; + class Visitor; class Function; typedef Function * FunctionPtr; @@ -692,6 +693,7 @@ namespace das virtual bool swap_tail ( Expression *, Expression * ) { return false; } virtual uint32_t getEvalFlags() const { return 0; } virtual void serialize ( AstSerializer & ser ); + virtual void dispatch ( Visitor & vis ); virtual void gc_collect ( gc_root * target, gc_root * from ); LineInfo at; TypeDeclPtr type = nullptr; diff --git a/include/daScript/ast/ast_expressions.h b/include/daScript/ast/ast_expressions.h index 51ebdb80bf..0e42c5935b 100644 --- a/include/daScript/ast/ast_expressions.h +++ b/include/daScript/ast/ast_expressions.h @@ -13,6 +13,7 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ReaderMacroPtr macro = nullptr; string sequence; @@ -26,6 +27,7 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isLabel() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; int32_t label = -1; string comment; @@ -42,6 +44,7 @@ namespace das virtual bool rtti_isGoto() const override { return true; } virtual uint32_t getEvalFlags() const override { return EvalFlags::jumpToLabel; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; int32_t label = -1; ExpressionPtr subexpr = nullptr; @@ -53,6 +56,7 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isR2V() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; virtual void markNoDiscard() override; ExpressionPtr subexpr = nullptr; @@ -66,6 +70,7 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isRef2Ptr() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; virtual void markNoDiscard() override; ExpressionPtr subexpr = nullptr; @@ -79,6 +84,7 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isPtr2Ref() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr subexpr = nullptr; bool unsafeDeref = false; @@ -93,6 +99,7 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isAddr() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; string target; TypeDeclPtr funcType = nullptr; @@ -107,6 +114,7 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isNullCoalescing() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; virtual void markNoDiscard() override; ExpressionPtr defaultValue = nullptr; @@ -119,6 +127,7 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr subexpr = nullptr; ExpressionPtr sizeexpr = nullptr; @@ -133,6 +142,7 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isAt() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; virtual void markNoDiscard() override; ExpressionPtr subexpr = nullptr, index = nullptr; @@ -158,6 +168,7 @@ namespace das virtual bool rtti_isAt() const override { return false; } virtual bool rtti_isSafeAt() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; }; @@ -175,6 +186,7 @@ namespace das bool collapse(); static void collapse ( vector & res, const vector & lst ); virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; TypeDeclPtr makeBlockType () const; vector list; @@ -227,6 +239,7 @@ namespace das virtual bool rtti_isVar() const override { return true; } bool isGlobalVariable() const { return !local && !argument && !block; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; string name; VariablePtr variable = nullptr; @@ -256,6 +269,7 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr subexpr = nullptr; ExpressionPtr value = nullptr; @@ -274,6 +288,7 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isField() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; virtual void markNoDiscard() override; Structure::FieldDeclaration * field() const; @@ -333,6 +348,7 @@ namespace das virtual bool rtti_isField() const override { return false; } virtual bool rtti_isSafeAsVariant() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; bool skipQQ = false; }; @@ -345,6 +361,7 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isSwizzle() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr value = nullptr; string mask; @@ -370,6 +387,7 @@ namespace das virtual bool rtti_isField() const override { return false; } virtual bool rtti_isSafeField() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; bool skipQQ = false; }; @@ -384,6 +402,7 @@ namespace das virtual string describe() const; virtual bool rtti_isCallLikeExpr() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; string name; vector arguments; @@ -399,6 +418,7 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; Function * inFunction = nullptr; CallMacro * macro = nullptr; @@ -410,6 +430,7 @@ namespace das : ExprLooksLikeCall(a,n) { __rtti = "ExprCallFunc"; } virtual bool rtti_isCallFunc() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; Function * func = nullptr; uint32_t stackTop = 0; @@ -428,6 +449,7 @@ namespace das : ExprCallFunc(a,o), op(o) { __rtti = "ExprOp"; } virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; string op; }; @@ -444,6 +466,7 @@ namespace das virtual bool rtti_isOp1() const override { return true; } virtual string describe() const override; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; virtual void markNoDiscard() override; ExpressionPtr subexpr = nullptr; @@ -461,6 +484,7 @@ namespace das virtual bool rtti_isOp2() const override { return true; } virtual string describe() const override; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; virtual void markNoDiscard() override; ExpressionPtr left = nullptr, right = nullptr; @@ -474,6 +498,7 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; virtual bool rtti_isCopy() const override { return true; } union { @@ -494,6 +519,7 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; union { struct { @@ -513,6 +539,7 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; virtual bool rtti_isClone() const override { return true; } }; @@ -539,6 +566,7 @@ namespace das virtual bool rtti_isOp3() const override { return true; } virtual string describe() const override; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; virtual void markNoDiscard() override; ExpressionPtr subexpr = nullptr, left = nullptr, right = nullptr; @@ -552,6 +580,7 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual uint32_t getEvalFlags() const override; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr try_block = nullptr, catch_block = nullptr; }; @@ -569,6 +598,7 @@ namespace das } virtual bool rtti_isReturn() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr subexpr = nullptr; union { @@ -639,6 +669,7 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr ) const override; virtual bool rtti_isNullPtr() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; auto getValue() const { return ExprConstT::getValue(); }; }; @@ -671,6 +702,7 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual ExpressionPtr clone( ExpressionPtr expr ) const override; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; EnumerationPtr enumType = nullptr; string text; @@ -708,6 +740,7 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr ) const override; auto getValue() const { return ExprConstT::getValue(); }; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; TypeDeclPtr bitfieldType = nullptr; }; @@ -890,6 +923,7 @@ namespace das const string & getValue() const { return text; } virtual bool rtti_isStringConstant() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; string text; }; @@ -902,6 +936,7 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; vector elements; union { @@ -919,6 +954,7 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isLet() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; vector variables; LineInfo visibility; @@ -944,6 +980,7 @@ namespace das virtual uint32_t getEvalFlags() const override; virtual bool rtti_isFor() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; vector iterators; vector iteratorsAka; @@ -967,6 +1004,7 @@ namespace das virtual bool rtti_isUnsafe() const override { return true; } virtual ExpressionPtr visit(Visitor & vis) override; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr body = nullptr; }; @@ -980,6 +1018,7 @@ namespace das virtual bool rtti_isWhile() const override { return true; } virtual ExpressionPtr visit(Visitor & vis) override; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr cond = nullptr, body = nullptr; }; @@ -992,6 +1031,7 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isWith() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr with = nullptr, body = nullptr; }; @@ -1006,6 +1046,7 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isAssume() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; string alias; ExpressionPtr subexpr = nullptr; @@ -1051,6 +1092,7 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual bool rtti_isMakeBlock() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; vector capture; LineInfo captureAt; @@ -1072,6 +1114,7 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; TypeDeclPtr iterType = nullptr; vector capture; @@ -1085,6 +1128,7 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual uint32_t getEvalFlags() const override { return EvalFlags::yield; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr subexpr = nullptr; union { @@ -1104,6 +1148,7 @@ namespace das bool isCopyOrMove() const; __forceinline bool allowCmresSkip() const { return !cmresAlias && isCopyOrMove(); } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; uint32_t stackTop = 0; bool doesNotNeedSp = false; @@ -1117,6 +1162,7 @@ namespace das : ExprLikeCall(a,name) { isVerify = isV; __rtti = "ExprAssert"; } virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; bool isVerify = false; }; @@ -1128,6 +1174,7 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; }; @@ -1167,6 +1214,7 @@ namespace das } virtual ExpressionPtr visit ( Visitor & vis ) override; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; }; @@ -1182,6 +1230,7 @@ namespace das } virtual ExpressionPtr visit ( Visitor & vis ) override; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; }; @@ -1228,6 +1277,7 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; string trait; ExpressionPtr subexpr = nullptr; @@ -1244,6 +1294,7 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr subexpr = nullptr; TypeDeclPtr typeexpr = nullptr; @@ -1257,6 +1308,7 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isAscend() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr subexpr = nullptr; TypeDeclPtr ascType = nullptr; @@ -1278,6 +1330,7 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isCast() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr subexpr = nullptr; TypeDeclPtr castType = nullptr; @@ -1297,6 +1350,7 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; TypeDeclPtr typeexpr = nullptr; bool initializer = false; @@ -1310,6 +1364,7 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; virtual void markNoDiscard() override; bool doesNotNeedSp = false; @@ -1331,6 +1386,7 @@ namespace das virtual bool rtti_isIfThenElse() const override { return true; } virtual uint32_t getEvalFlags() const override; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr cond = nullptr, if_true = nullptr, if_false = nullptr; union { @@ -1385,6 +1441,7 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isNamedCall() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; string name; vector nonNamedArguments; @@ -1400,6 +1457,7 @@ namespace das virtual bool rtti_isMakeLocal() const override { return true; } virtual void setRefSp ( bool ref, bool cmres, uint32_t sp, uint32_t off ); virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; TypeDeclPtr makeType = nullptr; uint32_t stackTop = 0; @@ -1426,6 +1484,7 @@ namespace das virtual void setRefSp ( bool ref, bool cmres, uint32_t sp, uint32_t off ) override; virtual bool rtti_isMakeStruct() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; virtual void markNoDiscard() override; vector structs; @@ -1459,6 +1518,7 @@ namespace das virtual void setRefSp ( bool ref, bool cmres, uint32_t sp, uint32_t off ) override; virtual bool rtti_isMakeVariant() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; virtual void markNoDiscard() override; vector variants; @@ -1473,6 +1533,7 @@ namespace das virtual void setRefSp ( bool ref, bool cmres, uint32_t sp, uint32_t off ) override; virtual bool rtti_isMakeArray() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; virtual void markNoDiscard() override; TypeDeclPtr recordType = nullptr; @@ -1489,6 +1550,7 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual void setRefSp ( bool ref, bool cmres, uint32_t sp, uint32_t off ) override; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; bool isKeyValue = false; vector recordNames; @@ -1501,6 +1563,7 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr exprFor = nullptr; ExpressionPtr exprWhere = nullptr; @@ -1517,6 +1580,7 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isTypeDecl() const override { return true; } virtual void serialize( AstSerializer & ser ) override; + virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; TypeDeclPtr typeexpr = nullptr; }; diff --git a/include/daScript/ast/ast_visitor.h b/include/daScript/ast/ast_visitor.h index 901748f903..2806af408d 100644 --- a/include/daScript/ast/ast_visitor.h +++ b/include/daScript/ast/ast_visitor.h @@ -376,6 +376,11 @@ namespace das { return llk == this ? vis.visit(static_cast(this)) : llk; } + template + void ExprTableKeysOrValues::dispatch ( Visitor & vis ) { + vis.preVisit(static_cast(this)); + } + template ExpressionPtr ExprArrayCallWithSizeOrIndex::visit(Visitor & vis) { vis.preVisit(static_cast(this)); @@ -383,6 +388,11 @@ namespace das { return llk == this ? vis.visit(static_cast(this)) : llk; } + template + void ExprArrayCallWithSizeOrIndex::dispatch ( Visitor & vis ) { + vis.preVisit(static_cast(this)); + } + template ExpressionPtr ExprConstT::visit(Visitor & vis) { vis.preVisit((ExprConst*)this); diff --git a/src/ast/ast_dispatch.cpp b/src/ast/ast_dispatch.cpp new file mode 100644 index 0000000000..fafe036aa9 --- /dev/null +++ b/src/ast/ast_dispatch.cpp @@ -0,0 +1,91 @@ +#include "daScript/misc/platform.h" + +#include "daScript/ast/ast.h" +#include "daScript/ast/ast_expressions.h" +#include "daScript/ast/ast_visitor.h" + +// Per-class virtual shim for SerializeVisitor-style dispatch: +// static-type-aware single call to the matching Visitor::preVisit(ExprXxx*) overload, +// with no child walk and no post-visit. Abstract intermediate classes and templates +// without a matching preVisit overload fall through to preVisitExpression. + +namespace das +{ + + void Expression::dispatch ( Visitor & vis ) { + vis.preVisitExpression(this); + } + + // concrete Expression subclasses -> preVisit(ExprXxx*) + void ExprReader::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprLabel::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprGoto::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprRef2Value::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprRef2Ptr::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprPtr2Ref::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprAddr::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprNullCoalescing::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprDelete::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprAt::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprSafeAt::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprBlock::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprVar::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprTag::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprField::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprSafeAsVariant::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprSwizzle::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprSafeField::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprLooksLikeCall::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprCallMacro::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprOp1::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprOp2::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprCopy::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprMove::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprClone::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprOp3::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprTryCatch::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprReturn::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprConstPtr::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprConstEnumeration::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprConstBitfield::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprConstString::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprStringBuilder::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprLet::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprFor::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprUnsafe::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprWhile::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprWith::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprAssume::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprMakeBlock::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprMakeGenerator::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprYield::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprInvoke::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprAssert::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprQuote::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprTypeInfo::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprIs::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprAscend::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprCast::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprNew::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprCall::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprIfThenElse::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprNamedCall::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprMakeStruct::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprMakeVariant::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprMakeArray::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprMakeTuple::dispatch ( Visitor & vis ) { vis.preVisit(this); } + void ExprArrayComprehension::dispatch( Visitor & vis ) { vis.preVisit(this); } + void ExprTypeDecl::dispatch ( Visitor & vis ) { vis.preVisit(this); } + + // abstract intermediate classes -> route to nearest concrete preVisit overload + void ExprOp::dispatch ( Visitor & vis ) { vis.preVisit(static_cast(this)); } + void ExprCallFunc::dispatch ( Visitor & vis ) { vis.preVisit(static_cast(this)); } + void ExprMakeLocal::dispatch ( Visitor & vis ) { vis.preVisitExpression(this); } + + void ExprConst::dispatch ( Visitor & vis ) { vis.preVisit(this); } + + // ExprTableKeysOrValues / ExprArrayCallWithSizeOrIndex dispatch live in + // ast_visitor.h alongside their visit() templates so every TU that + // instantiates a specialization picks up the definition. + +} diff --git a/web/CMakeLists.txt b/web/CMakeLists.txt index d8bca88dee..03cdb357f9 100644 --- a/web/CMakeLists.txt +++ b/web/CMakeLists.txt @@ -324,6 +324,7 @@ SOURCE_GROUP_FILES("vecmath" VECMATH_SRC) SET(AST_SRC ../src/ast/ast.cpp +../src/ast/ast_dispatch.cpp ../src/ast/ast_interop.cpp ../src/ast/ast_tls.cpp ../src/ast/ast_visitor.cpp From 74ec5cb3015e3e397efbbe4d480757f2b0ac7556 Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Wed, 22 Apr 2026 15:15:59 +0300 Subject: [PATCH 2/8] compiler: extract serialize from ast Ast should not know anything about serialization. Now serializer is separate visitor declared in src/builtin/module_builtin_ast_serialize.cpp --- include/daScript/ast/ast.h | 3 +- include/daScript/ast/ast_expressions.h | 64 -- src/builtin/module_builtin_ast_serialize.cpp | 727 +++++++++++-------- 3 files changed, 430 insertions(+), 364 deletions(-) diff --git a/include/daScript/ast/ast.h b/include/daScript/ast/ast.h index f6deae79c9..01eaa904ea 100644 --- a/include/daScript/ast/ast.h +++ b/include/daScript/ast/ast.h @@ -692,7 +692,6 @@ namespace das virtual Expression * tail() { return this; } virtual bool swap_tail ( Expression *, Expression * ) { return false; } virtual uint32_t getEvalFlags() const { return 0; } - virtual void serialize ( AstSerializer & ser ); virtual void dispatch ( Visitor & vis ); virtual void gc_collect ( gc_root * target, gc_root * from ); LineInfo at; @@ -747,7 +746,7 @@ namespace das virtual bool rtti_isConstant() const override { return true; } template QQ & cvalue() { return *((QQ *)&value); } template const QQ & cvalue() const { return *((const QQ *)&value); } - virtual void serialize ( AstSerializer & ser ) override; + virtual void dispatch ( Visitor & vis ) override; Type baseType = Type::none; vec4f value = v_zero(); bool foldedNonConst = false; diff --git a/include/daScript/ast/ast_expressions.h b/include/daScript/ast/ast_expressions.h index 0e42c5935b..eb00710e7c 100644 --- a/include/daScript/ast/ast_expressions.h +++ b/include/daScript/ast/ast_expressions.h @@ -12,7 +12,6 @@ namespace das : Expression(a), macro(rm) { __rtti = "ExprReader"; } virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ReaderMacroPtr macro = nullptr; @@ -26,7 +25,6 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isLabel() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; int32_t label = -1; @@ -43,7 +41,6 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isGoto() const override { return true; } virtual uint32_t getEvalFlags() const override { return EvalFlags::jumpToLabel; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; int32_t label = -1; @@ -55,7 +52,6 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isR2V() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; virtual void markNoDiscard() override; @@ -69,7 +65,6 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isRef2Ptr() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; virtual void markNoDiscard() override; @@ -83,7 +78,6 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isPtr2Ref() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr subexpr = nullptr; @@ -98,7 +92,6 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isAddr() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; string target; @@ -113,7 +106,6 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isNullCoalescing() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; virtual void markNoDiscard() override; @@ -126,7 +118,6 @@ namespace das : Expression(a), subexpr(s) { __rtti = "ExprDelete"; } virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr subexpr = nullptr; @@ -141,7 +132,6 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isAt() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; virtual void markNoDiscard() override; @@ -167,7 +157,6 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isAt() const override { return false; } virtual bool rtti_isSafeAt() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; }; @@ -185,7 +174,6 @@ namespace das string getMangledName(bool includeName = false, bool includeResult = false) const; bool collapse(); static void collapse ( vector & res, const vector & lst ); - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; TypeDeclPtr makeBlockType () const; @@ -238,7 +226,6 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isVar() const override { return true; } bool isGlobalVariable() const { return !local && !argument && !block; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; string name; @@ -268,7 +255,6 @@ namespace das : Expression(a), subexpr(se), value(va), name(n) { __rtti = "ExprTag"; } virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr subexpr = nullptr; @@ -287,7 +273,6 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isField() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; virtual void markNoDiscard() override; @@ -347,7 +332,6 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isField() const override { return false; } virtual bool rtti_isSafeAsVariant() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; bool skipQQ = false; @@ -360,7 +344,6 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isSwizzle() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr value = nullptr; @@ -386,7 +369,6 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isField() const override { return false; } virtual bool rtti_isSafeField() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; bool skipQQ = false; @@ -401,7 +383,6 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual string describe() const; virtual bool rtti_isCallLikeExpr() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; string name; @@ -417,7 +398,6 @@ namespace das : ExprLooksLikeCall(a,n) { __rtti = "ExprCallMacro"; } virtual ExpressionPtr visit(Visitor & vis) override; virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; Function * inFunction = nullptr; @@ -429,7 +409,6 @@ namespace das ExprCallFunc ( const LineInfo & a, const string & n ) : ExprLooksLikeCall(a,n) { __rtti = "ExprCallFunc"; } virtual bool rtti_isCallFunc() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; Function * func = nullptr; @@ -448,7 +427,6 @@ namespace das ExprOp ( const LineInfo & a, const string & o ) : ExprCallFunc(a,o), op(o) { __rtti = "ExprOp"; } virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; string op; @@ -465,7 +443,6 @@ namespace das virtual bool swap_tail ( Expression * expr, Expression * swapExpr ) override; virtual bool rtti_isOp1() const override { return true; } virtual string describe() const override; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; virtual void markNoDiscard() override; @@ -483,7 +460,6 @@ namespace das virtual bool swap_tail ( Expression * expr, Expression * swapExpr ) override; virtual bool rtti_isOp2() const override { return true; } virtual string describe() const override; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; virtual void markNoDiscard() override; @@ -497,7 +473,6 @@ namespace das : ExprOp2(a, "=", l, r) { __rtti = "ExprCopy"; }; virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; virtual bool rtti_isCopy() const override { return true; } @@ -518,7 +493,6 @@ namespace das : ExprOp2(a, "<-", l, r) { __rtti = "ExprMove"; }; virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; union { @@ -538,7 +512,6 @@ namespace das : ExprOp2(a, ":=", l, r) { __rtti = "ExprClone"; }; virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; virtual bool rtti_isClone() const override { return true; } @@ -565,7 +538,6 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isOp3() const override { return true; } virtual string describe() const override; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; virtual void markNoDiscard() override; @@ -579,7 +551,6 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual uint32_t getEvalFlags() const override; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr try_block = nullptr, catch_block = nullptr; @@ -597,7 +568,6 @@ namespace das return ef; } virtual bool rtti_isReturn() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr subexpr = nullptr; @@ -668,7 +638,6 @@ namespace das TypeDeclPtr ptrType = nullptr; virtual ExpressionPtr clone( ExpressionPtr expr ) const override; virtual bool rtti_isNullPtr() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; auto getValue() const { return ExprConstT::getValue(); }; @@ -701,7 +670,6 @@ namespace das } virtual ExpressionPtr visit(Visitor & vis) override; virtual ExpressionPtr clone( ExpressionPtr expr ) const override; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; EnumerationPtr enumType = nullptr; @@ -739,7 +707,6 @@ namespace das : ExprConstT(a,i,Type::tBitfield) { __rtti = "ExprConstBitfield"; } virtual ExpressionPtr clone( ExpressionPtr expr ) const override; auto getValue() const { return ExprConstT::getValue(); }; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; TypeDeclPtr bitfieldType = nullptr; @@ -922,7 +889,6 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr ) const override; const string & getValue() const { return text; } virtual bool rtti_isStringConstant() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; string text; @@ -935,7 +901,6 @@ namespace das virtual bool rtti_isStringBuilder() const override { return true; } virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; vector elements; @@ -953,7 +918,6 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isLet() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; vector variables; @@ -979,7 +943,6 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual uint32_t getEvalFlags() const override; virtual bool rtti_isFor() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; vector iterators; @@ -1003,7 +966,6 @@ namespace das virtual uint32_t getEvalFlags() const override; virtual bool rtti_isUnsafe() const override { return true; } virtual ExpressionPtr visit(Visitor & vis) override; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr body = nullptr; @@ -1017,7 +979,6 @@ namespace das virtual uint32_t getEvalFlags() const override; virtual bool rtti_isWhile() const override { return true; } virtual ExpressionPtr visit(Visitor & vis) override; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr cond = nullptr, body = nullptr; @@ -1030,7 +991,6 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isWith() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr with = nullptr, body = nullptr; @@ -1045,7 +1005,6 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isAssume() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; string alias; @@ -1091,7 +1050,6 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual bool rtti_isMakeBlock() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; vector capture; @@ -1113,7 +1071,6 @@ namespace das ExprMakeGenerator ( const LineInfo & a, ExpressionPtr b = nullptr ); virtual ExpressionPtr visit(Visitor & vis) override; virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; TypeDeclPtr iterType = nullptr; @@ -1127,7 +1084,6 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual uint32_t getEvalFlags() const override { return EvalFlags::yield; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr subexpr = nullptr; @@ -1147,7 +1103,6 @@ namespace das virtual bool rtti_isInvoke() const override { return true; } bool isCopyOrMove() const; __forceinline bool allowCmresSkip() const { return !cmresAlias && isCopyOrMove(); } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; uint32_t stackTop = 0; @@ -1161,7 +1116,6 @@ namespace das ExprAssert ( const LineInfo & a, const string & name, bool isV ) : ExprLikeCall(a,name) { isVerify = isV; __rtti = "ExprAssert"; } virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; bool isVerify = false; @@ -1173,7 +1127,6 @@ namespace das : ExprLikeCall(a,name) { __rtti = "ExprQuote"; } virtual ExpressionPtr visit(Visitor & vis) override; virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; }; @@ -1213,7 +1166,6 @@ namespace das return cexpr; } virtual ExpressionPtr visit ( Visitor & vis ) override; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; }; @@ -1229,7 +1181,6 @@ namespace das return cexpr; } virtual ExpressionPtr visit ( Visitor & vis ) override; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; }; @@ -1276,7 +1227,6 @@ namespace das : Expression(a), trait(tr), typeexpr(d), subtrait(stt), extratrait(ett) { __rtti = "ExprTypeInfo"; } virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; string trait; @@ -1293,7 +1243,6 @@ namespace das : Expression(a), subexpr(s), typeexpr(t) { __rtti = "ExprIs"; } virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr subexpr = nullptr; @@ -1307,7 +1256,6 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isAscend() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr subexpr = nullptr; @@ -1329,7 +1277,6 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isCast() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr subexpr = nullptr; @@ -1349,7 +1296,6 @@ namespace das : ExprCallFunc(a,"new"), typeexpr(t), initializer(ini) { __rtti = "ExprNew"; } virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; TypeDeclPtr typeexpr = nullptr; @@ -1363,7 +1309,6 @@ namespace das virtual bool rtti_isCall() const override { return true; } virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; virtual void markNoDiscard() override; @@ -1385,7 +1330,6 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isIfThenElse() const override { return true; } virtual uint32_t getEvalFlags() const override; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr cond = nullptr, if_true = nullptr, if_false = nullptr; @@ -1440,7 +1384,6 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isNamedCall() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; string name; @@ -1456,7 +1399,6 @@ namespace das : Expression(at) { __rtti = "ExprMakeLocal"; } virtual bool rtti_isMakeLocal() const override { return true; } virtual void setRefSp ( bool ref, bool cmres, uint32_t sp, uint32_t off ); - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; TypeDeclPtr makeType = nullptr; @@ -1483,7 +1425,6 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual void setRefSp ( bool ref, bool cmres, uint32_t sp, uint32_t off ) override; virtual bool rtti_isMakeStruct() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; virtual void markNoDiscard() override; @@ -1517,7 +1458,6 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual void setRefSp ( bool ref, bool cmres, uint32_t sp, uint32_t off ) override; virtual bool rtti_isMakeVariant() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; virtual void markNoDiscard() override; @@ -1532,7 +1472,6 @@ namespace das virtual ExpressionPtr visit(Visitor & vis) override; virtual void setRefSp ( bool ref, bool cmres, uint32_t sp, uint32_t off ) override; virtual bool rtti_isMakeArray() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; virtual void markNoDiscard() override; @@ -1549,7 +1488,6 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual void setRefSp ( bool ref, bool cmres, uint32_t sp, uint32_t off ) override; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; bool isKeyValue = false; @@ -1562,7 +1500,6 @@ namespace das : Expression(at) { __rtti = "ExprArrayComprehension"; } virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; ExpressionPtr exprFor = nullptr; @@ -1579,7 +1516,6 @@ namespace das virtual ExpressionPtr clone( ExpressionPtr expr = nullptr ) const override; virtual ExpressionPtr visit(Visitor & vis) override; virtual bool rtti_isTypeDecl() const override { return true; } - virtual void serialize( AstSerializer & ser ) override; virtual void dispatch( Visitor & vis ) override; virtual void gc_collect ( gc_root * target, gc_root * from ) override; TypeDeclPtr typeexpr = nullptr; diff --git a/src/builtin/module_builtin_ast_serialize.cpp b/src/builtin/module_builtin_ast_serialize.cpp index bf0708c8b7..68955718b9 100644 --- a/src/builtin/module_builtin_ast_serialize.cpp +++ b/src/builtin/module_builtin_ast_serialize.cpp @@ -5,6 +5,7 @@ #include "daScript/ast/ast_serializer.h" #include "daScript/ast/ast_handle.h" #include "daScript/ast/ast.h" +#include "daScript/ast/ast_visitor.h" #include #include #include @@ -321,31 +322,6 @@ namespace das { return *this; } - AstSerializer & AstSerializer::operator << ( ExpressionPtr & expr ) { - dtag(HASH_TAG("ExpressionPtr")); - bool is_null = expr == nullptr; - *this << is_null; - if ( is_null ) { - if ( !writing ) expr = nullptr; - return *this; - } - if ( writing ) { - uint32_t rtti = hash_tag(expr->__rtti); - DAS_ASSERT(rtti); - *this << rtti; - expr->serialize(*this); - } else { - uint32_t rtti = 0; *this << rtti; - auto itA = rttiHash2Annotation.find(rtti); - SERIALIZER_VERIFYF(itA != rttiHash2Annotation.end(), "annotation '%u' is not found", rtti); - auto annotation = itA->second; - expr = (Expression *) static_cast(annotation)->factory(); - expr->serialize(*this); - } - dtag(HASH_TAG("/ExpressionPtr")); - return *this; - } - bool AstSerializer::isInThisModule ( Function * & ptr ) { return ptr->module == thisModule; } @@ -1328,447 +1304,602 @@ namespace das { } // Expressions +// +// Per-node serialization lives in SerializeVisitor. AstSerializer::operator<<(ExpressionPtr&) +// calls expr->dispatch(v), which selects the matching preVisit(ExprXxx*) override below. +// Helper methods serializeXxx(...) replicate the old virtual inheritance chain +// (ExprOp1 -> ExprOp -> ExprCallFunc -> ExprLooksLikeCall -> Expression) without +// relying on virtual dispatch inside this visitor. + + class SerializeVisitor : public Visitor { + AstSerializer & ser; + + void serializeBase ( Expression * expr ); + void serializeLooksLikeCall ( ExprLooksLikeCall * expr ); + void serializeCallFunc ( ExprCallFunc * expr ); + void serializeOp ( ExprOp * expr ); + void serializeOp2 ( ExprOp2 * expr ); + void serializeConst ( ExprConst * expr ); + void serializeMakeLocal ( ExprMakeLocal * expr ); + void serializeAt ( ExprAt * expr ); + void serializePtr2Ref ( ExprPtr2Ref * expr ); + void serializeField ( ExprField * expr ); + void serializeMakeArray ( ExprMakeArray * expr ); + + public: + explicit SerializeVisitor ( AstSerializer & s ) : ser(s) {} + using Visitor::preVisit; + + void preVisitExpression ( Expression * expr ) override; + void preVisit ( ExprReader * expr ) override; + void preVisit ( ExprLabel * expr ) override; + void preVisit ( ExprGoto * expr ) override; + void preVisit ( ExprRef2Value * expr ) override; + void preVisit ( ExprRef2Ptr * expr ) override; + void preVisit ( ExprPtr2Ref * expr ) override; + void preVisit ( ExprAddr * expr ) override; + void preVisit ( ExprNullCoalescing * expr ) override; + void preVisit ( ExprDelete * expr ) override; + void preVisit ( ExprAt * expr ) override; + void preVisit ( ExprSafeAt * expr ) override; + void preVisit ( ExprBlock * expr ) override; + void preVisit ( ExprVar * expr ) override; + void preVisit ( ExprTag * expr ) override; + void preVisit ( ExprField * expr ) override; + void preVisit ( ExprSafeAsVariant * expr ) override; + void preVisit ( ExprSwizzle * expr ) override; + void preVisit ( ExprSafeField * expr ) override; + void preVisit ( ExprLooksLikeCall * expr ) override; + void preVisit ( ExprCallMacro * expr ) override; + void preVisit ( ExprOp1 * expr ) override; + void preVisit ( ExprOp2 * expr ) override; + void preVisit ( ExprCopy * expr ) override; + void preVisit ( ExprMove * expr ) override; + void preVisit ( ExprClone * expr ) override; + void preVisit ( ExprOp3 * expr ) override; + void preVisit ( ExprTryCatch * expr ) override; + void preVisit ( ExprReturn * expr ) override; + void preVisit ( ExprConst * expr ) override; + void preVisit ( ExprConstPtr * expr ) override; + void preVisit ( ExprConstEnumeration * expr ) override; + void preVisit ( ExprConstBitfield * expr ) override; + void preVisit ( ExprConstString * expr ) override; + void preVisit ( ExprStringBuilder * expr ) override; + void preVisit ( ExprLet * expr ) override; + void preVisit ( ExprFor * expr ) override; + void preVisit ( ExprUnsafe * expr ) override; + void preVisit ( ExprWhile * expr ) override; + void preVisit ( ExprWith * expr ) override; + void preVisit ( ExprAssume * expr ) override; + void preVisit ( ExprMakeBlock * expr ) override; + void preVisit ( ExprMakeGenerator * expr ) override; + void preVisit ( ExprYield * expr ) override; + void preVisit ( ExprInvoke * expr ) override; + void preVisit ( ExprAssert * expr ) override; + void preVisit ( ExprQuote * expr ) override; + void preVisit ( ExprTypeInfo * expr ) override; + void preVisit ( ExprIs * expr ) override; + void preVisit ( ExprAscend * expr ) override; + void preVisit ( ExprCast * expr ) override; + void preVisit ( ExprNew * expr ) override; + void preVisit ( ExprCall * expr ) override; + void preVisit ( ExprIfThenElse * expr ) override; + void preVisit ( ExprNamedCall * expr ) override; + void preVisit ( ExprMakeStruct * expr ) override; + void preVisit ( ExprMakeVariant * expr ) override; + void preVisit ( ExprMakeArray * expr ) override; + void preVisit ( ExprMakeTuple * expr ) override; + void preVisit ( ExprArrayComprehension * expr ) override; + void preVisit ( ExprTypeDecl * expr ) override; + }; + + void SerializeVisitor::serializeBase ( Expression * expr ) { + ser << expr->at + << expr->type + << expr->genFlags + << expr->flags + << expr->printFlags; + ser.dtag(HASH_TAG("ptr_ref_count")); + } + + void SerializeVisitor::serializeLooksLikeCall ( ExprLooksLikeCall * expr ) { + serializeBase(expr); + ser << expr->name << expr->arguments; + ser << expr->argumentsFailedToInfer << expr->aliasSubstitution << expr->atEnclosure; + } + + void SerializeVisitor::serializeCallFunc ( ExprCallFunc * expr ) { + serializeLooksLikeCall(expr); + ser.serializePointer(expr->func); + ser << expr->stackTop; + } + + void SerializeVisitor::serializeOp ( ExprOp * expr ) { + serializeCallFunc(expr); + ser << expr->op; + } - void ExprReader::serialize ( AstSerializer & ser ) { + void SerializeVisitor::serializeOp2 ( ExprOp2 * expr ) { + serializeOp(expr); + ser << expr->left; + ser << expr->right; + } + + void SerializeVisitor::serializeConst ( ExprConst * expr ) { + serializeBase(expr); + ser << expr->baseType << expr->value << expr->foldedNonConst; + } + + void SerializeVisitor::serializeMakeLocal ( ExprMakeLocal * expr ) { + serializeBase(expr); + ser << expr->makeType << expr->stackTop << expr->extraOffset << expr->makeFlags; + } + + void SerializeVisitor::serializeAt ( ExprAt * expr ) { + ser.dtag(HASH_TAG("ExprAt")); + serializeBase(expr); + ser << expr->subexpr << expr->index; + ser << expr->atFlags; + } + + void SerializeVisitor::serializePtr2Ref ( ExprPtr2Ref * expr ) { + serializeBase(expr); + ser << expr->subexpr << expr->unsafeDeref << expr->assumeNoAlias; + } + + void SerializeVisitor::serializeField ( ExprField * expr ) { + ser.dtag(HASH_TAG("ExprField")); + serializeBase(expr); + ser << expr->value << expr->name << expr->atField + << expr->fieldIndex << expr->annotation << expr->derefFlags + << expr->fieldFlags; + + if ( ser.writing ) { + bool has_field = expr->value->type && ( + expr->value->type->isStructure() || ( expr->value->type->isPointer() && expr->value->type->firstType->isStructure() ) + ); + ser << has_field; + if ( !has_field ) return; + string mangledName; + if ( expr->value->type->isPointer() ) { + SERIALIZER_VERIFYF(expr->value->type->firstType->isStructure(), "expected to see structure field access via pointer"); + mangledName = expr->value->type->firstType->structType->getMangledName(); + ser << expr->value->type->firstType->structType->module; + } else { + SERIALIZER_VERIFYF(expr->value->type->isStructure(), "expected to see structure field access"); + mangledName = expr->value->type->structType->getMangledName(); + ser << expr->value->type->structType->module; + } + ser << mangledName; + if ( expr->annotation != nullptr && expr->annotation->getFieldOffset(expr->name) == static_cast(-1) ) { + LOG(LogLevel::warning) << "das: serialize: Field '" << expr->name << "' not found in '" << expr->annotation->name << "'"; + } + } else { + if ( expr->annotation != nullptr && expr->annotation->getFieldOffset(expr->name) == static_cast(-1) ) { + SERIALIZER_VERIFYF(false, "Field '%s' not found in '%s'", expr->name.c_str(), expr->annotation->name.c_str()); + } + bool has_field = false; ser << has_field; + if ( !has_field ) return; + Module * module = nullptr; ser << module; + string mangledName; ser << mangledName; + ser.fieldRefs.emplace_back(&expr->fieldRef, module, das::move(mangledName), expr->name); + } + } + + void SerializeVisitor::serializeMakeArray ( ExprMakeArray * expr ) { + serializeMakeLocal(expr); + ser << expr->recordType << expr->values << expr->gen2; + } + + void SerializeVisitor::preVisitExpression ( Expression * expr ) { + // ExprMakeLocal::dispatch routes here (no dedicated preVisit overload). + // ExprMakeLocal carries makeType/stackTop/extraOffset/makeFlags that + // serializeBase alone would drop, so detect it and use the proper helper. + if ( expr->rtti_isMakeLocal() ) { + serializeMakeLocal(static_cast(expr)); + return; + } + serializeBase(expr); + } + + void SerializeVisitor::preVisit ( ExprReader * expr ) { ser.dtag(HASH_TAG("ExprReader")); - Expression::serialize(ser); - ser << macro << sequence; + serializeBase(expr); + ser << expr->macro << expr->sequence; } - void ExprLabel::serialize ( AstSerializer & ser ) { + void SerializeVisitor::preVisit ( ExprLabel * expr ) { ser.dtag(HASH_TAG("ExprLabel")); - Expression::serialize(ser); - ser << label << comment; + serializeBase(expr); + ser << expr->label << expr->comment; } - void ExprGoto::serialize ( AstSerializer & ser ) { + void SerializeVisitor::preVisit ( ExprGoto * expr ) { ser.dtag(HASH_TAG("ExprGoto")); - Expression::serialize(ser); - ser << label << subexpr; + serializeBase(expr); + ser << expr->label << expr->subexpr; } - void ExprRef2Value::serialize ( AstSerializer & ser ) { + void SerializeVisitor::preVisit ( ExprRef2Value * expr ) { ser.dtag(HASH_TAG("ExprRef2Value")); - Expression::serialize(ser); - ser << subexpr; + serializeBase(expr); + ser << expr->subexpr; } - void ExprRef2Ptr::serialize ( AstSerializer & ser ) { + void SerializeVisitor::preVisit ( ExprRef2Ptr * expr ) { ser.dtag(HASH_TAG("ExprRef2Ptr")); - Expression::serialize(ser); - ser << subexpr; + serializeBase(expr); + ser << expr->subexpr; } - void ExprPtr2Ref::serialize ( AstSerializer & ser ) { + void SerializeVisitor::preVisit ( ExprPtr2Ref * expr ) { ser.dtag(HASH_TAG("ExprPtr2Ref")); - Expression::serialize(ser); - ser << subexpr << unsafeDeref << assumeNoAlias; + serializePtr2Ref(expr); } - void ExprAddr::serialize ( AstSerializer & ser ) { + void SerializeVisitor::preVisit ( ExprAddr * expr ) { ser.dtag(HASH_TAG("ExprAddr")); - Expression::serialize(ser); - ser << target << funcType; - ser.serializePointer(func); + serializeBase(expr); + ser << expr->target << expr->funcType; + ser.serializePointer(expr->func); } - void ExprNullCoalescing::serialize ( AstSerializer & ser ) { + void SerializeVisitor::preVisit ( ExprNullCoalescing * expr ) { ser.dtag(HASH_TAG("ExprNullCoalescing")); - ExprPtr2Ref::serialize(ser); - ser << defaultValue; + serializePtr2Ref(expr); + ser << expr->defaultValue; } - void ExprDelete::serialize ( AstSerializer & ser ) { + void SerializeVisitor::preVisit ( ExprDelete * expr ) { ser.dtag(HASH_TAG("ExprDelete")); - Expression::serialize(ser); - ser << subexpr << sizeexpr << native; + serializeBase(expr); + ser << expr->subexpr << expr->sizeexpr << expr->native; } - void ExprAt::serialize ( AstSerializer & ser ) { - ser.dtag(HASH_TAG("ExprAt")); - Expression::serialize(ser); - ser << subexpr << index; - ser << atFlags; + void SerializeVisitor::preVisit ( ExprAt * expr ) { + serializeAt(expr); } - void ExprSafeAt::serialize ( AstSerializer & ser ) { + void SerializeVisitor::preVisit ( ExprSafeAt * expr ) { ser.dtag(HASH_TAG("ExprSafeAt")); - ExprAt::serialize(ser); + serializeAt(expr); } - void ExprBlock::serialize ( AstSerializer & ser ) { + void SerializeVisitor::preVisit ( ExprBlock * expr ) { ser.dtag(HASH_TAG("ExprBlock")); - Expression::serialize(ser); + serializeBase(expr); if ( ser.writing ) { - void * thisBlock = this; + void * thisBlock = expr; ser << thisBlock; } else { void * thisBlock = nullptr; ser << thisBlock; - ser.exprBlockMap.emplace((uint64_t) thisBlock, this); + ser.exprBlockMap.emplace((uint64_t) thisBlock, expr); } - ser << list << finalList << returnType << arguments << stackTop - << stackVarTop << stackVarBottom << stackCleanVars << maxLabelIndex - << annotationData << annotationDataSid << blockFlags; - ser.serializePointer(inFunction); + ser << expr->list << expr->finalList << expr->returnType << expr->arguments << expr->stackTop + << expr->stackVarTop << expr->stackVarBottom << expr->stackCleanVars << expr->maxLabelIndex + << expr->annotationData << expr->annotationDataSid << expr->blockFlags; + ser.serializePointer(expr->inFunction); - serializeAnnotationList(ser, annotations); + serializeAnnotationList(ser, expr->annotations); } - void ExprVar::serialize ( AstSerializer & ser ) { + void SerializeVisitor::preVisit ( ExprVar * expr ) { ser.dtag(HASH_TAG("ExprVar")); - Expression::serialize(ser); + serializeBase(expr); - ser << name << argumentIndex << varFlags; - ser << pBlock; + ser << expr->name << expr->argumentIndex << expr->varFlags; + ser << expr->pBlock; // The variable is smart_ptr but we actually need // non-owning semantics if ( ser.writing ) { - bool inThisModule = variable == nullptr // this happens with [generic] functions, for example - || variable->module == nullptr - || variable->module == ser.thisModule; + bool inThisModule = expr->variable == nullptr // this happens with [generic] functions, for example + || expr->variable->module == nullptr + || expr->variable->module == ser.thisModule; ser << inThisModule; if ( inThisModule ) { - ser << variable; // serialize as smart pointer + ser << expr->variable; // serialize as smart pointer } else { - ser << variable->name; - ser << variable->module->nameHash; + ser << expr->variable->name; + ser << expr->variable->module->nameHash; } } else { bool inThisModule = false; ser << inThisModule; if ( inThisModule ) { - ser << variable; + ser << expr->variable; } else { string varname; uint64_t modname = 0; ser << varname << modname; auto mod = ser.moduleLibrary->findModuleByMangledNameHash(modname); SERIALIZER_VERIFYF(mod, "expected to find module '%llu'", modname); - variable = mod->findVariable(varname); + expr->variable = mod->findVariable(varname); } } } - void ExprTag::serialize ( AstSerializer & ser ) { + void SerializeVisitor::preVisit ( ExprTag * expr ) { ser.dtag(HASH_TAG("ExprTag")); - Expression::serialize(ser); - ser << subexpr << value << name; + serializeBase(expr); + ser << expr->subexpr << expr->value << expr->name; } - void ExprField::serialize ( AstSerializer & ser ) { - ser.dtag(HASH_TAG("ExprField")); - Expression::serialize(ser); - ser << value << name << atField - << fieldIndex << annotation << derefFlags - << fieldFlags; - - if ( ser.writing ) { - bool has_field = value->type && ( - value->type->isStructure() || ( value->type->isPointer() && value->type->firstType->isStructure() ) - ); - ser << has_field; - if ( !has_field ) return; - string mangledName; - if ( value->type->isPointer() ) { - SERIALIZER_VERIFYF(value->type->firstType->isStructure(), "expected to see structure field access via pointer"); - mangledName = value->type->firstType->structType->getMangledName(); - ser << value->type->firstType->structType->module; - } else { - SERIALIZER_VERIFYF(value->type->isStructure(), "expected to see structure field access"); - mangledName = value->type->structType->getMangledName(); - ser << value->type->structType->module; - } - ser << mangledName; - if ( annotation != nullptr && annotation->getFieldOffset(name) == static_cast(-1) ) { - LOG(LogLevel::warning) << "das: serialize: Field '" << name << "' not found in '" << annotation->name << "'"; - } - } else { - if ( annotation != nullptr && annotation->getFieldOffset(name) == static_cast(-1) ) { - SERIALIZER_VERIFYF("Field '%s' not found in '%s'", name.c_str(), annotation->name.c_str()); - } - bool has_field = false; ser << has_field; - if ( !has_field ) return; - Module * module = nullptr; ser << module; - string mangledName; ser << mangledName; - ser.fieldRefs.emplace_back(&fieldRef, module, das::move(mangledName), name); - } - } - - void ExprSafeAsVariant::serialize ( AstSerializer & ser ) { - ExprField::serialize(ser); - ser << skipQQ; - } - - void ExprSwizzle::serialize ( AstSerializer & ser ) { - Expression::serialize(ser); - ser << value << mask << fields << fieldFlags; + void SerializeVisitor::preVisit ( ExprField * expr ) { + serializeField(expr); } - void ExprSafeField::serialize ( AstSerializer & ser ) { - ExprField::serialize(ser); - ser << skipQQ; + void SerializeVisitor::preVisit ( ExprSafeAsVariant * expr ) { + serializeField(expr); + ser << expr->skipQQ; } - void ExprLooksLikeCall::serialize ( AstSerializer & ser ) { - Expression::serialize(ser); - ser << name << arguments; - ser << argumentsFailedToInfer << aliasSubstitution << atEnclosure; + void SerializeVisitor::preVisit ( ExprSwizzle * expr ) { + serializeBase(expr); + ser << expr->value << expr->mask << expr->fields << expr->fieldFlags; } - void ExprCallMacro::serialize ( AstSerializer & ser ) { - ExprLooksLikeCall::serialize(ser); - ser << macro; - ser.serializePointer(inFunction); + void SerializeVisitor::preVisit ( ExprSafeField * expr ) { + serializeField(expr); + ser << expr->skipQQ; } - void ExprCallFunc::serialize ( AstSerializer & ser ) { - ExprLooksLikeCall::serialize(ser); - ser.serializePointer(func); - ser << stackTop; - } - - void ExprOp::serialize ( AstSerializer & ser ) { - ExprCallFunc::serialize(ser); - ser << op; - } - - void ExprOp1::serialize ( AstSerializer & ser ) { - ExprOp::serialize(ser); - ser << subexpr; - } - - void ExprOp2::serialize ( AstSerializer & ser ) { - ExprOp::serialize(ser); - ser << left; - ser << right; + void SerializeVisitor::preVisit ( ExprLooksLikeCall * expr ) { + // ExprCallFunc::dispatch and ExprOp::dispatch route here (no dedicated + // preVisit overload). They carry func/stackTop/callFlags (and op for + // ExprOp) that serializeLooksLikeCall alone would drop, so detect them + // via __rtti and use the proper helper. + if ( expr->rtti_isCallFunc() ) { + if ( expr->__rtti && strcmp(expr->__rtti, "ExprOp") == 0 ) { + serializeOp(static_cast(expr)); + return; + } + serializeCallFunc(static_cast(expr)); + return; + } + serializeLooksLikeCall(expr); } - void ExprCopy::serialize ( AstSerializer & ser ) { - ExprOp2::serialize(ser); - ser << copyFlags; + void SerializeVisitor::preVisit ( ExprCallMacro * expr ) { + serializeLooksLikeCall(expr); + ser << expr->macro; + ser.serializePointer(expr->inFunction); } - void ExprMove::serialize ( AstSerializer & ser ) { - ExprOp2::serialize(ser); - ser << moveFlags; + void SerializeVisitor::preVisit ( ExprOp1 * expr ) { + serializeOp(expr); + ser << expr->subexpr; } - void ExprClone::serialize ( AstSerializer & ser ) { - ExprOp2::serialize(ser); + void SerializeVisitor::preVisit ( ExprOp2 * expr ) { + serializeOp2(expr); } - void ExprOp3::serialize ( AstSerializer & ser ) { - ExprOp::serialize(ser); - ser << subexpr << left << right; + void SerializeVisitor::preVisit ( ExprCopy * expr ) { + serializeOp2(expr); + ser << expr->copyFlags; } - void ExprTryCatch::serialize ( AstSerializer & ser ) { - Expression::serialize(ser); - ser << try_block << catch_block; + void SerializeVisitor::preVisit ( ExprMove * expr ) { + serializeOp2(expr); + ser << expr->moveFlags; } - void ExprReturn::serialize ( AstSerializer & ser ) { - Expression::serialize(ser); - ser << subexpr << returnFlags << stackTop << refStackTop - << returnFunc << block << returnType; + void SerializeVisitor::preVisit ( ExprClone * expr ) { + serializeOp2(expr); } - void ExprConst::serialize ( AstSerializer & ser ) { - Expression::serialize(ser); - ser << baseType << value << foldedNonConst; + void SerializeVisitor::preVisit ( ExprOp3 * expr ) { + serializeOp(expr); + ser << expr->subexpr << expr->left << expr->right; } - void ExprConstPtr::serialize( AstSerializer & ser ) { - ExprConstT::serialize(ser); - ser << isSmartPtr << ptrType; + void SerializeVisitor::preVisit ( ExprTryCatch * expr ) { + serializeBase(expr); + ser << expr->try_block << expr->catch_block; } - void ExprConstEnumeration::serialize( AstSerializer & ser ) { - ExprConst::serialize(ser); - ser << enumType << text; + void SerializeVisitor::preVisit ( ExprReturn * expr ) { + serializeBase(expr); + ser << expr->subexpr << expr->returnFlags << expr->stackTop << expr->refStackTop + << expr->returnFunc << expr->block << expr->returnType; } - void ExprConstBitfield::serialize( AstSerializer & ser ) { - ExprConst::serialize(ser); - ser << bitfieldType; + void SerializeVisitor::preVisit ( ExprConst * expr ) { + serializeConst(expr); } - void ExprConstString::serialize(AstSerializer& ser) { - ExprConst::serialize(ser); - ser << text; + void SerializeVisitor::preVisit ( ExprConstPtr * expr ) { + serializeConst(expr); + ser << expr->isSmartPtr << expr->ptrType; } - void ExprStringBuilder::serialize(AstSerializer& ser) { - Expression::serialize(ser); - ser << elements << stringBuilderFlags; + void SerializeVisitor::preVisit ( ExprConstEnumeration * expr ) { + serializeConst(expr); + ser << expr->enumType << expr->text; } - void ExprLet::serialize(AstSerializer& ser) { - Expression::serialize(ser); - ser << variables << visibility << atInit << letFlags; + void SerializeVisitor::preVisit ( ExprConstBitfield * expr ) { + serializeConst(expr); + ser << expr->bitfieldType; } - void ExprFor::serialize(AstSerializer& ser) { - Expression::serialize(ser); - ser << iterators << iteratorsAka << iteratorsAt << iteratorsTupleExpansion << iteratorsTags - << iteratorVariables << sources << body << visibility - << allowIteratorOptimization << canShadow; + void SerializeVisitor::preVisit ( ExprConstString * expr ) { + serializeConst(expr); + ser << expr->text; } - void ExprUnsafe::serialize(AstSerializer& ser) { - Expression::serialize(ser); - ser << body; + void SerializeVisitor::preVisit ( ExprStringBuilder * expr ) { + serializeBase(expr); + ser << expr->elements << expr->stringBuilderFlags; } - void ExprWhile::serialize(AstSerializer& ser) { - Expression::serialize(ser); - ser << cond << body; + void SerializeVisitor::preVisit ( ExprLet * expr ) { + serializeBase(expr); + ser << expr->variables << expr->visibility << expr->atInit << expr->letFlags; } - void ExprWith::serialize(AstSerializer& ser) { - Expression::serialize(ser); - ser << with << body; + void SerializeVisitor::preVisit ( ExprFor * expr ) { + serializeBase(expr); + ser << expr->iterators << expr->iteratorsAka << expr->iteratorsAt << expr->iteratorsTupleExpansion << expr->iteratorsTags + << expr->iteratorVariables << expr->sources << expr->body << expr->visibility + << expr->allowIteratorOptimization << expr->canShadow; } - void ExprAssume::serialize(AstSerializer& ser) { - Expression::serialize(ser); - ser << alias << subexpr << assumeType; + void SerializeVisitor::preVisit ( ExprUnsafe * expr ) { + serializeBase(expr); + ser << expr->body; } - void ExprMakeBlock::serialize(AstSerializer & ser) { - Expression::serialize(ser); - ser << capture << captureAt << block << stackTop << mmFlags; + void SerializeVisitor::preVisit ( ExprWhile * expr ) { + serializeBase(expr); + ser << expr->cond << expr->body; } - void ExprMakeGenerator::serialize(AstSerializer & ser) { - ExprLooksLikeCall::serialize(ser); - ser << iterType << capture << captureAt; + void SerializeVisitor::preVisit ( ExprWith * expr ) { + serializeBase(expr); + ser << expr->with << expr->body; } - void ExprYield::serialize(AstSerializer & ser) { - Expression::serialize(ser); - ser << subexpr << returnFlags; + void SerializeVisitor::preVisit ( ExprAssume * expr ) { + serializeBase(expr); + ser << expr->alias << expr->subexpr << expr->assumeType; } - void ExprInvoke::serialize(AstSerializer & ser) { - ExprLikeCall::serialize(ser); - ser << stackTop << doesNotNeedSp << isInvokeMethod << cmresAlias; + void SerializeVisitor::preVisit ( ExprMakeBlock * expr ) { + serializeBase(expr); + ser << expr->capture << expr->captureAt << expr->block << expr->stackTop << expr->mmFlags; } - void ExprAssert::serialize(AstSerializer & ser) { - ExprLikeCall::serialize(ser); - ser << isVerify; + void SerializeVisitor::preVisit ( ExprMakeGenerator * expr ) { + serializeLooksLikeCall(expr); + ser << expr->iterType << expr->capture << expr->captureAt; } - void ExprQuote::serialize(AstSerializer & ser) { - ExprLikeCall::serialize(ser); + void SerializeVisitor::preVisit ( ExprYield * expr ) { + serializeBase(expr); + ser << expr->subexpr << expr->returnFlags; } - template - void ExprTableKeysOrValues::serialize(AstSerializer & ser) { - ExprLooksLikeCall::serialize(ser); + void SerializeVisitor::preVisit ( ExprInvoke * expr ) { + serializeLooksLikeCall(expr); + ser << expr->stackTop << expr->doesNotNeedSp << expr->isInvokeMethod << expr->cmresAlias; } - template - void ExprArrayCallWithSizeOrIndex::serialize(AstSerializer & ser) { - ExprLooksLikeCall::serialize(ser); + void SerializeVisitor::preVisit ( ExprAssert * expr ) { + serializeLooksLikeCall(expr); + ser << expr->isVerify; } - void ExprTypeInfo::serialize(AstSerializer & ser) { - Expression::serialize(ser); - ser << trait << subexpr << typeexpr << subtrait << extratrait << macro; + void SerializeVisitor::preVisit ( ExprQuote * expr ) { + serializeLooksLikeCall(expr); } - void ExprIs::serialize(AstSerializer & ser) { - Expression::serialize(ser); - ser << subexpr << typeexpr; + void SerializeVisitor::preVisit ( ExprTypeInfo * expr ) { + serializeBase(expr); + ser << expr->trait << expr->subexpr << expr->typeexpr << expr->subtrait << expr->extratrait << expr->macro; } - void ExprAscend::serialize(AstSerializer & ser) { - Expression::serialize(ser); - ser << subexpr << ascType << stackTop << ascendFlags; + void SerializeVisitor::preVisit ( ExprIs * expr ) { + serializeBase(expr); + ser << expr->subexpr << expr->typeexpr; } - void ExprCast::serialize(AstSerializer & ser) { - Expression::serialize(ser); - ser << subexpr << castType << castFlags; + void SerializeVisitor::preVisit ( ExprAscend * expr ) { + serializeBase(expr); + ser << expr->subexpr << expr->ascType << expr->stackTop << expr->ascendFlags; } - void ExprNew::serialize(AstSerializer & ser) { - ExprCallFunc::serialize(ser); - ser << typeexpr << initializer; + void SerializeVisitor::preVisit ( ExprCast * expr ) { + serializeBase(expr); + ser << expr->subexpr << expr->castType << expr->castFlags; } - void ExprCall::serialize(AstSerializer & ser) { - ExprCallFunc::serialize(ser); - ser << doesNotNeedSp << cmresAlias; + void SerializeVisitor::preVisit ( ExprNew * expr ) { + serializeCallFunc(expr); + ser << expr->typeexpr << expr->initializer; } - void ExprIfThenElse::serialize ( AstSerializer & ser ) { - Expression::serialize(ser); - ser << cond << if_true << if_false << ifFlags; + void SerializeVisitor::preVisit ( ExprCall * expr ) { + serializeCallFunc(expr); + ser << expr->doesNotNeedSp << expr->cmresAlias; } - void MakeFieldDecl::serialize ( AstSerializer & ser ) { - ser << at << name << value << tag << flags; + void SerializeVisitor::preVisit ( ExprIfThenElse * expr ) { + serializeBase(expr); + ser << expr->cond << expr->if_true << expr->if_false << expr->ifFlags; } - void MakeStruct::serialize( AstSerializer & ser ) { - ser << static_cast &> ( *this ); + void SerializeVisitor::preVisit ( ExprNamedCall * expr ) { + serializeBase(expr); + ser << expr->name << expr->nonNamedArguments << expr->arguments << expr->argumentsFailedToInfer; } - void ExprNamedCall::serialize ( AstSerializer & ser ) { - Expression::serialize(ser); - ser << name << nonNamedArguments << arguments << argumentsFailedToInfer; + void SerializeVisitor::preVisit ( ExprMakeStruct * expr ) { + serializeMakeLocal(expr); + ser << expr->structs << expr->block << expr->makeStructFlags; + ser.serializePointer(expr->constructor); } - void ExprMakeLocal::serialize ( AstSerializer & ser ) { - Expression::serialize(ser); - ser << makeType << stackTop << extraOffset << makeFlags; + void SerializeVisitor::preVisit ( ExprMakeVariant * expr ) { + serializeMakeLocal(expr); + ser << expr->variants; } - void ExprMakeStruct::serialize ( AstSerializer & ser ) { - ExprMakeLocal::serialize(ser); - ser << structs << block << makeStructFlags; - ser.serializePointer(constructor); + void SerializeVisitor::preVisit ( ExprMakeArray * expr ) { + serializeMakeArray(expr); } - void ExprMakeVariant::serialize ( AstSerializer & ser ) { - ExprMakeLocal::serialize(ser); - ser << variants; + void SerializeVisitor::preVisit ( ExprMakeTuple * expr ) { + serializeMakeArray(expr); + ser << expr->isKeyValue << expr->recordNames; } - void ExprMakeArray::serialize ( AstSerializer & ser ) { - ExprMakeLocal::serialize(ser); - ser << recordType << values << gen2; + void SerializeVisitor::preVisit ( ExprArrayComprehension * expr ) { + serializeBase(expr); + ser << expr->exprFor << expr->exprWhere << expr->subexpr << expr->generatorSyntax << expr->tableSyntax; } - void ExprMakeTuple::serialize ( AstSerializer & ser ) { - ExprMakeArray::serialize(ser); - ser << isKeyValue << recordNames; + void SerializeVisitor::preVisit ( ExprTypeDecl * expr ) { + serializeBase(expr); + ser << expr->typeexpr; } - void ExprArrayComprehension::serialize ( AstSerializer & ser ) { - Expression::serialize(ser); - ser << exprFor << exprWhere << subexpr << generatorSyntax << tableSyntax; + AstSerializer & AstSerializer::operator << ( ExpressionPtr & expr ) { + dtag(HASH_TAG("ExpressionPtr")); + bool is_null = expr == nullptr; + *this << is_null; + if ( is_null ) { + if ( !writing ) expr = nullptr; + return *this; + } + SerializeVisitor sv(*this); + if ( writing ) { + uint32_t rtti = hash_tag(expr->__rtti); + DAS_ASSERT(rtti); + *this << rtti; + expr->dispatch(sv); + } else { + uint32_t rtti = 0; *this << rtti; + auto itA = rttiHash2Annotation.find(rtti); + SERIALIZER_VERIFYF(itA != rttiHash2Annotation.end(), "annotation '%u' is not found", rtti); + auto annotation = itA->second; + expr = (Expression *) static_cast(annotation)->factory(); + expr->dispatch(sv); + } + dtag(HASH_TAG("/ExpressionPtr")); + return *this; } - void ExprTypeDecl::serialize ( AstSerializer & ser ) { - Expression::serialize(ser); - ser << typeexpr; + void MakeFieldDecl::serialize ( AstSerializer & ser ) { + ser << at << name << value << tag << flags; } - void Expression::serialize ( AstSerializer & ser ) { - ser << at - << type - << genFlags - << flags - << printFlags; - ser.dtag(HASH_TAG("ptr_ref_count")); + void MakeStruct::serialize( AstSerializer & ser ) { + ser << static_cast &> ( *this ); } void FileInfo::serialize ( AstSerializer & ser ) { From a140e7a84da17beb63fd272192c008e33567364c Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Thu, 7 May 2026 00:10:42 +0300 Subject: [PATCH 3/8] serialization: unify failed tests naming We need to skip expected failures in serialization. To do so their naming should be unified. --- tests/README.md | 2 ++ tests/aot/CMakeLists.txt | 4 ++-- .../{test_constexpr_fail.das => failed_test_constexpr.das} | 0 ...issing_inherited.das => failed_test_missing_inherited.das} | 0 ...test_missing_method.das => failed_test_missing_method.das} | 0 ...led_containers_failed.das => failed_failed_containers.das} | 0 ...cal_classes_failed.das => failed_failed_local_classes.das} | 0 .../{test_linq_join_errors.das => failed_test_linq_join.das} | 0 ...porary_strings_failed.das => failed_temporary_strings.das} | 0 .../{unsafe_reference.das => failed_unsafe_reference.das} | 0 ...erify_completion.das => failed_test_verify_completion.das} | 0 11 files changed, 4 insertions(+), 2 deletions(-) rename tests/constexpr/{test_constexpr_fail.das => failed_test_constexpr.das} (100%) rename tests/interfaces/{test_missing_inherited.das => failed_test_missing_inherited.das} (100%) rename tests/interfaces/{test_missing_method.das => failed_test_missing_method.das} (100%) rename tests/language/{failed_containers_failed.das => failed_failed_containers.das} (100%) rename tests/language/{failed_local_classes_failed.das => failed_failed_local_classes.das} (100%) rename tests/linq/{test_linq_join_errors.das => failed_test_linq_join.das} (100%) rename tests/strings/{temporary_strings_failed.das => failed_temporary_strings.das} (100%) rename tests/unsafe/{unsafe_reference.das => failed_unsafe_reference.das} (100%) rename tests/verify/{test_verify_completion.das => failed_test_verify_completion.das} (100%) diff --git a/tests/README.md b/tests/README.md index 66f8a0250e..c63df3332a 100644 --- a/tests/README.md +++ b/tests/README.md @@ -4,6 +4,8 @@ Every `.das` file in this directory tree is listed below, grouped by subdirectory. Files marked **expect** use `expect` directives and are expected to produce specific compile errors. Helper/module files that are not standalone tests are marked *(helper)*. +**Naming convention for expected-failure tests:** Files that are expected to *fail compilation* use one of three filename prefixes: `failed_`, `cant_`, or `invalid_`. Tools that need to skip files with no compilable AST (e.g. `--ser` serialization passes) should filter on these three prefixes. + ## live_host/ > **Note:** These tests require the dasLiveHost module. Skipped automatically via `.das_test` when the module is not available. Run separately: `dastest -- --test tests/live_host/` diff --git a/tests/aot/CMakeLists.txt b/tests/aot/CMakeLists.txt index 2576d00547..3c6bad2f41 100644 --- a/tests/aot/CMakeLists.txt +++ b/tests/aot/CMakeLists.txt @@ -251,7 +251,7 @@ FILE(GLOB AOT_LINQ_FILES RELATIVE ${PROJECT_SOURCE_DIR} CONFIGURE_DEPENDS "tests # Exclude module files (they are compiled via DAS_AOT_LIB below) and # expect-error tests (use `expect` to assert intentional macro_failed # diagnostics; AOT cannot compile them) -list(FILTER AOT_LINQ_FILES EXCLUDE REGEX "/_|_errors\\.das$") +list(FILTER AOT_LINQ_FILES EXCLUDE REGEX "/_|failed_") # Linq test module files (libraries required by linq tests) SET(AOT_LINQ_MODULE_FILES @@ -295,7 +295,7 @@ FILE(GLOB AOT_SPOOF_FILES RELATIVE ${PROJECT_SOURCE_DIR} CONFIGURE_DEPENDS "test # AOT for strings test files FILE(GLOB AOT_STRINGS_FILES RELATIVE ${PROJECT_SOURCE_DIR} CONFIGURE_DEPENDS "tests/strings/*.das") # Exclude expect-error tests -list(FILTER AOT_STRINGS_FILES EXCLUDE REGEX "_failed") +list(FILTER AOT_STRINGS_FILES EXCLUDE REGEX "failed_") # AOT for template test files FILE(GLOB AOT_TEMPLATE_FILES RELATIVE ${PROJECT_SOURCE_DIR} CONFIGURE_DEPENDS "tests/template/*.das") diff --git a/tests/constexpr/test_constexpr_fail.das b/tests/constexpr/failed_test_constexpr.das similarity index 100% rename from tests/constexpr/test_constexpr_fail.das rename to tests/constexpr/failed_test_constexpr.das diff --git a/tests/interfaces/test_missing_inherited.das b/tests/interfaces/failed_test_missing_inherited.das similarity index 100% rename from tests/interfaces/test_missing_inherited.das rename to tests/interfaces/failed_test_missing_inherited.das diff --git a/tests/interfaces/test_missing_method.das b/tests/interfaces/failed_test_missing_method.das similarity index 100% rename from tests/interfaces/test_missing_method.das rename to tests/interfaces/failed_test_missing_method.das diff --git a/tests/language/failed_containers_failed.das b/tests/language/failed_failed_containers.das similarity index 100% rename from tests/language/failed_containers_failed.das rename to tests/language/failed_failed_containers.das diff --git a/tests/language/failed_local_classes_failed.das b/tests/language/failed_failed_local_classes.das similarity index 100% rename from tests/language/failed_local_classes_failed.das rename to tests/language/failed_failed_local_classes.das diff --git a/tests/linq/test_linq_join_errors.das b/tests/linq/failed_test_linq_join.das similarity index 100% rename from tests/linq/test_linq_join_errors.das rename to tests/linq/failed_test_linq_join.das diff --git a/tests/strings/temporary_strings_failed.das b/tests/strings/failed_temporary_strings.das similarity index 100% rename from tests/strings/temporary_strings_failed.das rename to tests/strings/failed_temporary_strings.das diff --git a/tests/unsafe/unsafe_reference.das b/tests/unsafe/failed_unsafe_reference.das similarity index 100% rename from tests/unsafe/unsafe_reference.das rename to tests/unsafe/failed_unsafe_reference.das diff --git a/tests/verify/test_verify_completion.das b/tests/verify/failed_test_verify_completion.das similarity index 100% rename from tests/verify/test_verify_completion.das rename to tests/verify/failed_test_verify_completion.das From 7b167130dc7912a0997987f3e0d2b0032673e211 Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Thu, 7 May 2026 00:10:57 +0300 Subject: [PATCH 4/8] serialization: fix leaks Fix module leaks. Now this all memory ownership is confusing and error-prone. To supress leak we should manually release module. --- src/builtin/module_builtin_ast_serialize.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/builtin/module_builtin_ast_serialize.cpp b/src/builtin/module_builtin_ast_serialize.cpp index 68955718b9..bebd96584a 100644 --- a/src/builtin/module_builtin_ast_serialize.cpp +++ b/src/builtin/module_builtin_ast_serialize.cpp @@ -2807,15 +2807,27 @@ namespace das { } */ } + // Module::serialize leaves g_Program pointing to a temporary program with a + // released thisModule. Restore it so compiling_module() sees the correct module + // during simulate() and while tests run inside the block. + auto & bound = *daScriptEnvironment::getBound(); + auto savedProg = bound.g_Program; + bound.g_Program = prog.get(); if ( prog->failToCompile ) { string err = "deserialization failed"; das_invoke::invoke,const string &>( context, at, block, false, ProgramPtr(), err); + (void)prog->thisModule.release(); + prog->library.reset(); + bound.g_Program = savedProg; return; } string okStr; das_invoke::invoke,const string &>( context, at, block, true, prog, okStr); + (void)prog->thisModule.release(); + prog->library.reset(); + bound.g_Program = savedProg; } void rtti_ast_serializer_get_data ( From ae69669184fff4f8a24557e7837b25e843941f60 Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Wed, 6 May 2026 13:52:30 -0700 Subject: [PATCH 5/8] super: walk up the inheritance chain past empty intermediates Fixes three related gaps when an intermediate class/struct in the chain defines neither the constructor, the method being overridden, nor a finalizer. * `super()` constructor: rewritten in InferTypes::visit (post-visit), not preVisit, so argument types are populated. Walks up classParent->parent->... and matches by argument types. Previously crashed with SIGSEGV on `super(arg)` because preVisit fed null arg->type into findMatchingFunctions. * `super.method()` already had walk-up logic (visit ExprCall) but was missing this fix. * `delete super.self`: walks up selfStruct->parent looking for the nearest ancestor whose type resolves a user-defined `finalize`. Filters out auto-generated finalizers (generateStructureFinalizer / makeClassFinalize) via Function::generated, since they don't chain to ancestors. Previously cast unconditionally to the immediate parent, silently dropping ancestor finalizers when the parent was empty. Tests/language: 4 new skip-level cases for super/super.method in super.das and 4 new cases (1-empty, 2-empty, mid-chain, struct-skip) in super_finalize.das. classes.rst and structs.rst updated to describe the walk-up. 925/925 tests/language pass under both interpreter and AOT. Co-Authored-By: Claude Opus 4.7 (1M context) --- doc/source/reference/language/classes.rst | 41 ++++++- doc/source/reference/language/structs.rst | 11 +- src/ast/ast_infer_type.cpp | 140 +++++++++++++++++---- tests/language/super.das | 56 +++++++++ tests/language/super_finalize.das | 142 ++++++++++++++++++++++ 5 files changed, 355 insertions(+), 35 deletions(-) diff --git a/doc/source/reference/language/classes.rst b/doc/source/reference/language/classes.rst index 8b1775ddf6..e17381b30e 100644 --- a/doc/source/reference/language/classes.rst +++ b/doc/source/reference/language/classes.rst @@ -146,6 +146,31 @@ Inside a derived class, ``super()`` calls the parent class constructor: Both forms are rewritten by the compiler into explicit calls to the parent class function: ``super()`` becomes ``Base`Base(self)`` and ``super.process(x)`` becomes ``Base`process(self, x)``. +If the immediate parent does not define a matching constructor or method, ``super`` walks +up the inheritance chain to the nearest ancestor that does: + +.. code-block:: das + + class Base { + def process(x : int) { /* ... */ } + } + + class Mid : Base { // empty intermediate + } + + class Leaf : Mid { + def Leaf { + super() // resolves to Base`Base(self) — Mid is skipped + } + def override process(x : int) { + super.process(x) // resolves to Base`process(self, x) — Mid is skipped + } + } + +Walk-up matches by argument types, so overloaded ``super(args)`` calls pick the closest +ancestor whose constructor or method accepts those arguments. If no ancestor matches, the +call is rejected at compile time. + Inside a derived class's finalizer (``operator delete``), ``delete super.self`` runs the parent's finalizer on the current object: @@ -158,13 +183,19 @@ parent's finalizer on the current object: } } -The compiler rewrites ``delete super.self`` into ``delete cast(self)``. It is only -valid inside an ``operator delete`` (or equivalently ``def finalize``) of a class that has -a base class; other uses are rejected at compile time. The same form also works for struct -finalizers declared as free functions — see :ref:`Structs `. +The compiler rewrites ``delete super.self`` into ``delete cast(self)``, where ``T`` +is the closest ancestor whose ``finalize`` lookup resolves to a user-defined finalizer. +For class hierarchies that means walking past intermediate classes that do not define +their own ``def operator delete``. For struct hierarchies, ``finalize`` resolution honors +inheritance substitution (a derived struct can be passed where its base is expected), so +``T`` may be the immediate parent even when only an ancestor defines ``operator delete``. +``delete super.self`` is only valid inside an ``operator delete`` (or equivalently +``def finalize``) of a class that has a base with a finalizer; other uses are rejected +at compile time. The same form also works for struct finalizers declared as free +functions — see :ref:`Structs `. Base-class finalization is explicit, not automatic: a derived finalizer that omits -``delete super.self`` will not run the base finalizer. +``delete super.self`` will not run any ancestor finalizer. The option ``always_call_super`` can be enabled to require ``super()`` in every constructor (see :ref:`Options `). diff --git a/doc/source/reference/language/structs.rst b/doc/source/reference/language/structs.rst index 6298ebe00a..ae5e40387f 100644 --- a/doc/source/reference/language/structs.rst +++ b/doc/source/reference/language/structs.rst @@ -238,9 +238,14 @@ whose first argument is ``self``. Inside such a finalizer for a derived struct, // additional cleanup } -The compiler rewrites ``delete super.self`` into ``delete cast(self)``. See -:ref:`Classes ` for the full description of the ``super`` sugar — it applies -identically to structs with inheritance. +The compiler rewrites ``delete super.self`` into ``delete cast(self)``, where ``T`` is +the closest ancestor struct whose ``finalize`` lookup resolves to a user-defined finalizer. +Because struct ``finalize`` resolution honors inheritance substitution (a derived struct +can be passed where its base is expected), ``T`` is typically the immediate parent — even +when only a more distant ancestor defines ``operator delete``. See :ref:`Classes ` +for the full description of the ``super`` sugar; the behavior is the same except that +classes lack this substitution and instead skip past empty intermediate classes that +don't define their own ``operator delete``. .. _structs_alignment: diff --git a/src/ast/ast_infer_type.cpp b/src/ast/ast_infer_type.cpp index 5d32f5cc64..5cc07ee2f7 100644 --- a/src/ast/ast_infer_type.cpp +++ b/src/ast/ast_infer_type.cpp @@ -1400,15 +1400,46 @@ namespace das { auto eVar = static_cast(eField->value); if (eVar->name == "super") { if (auto baseClass = func->classParent->parent) { - reportAstChanged(); - auto callName = "_::" + baseClass->name + "`" + eField->name; - auto newCall = new ExprCall(expr->at, callName); - newCall->atEnclosure = expr->atEnclosure; - newCall->arguments.push_back(new ExprVar(expr->at, "self")); + // We're in argumentsFailedToInfer because `super` itself doesn't resolve. + // The actual call args (i >= 2) may also still be uninferred — either + // null type, or alias/expr placeholder waiting on a later pass. Defer + // in either case (mirrors the allOtherInferred check below at line 1480 + // and the ExprLooksLikeCall contract that flags both as "not ready"). for (size_t i = 2; i != expr->arguments.size(); ++i) { - newCall->arguments.push_back(expr->arguments[i]); + if (!expr->arguments[i]->type || expr->arguments[i]->type->isAliasOrExpr()) { + return Visitor::visit(expr); + } + } + vector argTypes; + auto selfType = new TypeDecl(Type::tStructure); + selfType->structType = func->classParent; + argTypes.push_back(selfType); + for (size_t i = 2; i != expr->arguments.size(); ++i) { + argTypes.push_back(expr->arguments[i]->type); + } + while (baseClass) { + auto callName = "_::" + baseClass->name + "`" + eField->name; + auto fnCandidates = findMatchingFunctions(callName, argTypes, false); + if (fnCandidates.size() == 1) { + reportAstChanged(); + auto newCall = new ExprCall(expr->at, callName); + newCall->atEnclosure = expr->atEnclosure; + newCall->arguments.push_back(new ExprVar(expr->at, "self")); + for (size_t i = 2; i != expr->arguments.size(); ++i) { + newCall->arguments.push_back(expr->arguments[i]); + } + return newCall; + } else if ( fnCandidates.size() > 1 ) { + error("too many candidates for super call " + callName, + verbose ? program->describeCandidates(fnCandidates) : "", "", + expr->at, CompilationError::function_not_found); + return Visitor::visit(expr); + } + baseClass = baseClass->parent; } - return newCall; + error("call to super in " + func->name + " is not allowed, no matching super method " + eField->name, "", "", + expr->at, CompilationError::function_not_found); + return Visitor::visit(expr); } else { error("call to super in " + func->name + " is not allowed, no base class for " + func->classParent->name, "", "", expr->at, CompilationError::function_not_found); @@ -2510,16 +2541,41 @@ namespace das { "", "", expr->at, CompilationError::bad_delete); return Visitor::visit(expr); } - reportAstChanged(); - auto selfVar = new ExprVar(expr->at, "self"); - auto castT = new TypeDecl(baseStruct); - auto castExpr = new ExprCast(expr->at, selfVar, castT); - auto newDel = new ExprDelete(expr->at, castExpr); - newDel->alwaysSafe = true; - newDel->native = expr->native; - if (expr->sizeexpr) - newDel->sizeexpr = expr->sizeexpr->clone(); - return newDel; + // Walk up the inheritance chain to the nearest ancestor with a user-defined + // finalizer (mirrors the super() / super.method() walk-ups). For structs, finalize + // substitution via base type matches at the immediate parent even when only an + // ancestor defines `def operator delete`. For classes there's no such substitution, + // so we must skip empty intermediates and cast to the actual finalizer's class. + while (baseStruct) { + auto baseType = new TypeDecl(Type::tStructure); + baseType->structType = baseStruct; + vector argTypes; + argTypes.push_back(baseType); + auto fnList = findMatchingFunctions("finalize", argTypes, false); + // Skip auto-generated finalizers (generateStructureFinalizer / makeClassFinalize): + // they don't chain to ancestors. Only stop at a user-defined finalizer (or one + // that an ancestor's user finalizer matches via inheritance substitution). + bool hasUserFinalize = false; + for (auto &f : fnList) { + if (!f->generated) { hasUserFinalize = true; break; } + } + if (hasUserFinalize) { + reportAstChanged(); + auto selfVar = new ExprVar(expr->at, "self"); + auto castT = new TypeDecl(baseStruct); + auto castExpr = new ExprCast(expr->at, selfVar, castT); + auto newDel = new ExprDelete(expr->at, castExpr); + newDel->alwaysSafe = true; + newDel->native = expr->native; + if (expr->sizeexpr) + newDel->sizeexpr = expr->sizeexpr->clone(); + return newDel; + } + baseStruct = baseStruct->parent; + } + error("delete super.self in " + func->name + ": no ancestor of " + selfStruct->name + " defines a finalizer", + "", "", expr->at, CompilationError::bad_delete); + return Visitor::visit(expr); } } } @@ -5323,16 +5379,7 @@ namespace das { "use let _ = " + call->name + "(...)", "", call->at, CompilationError::result_discarded); } - if (func && func->isClassMethod && func->classParent && call->name == "super") { - if (auto baseClass = func->classParent->parent) { - call->name = baseClass->name + "`" + baseClass->name; - call->arguments.insert(call->arguments.begin(), new ExprVar(call->at, "self")); - reportAstChanged(); - } else { - error("call to super in " + func->name + " is not allowed, no base class for " + func->classParent->name, "", "", - call->at, CompilationError::function_not_found); - } - } + // super() constructor rewrite is done in visit(ExprCall*), once argument types are inferred } void InferTypes::preVisitCallArg(ExprCall *call, Expression *arg, bool last) { Visitor::preVisitCallArg(call, arg, last); @@ -5361,6 +5408,45 @@ namespace das { func->notInferred(); return Visitor::visit(expr); } + // super(args) constructor rewrite — runs in post-visit so argument types are inferred. + // Walks up the inheritance chain looking for a parent class constructor that matches the + // argument types; this lets `super(...)` call the closest ancestor's constructor when an + // intermediate class doesn't define its own (mirrors super.method() / delete super.self). + if (func && func->isClassMethod && func->classParent && expr->name == "super") { + if (auto baseClass = func->classParent->parent) { + vector argumentTypes; + auto selfType = new TypeDecl(Type::tStructure); + selfType->structType = func->classParent; + argumentTypes.reserve(1 + expr->arguments.size()); + argumentTypes.push_back(selfType); + for (auto &arg : expr->arguments) { + argumentTypes.push_back(arg->type); + } + while (baseClass) { + auto candidateName = baseClass->name + "`" + baseClass->name; + auto matching = findMatchingFunctions(candidateName, argumentTypes, false); + if (matching.size() == 1) { + expr->name = candidateName; + expr->arguments.insert(expr->arguments.begin(), new ExprVar(expr->at, "self")); + reportAstChanged(); + return Visitor::visit(expr); + } else if (matching.size() > 1) { + error("too many candidates for super constructor " + candidateName, + verbose ? program->describeCandidates(matching) : "", "", + expr->at, CompilationError::function_not_found); + return Visitor::visit(expr); + } + baseClass = baseClass->parent; + } + error("call to super in " + func->name + ": no matching super constructor for " + func->classParent->name, "", "", + expr->at, CompilationError::function_not_found); + return Visitor::visit(expr); + } else { + error("call to super in " + func->name + " is not allowed, no base class for " + func->classParent->name, "", "", + expr->at, CompilationError::function_not_found); + return Visitor::visit(expr); + } + } if (forceInscopePod) { bool resolvedBefore = expr->genericFunction && expr->func; expr->func = inferFunctionCall(expr, InferCallError::functionOrGeneric, expr->genericFunction ? expr->func : nullptr); diff --git a/tests/language/super.das b/tests/language/super.das index 7e349939b7..8cb9e209bb 100644 --- a/tests/language/super.das +++ b/tests/language/super.das @@ -59,6 +59,32 @@ class Derived2 : Derived { } } +// Skip-level inheritance: SkipMid is empty (inherits Base's constructor and methods). +// SkipLeaf calls super() / super.method() — both must walk past SkipMid up to Base. +class SkipMid : Base { +} + +class SkipLeaf : SkipMid { + z : int = 0 + def SkipLeaf { + super() + z = 100 + } + def SkipLeaf(val : int) { + super(val) + z = val + 100 + } + def override get_value() : int { + return x + z + } + def get_super_value() : int { + return super.get_value() + } + def override add(a, b : int) : int { + return super.add(a, b) + z + } +} + [test] def test_super(t : T?) { t |> run("super() calls parent default constructor") @(t : T?) { @@ -130,4 +156,34 @@ def test_super(t : T?) { delete d } } + t |> run("super() walks past empty intermediate class") @(t : T?) { + unsafe { + // SkipMid has no constructor of its own; super() in SkipLeaf must reach Base. + var s = SkipLeaf() + t |> equal(s.x, 10) // Base default ctor ran + t |> equal(s.z, 100) + } + } + t |> run("super(args) walks past empty intermediate class") @(t : T?) { + unsafe { + var s = SkipLeaf(7) + t |> equal(s.x, 7) + t |> equal(s.z, 107) + } + } + t |> run("super.method() walks past empty intermediate class") @(t : T?) { + unsafe { + var s = SkipLeaf() + // SkipLeaf.get_super_value calls super.get_value; SkipMid has no own override, + // so the call must resolve to Base.get_value (returns x). + t |> equal(s->get_super_value(), 10) + } + } + t |> run("super.method(args) walks past empty intermediate class") @(t : T?) { + unsafe { + var s = SkipLeaf() + // SkipLeaf.add calls super.add(a,b) + z; SkipMid has no add, walks to Base: a+b + t |> equal(s->add(3, 4), 107) + } + } } diff --git a/tests/language/super_finalize.das b/tests/language/super_finalize.das index 09c192ae51..e42dd24d9f 100644 --- a/tests/language/super_finalize.das +++ b/tests/language/super_finalize.das @@ -62,6 +62,105 @@ def operator delete(var self : StructDerived) { struct_del_derived ++ } +// Skip-level inheritance: empty SkipMid in the middle of the chain. +// `delete super.self` in SkipLeaf must walk past SkipMid and call SkipBase's finalizer. +var skip_class_base = 0 +var skip_class_leaf = 0 + +class SkipBase { + def operator delete { + skip_class_base ++ + } +} + +class SkipMid : SkipBase { +} + +class SkipLeaf : SkipMid { + def operator delete { + delete super.self + skip_class_leaf ++ + } +} + +// 4-level skip with two empty intermediates. +var deep_class_base = 0 +var deep_class_leaf = 0 + +class DeepBase { + def operator delete { + deep_class_base ++ + } +} + +class DeepMid1 : DeepBase { +} + +class DeepMid2 : DeepMid1 { +} + +class DeepLeaf : DeepMid2 { + def operator delete { + delete super.self + deep_class_leaf ++ + } +} + +// Mid-chain with finalizer: only DeepLeaf and one ancestor have finalizers, +// the immediate parent does not. Ensures walk-up still chains transitively. +var mid_class_base = 0 +var mid_class_b = 0 +var mid_class_leaf = 0 + +class MidBase { + def operator delete { + mid_class_base ++ + } +} + +class MidB : MidBase { + def operator delete { + delete super.self + mid_class_b ++ + } +} + +class MidEmpty : MidB { +} + +class MidLeaf : MidEmpty { + def operator delete { + delete super.self + mid_class_leaf ++ + } +} + +// Struct skip-level: SkipStructMid has no finalizer (struct case relies on +// substitution at the immediate parent — we still verify it works). +var skip_struct_base = 0 +var skip_struct_leaf = 0 + +struct SkipStructBase { + a : int +} + +def operator delete(var self : SkipStructBase) { + skip_struct_base ++ +} + +struct SkipStructMid : SkipStructBase { + m : int +} + +struct SkipStructLeaf : SkipStructMid { + z : int +} + +def operator delete(var self : SkipStructLeaf) { + delete super.self + skip_struct_leaf ++ +} + [test] def test_delete_super_self(t : T?) { t |> run("class derived operator delete chains to base via super.self") @(t : T?) { @@ -96,4 +195,47 @@ def test_delete_super_self(t : T?) { t |> equal(struct_del_base, 1) t |> equal(struct_del_derived, 1) } + t |> run("delete super.self walks past empty intermediate class") @(t : T?) { + skip_class_base = 0 + skip_class_leaf = 0 + var s = new SkipLeaf() + unsafe { + delete s + } + t |> equal(skip_class_base, 1) + t |> equal(skip_class_leaf, 1) + } + t |> run("delete super.self walks past two empty intermediate classes") @(t : T?) { + deep_class_base = 0 + deep_class_leaf = 0 + var d = new DeepLeaf() + unsafe { + delete d + } + t |> equal(deep_class_base, 1) + t |> equal(deep_class_leaf, 1) + } + t |> run("delete super.self walks past empty mid-chain to nearest finalizer") @(t : T?) { + mid_class_base = 0 + mid_class_b = 0 + mid_class_leaf = 0 + var m = new MidLeaf() + unsafe { + delete m + } + // MidLeaf -> (skip MidEmpty) -> MidB -> MidBase + t |> equal(mid_class_base, 1) + t |> equal(mid_class_b, 1) + t |> equal(mid_class_leaf, 1) + } + t |> run("struct delete super.self walks past empty intermediate struct") @(t : T?) { + skip_struct_base = 0 + skip_struct_leaf = 0 + var s = new SkipStructLeaf() + unsafe { + delete s + } + t |> equal(skip_struct_base, 1) + t |> equal(skip_struct_leaf, 1) + } } From 831113ce7e1c70d185b3148753ca69e17ab94003 Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Wed, 6 May 2026 17:52:58 +0300 Subject: [PATCH 6/8] daslib: improve logging We should never use print() inside daslib/. And we should always prefer to_log everywhere, not only in daslib. Also made logs in serialize more clear. --- daslib/aot_cpp.das | 3 ++- src/builtin/module_builtin_ast_serialize.cpp | 20 +++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/daslib/aot_cpp.das b/daslib/aot_cpp.das index aab68b87f5..2bd8278ae3 100644 --- a/daslib/aot_cpp.das +++ b/daslib/aot_cpp.das @@ -3990,11 +3990,12 @@ def public compile_and_simulate(input : string, var access; var mg : ModuleGroup set_aot(); compile_file(input, access, mg, cop) $(ok; var program : smart_ptr; issues) { if (!ok) { - print("failed to compile {input}\n{issues}\n") + to_log(LOG_ERROR, "failed to compile {input}\n{issues}\n") return } simulate(program) $(sok; var pctx : smart_ptr; serrors) { if (!sok) { + to_log(LOG_ERROR, "failed to simulate {input}\n{serrors}\n") panic("Failed to simulate {serrors}") } blk(program, pctx) diff --git a/src/builtin/module_builtin_ast_serialize.cpp b/src/builtin/module_builtin_ast_serialize.cpp index bebd96584a..6a46314710 100644 --- a/src/builtin/module_builtin_ast_serialize.cpp +++ b/src/builtin/module_builtin_ast_serialize.cpp @@ -531,7 +531,7 @@ namespace das { } else { string name; *this << name; string expect = func->name; - SERIALIZER_VERIFYF(name == expect, "expected different function"); + SERIALIZER_VERIFYF(name == expect, "expected different function %s %s", name.c_str(), expect.c_str()); } } return *this; @@ -2050,7 +2050,7 @@ namespace das { } } else { string fname; ser << fname; - SERIALIZER_VERIFYF(fname == f->name, "expected to serialize in the same order"); + SERIALIZER_VERIFYF(fname == f->name, "expected to serialize in the same order: %s != %s", fname.c_str(), f->name.c_str()); uint64_t size = 0; ser << size; f->useFunctions.reserve(size); for ( uint64_t i = 0; i < size; i++ ) { @@ -2096,7 +2096,7 @@ namespace das { } } else { string name; ser << name; - SERIALIZER_VERIFYF(name == f->name, "expected to serialize in the same order"); + SERIALIZER_VERIFYF(name == f->name, "expected to serialize in the same order: %s != %s", name.c_str(), f->name.c_str()); uint64_t size = 0; ser << size; f->useFunctions.reserve(size); for ( uint64_t i = 0; i < size; i++ ) { @@ -2141,7 +2141,7 @@ namespace das { } } else { string name; ser << name; - SERIALIZER_VERIFYF(name == f->name, "expected to serialize in the same order"); + SERIALIZER_VERIFYF(name == f->name, "expected to serialize in the same order: %s %s", name.c_str(), f->name.c_str()); uint64_t size = 0; ser << size; f->useGlobalVariables.reserve(size); for ( uint64_t i = 0; i < size; i++ ) { @@ -2186,7 +2186,7 @@ namespace das { } } else { string name; ser << name; - SERIALIZER_VERIFYF(name == f->name, "expected to serialize in the same order"); + SERIALIZER_VERIFYF(name == f->name, "expected to serialize in the same order: %s != %s", name.c_str(), f->name.c_str()); uint64_t size = 0; ser << size; f->useGlobalVariables.reserve(size); for ( uint64_t i = 0; i < size; i++ ) { @@ -2348,7 +2348,7 @@ namespace das { ser << f->name; } else { string fname; ser << fname; - SERIALIZER_VERIFYF(fname == f->name, "expected to walk in the same order"); + SERIALIZER_VERIFYF(fname == f->name, "expected to walk in the same order: %s != %s", fname.c_str(), f->name.c_str()); } serializeUseVariables(ser, f); serializeUseFunctions(ser, f); @@ -2359,18 +2359,20 @@ namespace das { ser << f->name; } else { string fname; ser << fname; - SERIALIZER_VERIFYF(fname == f->name, "expected to walk in the same order"); + SERIALIZER_VERIFYF(fname == f->name, "expected to walk in the same order: %s != %s", fname.c_str(), f->name.c_str()); } serializeUseVariables(ser, f); serializeUseFunctions(ser, f); }); - globals.foreach_with_hash ([&](VariablePtr g, uint64_t hash) { + globals.foreach ([&]( VariablePtr g ) { + uint64_t hash = hash64z(g->name.c_str()); if ( ser.writing ) { ser << hash; } else { uint64_t h = 0; ser << h; - SERIALIZER_VERIFYF(h == hash, "expected to walk in the same order"); + SERIALIZER_VERIFYF(h == hash, "expected to walk in the same order: %llu != %llu", + (unsigned long long) h, (unsigned long long) hash); } serializeUseVariables(ser, g); serializeUseFunctions(ser, g); From 98cb658b330fba6303eebb3d75739556ab629741 Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Wed, 6 May 2026 20:50:00 +0300 Subject: [PATCH 7/8] serialization: stable id in serialization Make ids unique and stable. Enable serialization in dastest and added to CI. --- .github/workflows/extended_checks.yml | 4 + dastest/dastest.das | 7 + include/daScript/ast/ast_serializer.h | 77 +++++-- src/builtin/module_builtin_ast_serialize.cpp | 209 +++++++++++-------- 4 files changed, 191 insertions(+), 106 deletions(-) diff --git a/.github/workflows/extended_checks.yml b/.github/workflows/extended_checks.yml index 278cb34a31..221d8432aa 100644 --- a/.github/workflows/extended_checks.yml +++ b/.github/workflows/extended_checks.yml @@ -181,6 +181,10 @@ jobs: run: | set -eux $BIN/daslang_static _dasroot_/dastest/dastest.das -- --color --failures-only --test ./tests + - name: "Test ser/deser" + run: | + $BIN/daslang _dasroot_/dastest/dastest.das -- --color --failures-only --test ./tests --ser serialized.bin + $BIN/daslang _dasroot_/dastest/dastest.das -- --color --failures-only --test ./tests --deser serialized.bin - name: "Run self-binder (bind_clangbind.das)" if: matrix.target == 'linux' diff --git a/dastest/dastest.das b/dastest/dastest.das index cb0ff899b4..8bd37f6224 100644 --- a/dastest/dastest.das +++ b/dastest/dastest.das @@ -156,6 +156,13 @@ def serialize_path(ctx : SuiteCtx, files : array, out_file : string) { var inscope access <- make_file_access(ctx.projectPath) access |> add_file_access_root("dastest", ctx.dastestRoot) for (file in files) { + // Tests whose basename starts with cant_, failed_, or invalid_ + // are expected-failure compile tests anywhere in the tests tree; + // they have nothing to serialize. + let base = base_name(file) + if (base |> starts_with("cant_") || base |> starts_with("failed_") || base |> starts_with("invalid_")) { + continue + } using() $(var mg : ModuleGroup) { using() $(var cop : CodeOfPolicies) { cop.aot_module = true diff --git a/include/daScript/ast/ast_serializer.h b/include/daScript/ast/ast_serializer.h index 490274149a..4f9ba018ca 100644 --- a/include/daScript/ast/ast_serializer.h +++ b/include/daScript/ast/ast_serializer.h @@ -64,6 +64,30 @@ namespace das { } }; + // Composite key for serializer maps. Pairs a raw pointer with a per- + // AstSerializer epoch so reused addresses don't collide with prior entries. + // Hoisted to namespace das so daslang_hash can be specialized before the + // map instantiations inside AstSerializer. + struct SerializeNodeId { + void * ptr = nullptr; + size_t epoch = 0; + bool operator == ( const SerializeNodeId & o ) const noexcept { + return ptr == o.ptr && epoch == o.epoch; + } + bool operator != ( const SerializeNodeId & o ) const noexcept { + return !(*this == o); + } + }; + + template <> + struct daslang_hash { + size_t operator () ( const SerializeNodeId & s ) const noexcept { + const size_t pmix = (reinterpret_cast(s.ptr) >> 4) + * size_t(0x9E3779B97F4A7C15ull); + return pmix ^ (s.epoch * size_t(0xBF58476D1CE4E5B9ull)); + } + }; + struct DAS_API AstSerializer { ~AstSerializer (); AstSerializer ( SerializationStorage * storage, bool isWriting ); @@ -90,26 +114,32 @@ namespace das { das_hash_set doNotDelete; // profile data uint64_t totMacroTime = 0; + // Per-program epoch for SerializeNodeId. Bumped at the start of each + // serialize/deserialize call so reused pointer addresses across program + // boundaries don't collide with prior map entries on this persistent + // serializer. Must be initialized — uninitialized epoch produces garbage + // keys and silent map collisions. + size_t epoch = 0; // pointers - das_hash_map exprBlockMap; + das_hash_map exprBlockMap; using DataOffset = uint64_t; - das_hash_map writingFileInfoMap; - das_hash_map readingFileInfoMap; - das_hash_map fileAccessMap; + das_hash_map writingFileInfoMap; + das_hash_map readingFileInfoMap; + das_hash_map fileAccessMap; // smart pointers - das_hash_map smartMakeFieldDeclMap; - das_hash_map smartEnumerationMap; - das_hash_map smartStructureMap; - das_hash_map smartVariableMap; - das_hash_map smartFunctionMap; - das_hash_map smartMakeStructMap; - das_hash_map smartTypeDeclMap; + das_hash_map smartMakeFieldDeclMap; + das_hash_map smartEnumerationMap; + das_hash_map smartStructureMap; + das_hash_map smartVariableMap; + das_hash_map smartFunctionMap; + das_hash_map smartMakeStructMap; + das_hash_map smartTypeDeclMap; // refs - vector> blockRefs; - vector> functionRefs; - vector> variableRefs; - vector> structureRefs; - vector> enumerationRefs; + vector> blockRefs; + vector> functionRefs; + vector> variableRefs; + vector> structureRefs; + vector> enumerationRefs; // fieldRefs tuple contains: fieldptr, module, structname, fieldname vector> fieldRefs; // parseModule tuple contains: moduleName, mtime, thisModule, thisModule @@ -185,6 +215,7 @@ namespace das { AstSerializer & operator << ( CaptureEntry & entry ); AstSerializer & operator << ( MakeFieldDeclPtr & ptr ); AstSerializer & operator << ( MakeStructPtr & ptr ); + AstSerializer & operator << ( SerializeNodeId & value ); // Top-level AstSerializer & operator << ( Module & module ); AstSerializer & serializeModule ( Module & module, bool already_exists ); @@ -194,9 +225,6 @@ namespace das { void serializeProgram ( ProgramPtr program, ModuleGroup & libGroup ) noexcept; bool serializeScript ( ProgramPtr program ) noexcept; - template - void serializeSmartPtr( smart_ptr & obj, das_hash_map> & objMap ); - template AstSerializer& operator << ( int (&value)[n] ) { serialize(value, n * sizeof(int)); return *this; @@ -248,10 +276,10 @@ namespace das { void writeIdentifications ( Variable * & ptr ); void writeIdentifications ( TypeInfoMacro * & ptr ); - void fillOrPatchLater ( Function * & func, uint64_t id ); - void fillOrPatchLater ( Enumeration * & ptr, uint64_t id ); - void fillOrPatchLater ( Structure * & ptr, uint64_t id ); - void fillOrPatchLater ( Variable * & ptr, uint64_t id ); + void fillOrPatchLater ( Function * & func, SerializeNodeId id ); + void fillOrPatchLater ( Enumeration * & ptr, SerializeNodeId id ); + void fillOrPatchLater ( Structure * & ptr, SerializeNodeId id ); + void fillOrPatchLater ( Variable * & ptr, SerializeNodeId id ); auto readModuleAndName () -> pair; auto readModuleAndNameHash () -> pair; @@ -262,6 +290,8 @@ namespace das { void findExternal ( Variable * & ptr ); void findExternal ( TypeInfoMacro * & ptr ); + SerializeNodeId getSerializeId(void *ptr) { return {ptr, epoch}; } + template void serialize_small_enum ( EnumType & baseType ) { if ( writing ) { @@ -290,6 +320,7 @@ namespace das { // Opaque handle to expose serializer to daslang. struct AstSerializerState { unique_ptr storage; + unique_ptr serializer; }; // Create a writing serializer. diff --git a/src/builtin/module_builtin_ast_serialize.cpp b/src/builtin/module_builtin_ast_serialize.cpp index 6a46314710..22a56c11e0 100644 --- a/src/builtin/module_builtin_ast_serialize.cpp +++ b/src/builtin/module_builtin_ast_serialize.cpp @@ -65,7 +65,7 @@ namespace das { } template - void patchRefs ( vector> & refs, const das_hash_map> & objects) { + void patchRefs ( vector> & refs, const das_hash_map> & objects) { for ( auto & p : refs ) { auto it = objects.find(p.second); if ( it == objects.end() ) { @@ -78,7 +78,7 @@ namespace das { } template - void patchRefs ( vector> & refs, const das_hash_map & objects) { + void patchRefs ( vector> & refs, const das_hash_map & objects) { for ( auto & p : refs ) { auto it = objects.find(p.second); if ( it == objects.end() ) { @@ -225,6 +225,64 @@ namespace das { } } + AstSerializer & AstSerializer::operator << ( SerializeNodeId & value ) { + dtag(HASH_TAG("SerializeNodeId")); + // 64-bit user-space pointers fit in 48 bits (canonical form on x86-64 + // and AArch64 Linux/macOS — top 16 bits are sign-extended copies of + // bit 47, and we only ever see user-space addresses here, so they are + // zero). Pack a small epoch into those unused top 16 bits so the + // common case is a single 8-byte word. Reserve sentinel 0xFFFF for + // "epoch overflow" — fall back to a separate adaptive-size write. + // 32-bit hosts have no headroom; always emit ptr + adaptive epoch. + if constexpr ( sizeof(void *) == 8 ) { + constexpr uint64_t kPtrMask = (uint64_t(1) << 48) - 1; + constexpr uint64_t kEpochOverflowTag = 0xFFFFull << 48; + if ( writing ) { + uintptr_t pbits = reinterpret_cast(value.ptr); + DAS_ASSERTF((uint64_t(pbits) & ~kPtrMask) == 0, + "SerializeNodeId: pointer %p has non-zero top 16 bits — " + "non-canonical address violates packing assumption", value.ptr); + if ( value.epoch < 0xFFFFull ) { + uint64_t packed = (uint64_t(value.epoch) << 48) | uint64_t(pbits); + *this << packed; + } else { + uint64_t packed = kEpochOverflowTag | uint64_t(pbits); + *this << packed; + uint32_t epoch32 = uint32_t(value.epoch); + serializeAdaptiveSize32(epoch32); + } + } else { + uint64_t packed = 0; + *this << packed; + uint64_t topBits = packed & ~kPtrMask; + value.ptr = reinterpret_cast(uintptr_t(packed & kPtrMask)); + if ( topBits == kEpochOverflowTag ) { + uint32_t epoch32 = 0; + serializeAdaptiveSize32(epoch32); + value.epoch = size_t(epoch32); + } else { + value.epoch = size_t(topBits >> 48); + } + } + } else { + // 32-bit: pointer fills the word; epoch goes in its own adaptive int. + if ( writing ) { + uint32_t pbits = uint32_t(reinterpret_cast(value.ptr)); + uint32_t epoch32 = uint32_t(value.epoch); + *this << pbits; + serializeAdaptiveSize32(epoch32); + } else { + uint32_t pbits = 0; + *this << pbits; + uint32_t epoch32 = 0; + serializeAdaptiveSize32(epoch32); + value.ptr = reinterpret_cast(uintptr_t(pbits)); + value.epoch = size_t(epoch32); + } + } + return *this; + } + AstSerializer & AstSerializer::operator << ( string & str ) { dtag(HASH_TAG("string")); if ( writing ) { @@ -368,7 +426,7 @@ namespace das { *this << ptr->module->nameHash << ptr->name; } - void AstSerializer::fillOrPatchLater ( Function * & func, uint64_t id ) { + void AstSerializer::fillOrPatchLater ( Function * & func, SerializeNodeId id ) { auto it = smartFunctionMap.find(id); if ( it == smartFunctionMap.end() ) { func = ( Function * ) 1; @@ -378,7 +436,7 @@ namespace das { } } - void AstSerializer::fillOrPatchLater ( Enumeration * & ptr, uint64_t id ) { + void AstSerializer::fillOrPatchLater ( Enumeration * & ptr, SerializeNodeId id ) { auto it = smartEnumerationMap.find(id); if ( it == smartEnumerationMap.end() ) { ptr = ( Enumeration * ) 1; @@ -388,7 +446,7 @@ namespace das { } } - void AstSerializer::fillOrPatchLater ( Structure * & ptr, uint64_t id ) { + void AstSerializer::fillOrPatchLater ( Structure * & ptr, SerializeNodeId id ) { auto it = smartStructureMap.find(id); if ( it == smartStructureMap.end() ) { ptr = ( Structure * ) 1; @@ -398,7 +456,7 @@ namespace das { } } - void AstSerializer::fillOrPatchLater ( Variable * & ptr, uint64_t id ) { + void AstSerializer::fillOrPatchLater ( Variable * & ptr, SerializeNodeId id ) { auto it = smartVariableMap.find(id); if ( it == smartVariableMap.end() ) { ptr = ( Variable * ) 1; @@ -475,9 +533,9 @@ namespace das { template AstSerializer & AstSerializer::serializePointer ( TT * & ptr ) { - uint64_t fid = uintptr_t(ptr); + auto fid = getSerializeId(ptr); *this << fid; - if ( !fid ) { + if ( !fid.ptr ) { if ( !writing ) ptr = nullptr; return *this; } @@ -503,9 +561,9 @@ namespace das { if ( writing && func ) { SERIALIZER_VERIFYF(!func->builtIn, "cannot serialize built-in function"); } - uint64_t id = uint64_t(uintptr_t(func)); + auto id = getSerializeId(func); *this << id; - if ( id == 0 ) { + if ( id.ptr == 0 ) { if ( !writing ) func = nullptr; return *this; } @@ -539,9 +597,12 @@ namespace das { AstSerializer & AstSerializer::operator << ( TypeInfoMacro * & ptr ) { dtag(HASH_TAG("TypeInfoMacroPtr")); - uint64_t id = uintptr_t(ptr); - *this << id; - if ( !id ) { + // TypeInfoMacro is not gc_node and is always external (lives in another + // module). It is identified by name+module hash, so the wire form only + // needs a presence bit — no pointer/id leaks into the stream. + bool is_null = ptr == nullptr; + *this << is_null; + if ( is_null ) { if ( !writing ) ptr = nullptr; return *this; } @@ -567,7 +628,7 @@ namespace das { if ( !writing ) type = nullptr; return *this; } - uint64_t id = intptr_t(type); + auto id = getSerializeId(type); *this << id; if ( writing ) { if ( smartTypeDeclMap[id] == nullptr ) { @@ -753,9 +814,9 @@ namespace das { } AstSerializer & AstSerializer::operator << ( StructurePtr & struct_ ) { - uint64_t id = uint64_t(uintptr_t(struct_)); + auto id = getSerializeId(struct_); *this << id; - if ( id == 0 ) { + if ( id.ptr == 0 ) { if ( !writing ) struct_ = nullptr; return *this; } @@ -802,14 +863,14 @@ namespace das { return *this; } if ( writing ) { - uint64_t p = (uint64_t) ptr.get(); + auto p = getSerializeId(ptr.get()); *this << p; if ( fileAccessMap[p] == nullptr ) { fileAccessMap[p] = ptr.get(); ptr->serialize(*this); } } else { - uint64_t p = 0; *this << p; + SerializeNodeId p; *this << p; if ( fileAccessMap[p] == nullptr ) { uint8_t tag = 0; *this << tag; switch ( tag ) { @@ -828,32 +889,6 @@ namespace das { return *this; } - // This method creates concrete (i.e. non-polymorphic types without duplications) - template - void AstSerializer::serializeSmartPtr( smart_ptr & obj, das_hash_map> & objMap) { - uint64_t id = uint64_t(uintptr_t(obj.get())); - *this << id; - if ( id == 0 ) { - if ( !writing ) obj = nullptr; - return; - } - if ( writing ) { - if ( objMap.find(id) == objMap.end() ) { - objMap[id] = obj; - obj->serialize(*this); - } - } else { - auto it = objMap.find(id); - if ( it == objMap.end() ) { - obj = make_smart(); - objMap[id] = obj; - obj->serialize(*this); - } else { - obj = it->second; - } - } - } - AstSerializer & AstSerializer::operator << ( EnumerationPtr & enum_type ) { if ( writing ) { bool builtin = enum_type->module->builtIn && !enum_type->module->promoted; @@ -863,7 +898,7 @@ namespace das { string name = enum_type->name; *this << module << name; } else { - uint64_t id = uint64_t(uintptr_t(enum_type)); + auto id = getSerializeId(enum_type); *this << id; if ( smartEnumerationMap.find(id) == smartEnumerationMap.end() ) { smartEnumerationMap[id] = enum_type; @@ -882,9 +917,9 @@ namespace das { enum_type = pModule->findEnum(name); SERIALIZER_VERIFYF(enum_type, "expected to find enumeration '%llu'::'%s'", module, name.c_str()); } else { - uint64_t id = 0; + SerializeNodeId id; *this << id; - SERIALIZER_VERIFYF(id != 0, "expected non-null enumeration id"); + SERIALIZER_VERIFYF(id.ptr != 0, "expected non-null enumeration id"); auto it = smartEnumerationMap.find(id); if ( it == smartEnumerationMap.end() ) { enum_type = new Enumeration(); @@ -914,9 +949,9 @@ namespace das { } AstSerializer & AstSerializer::operator << ( VariablePtr & var ) { - uint64_t id = uint64_t(uintptr_t(var)); + auto id = getSerializeId(var); *this << id; - if ( id == 0 ) { + if ( id.ptr == 0 ) { if ( !writing ) var = nullptr; return *this; } @@ -985,11 +1020,11 @@ namespace das { AstSerializer & AstSerializer::operator << ( ExprBlock * & block ) { dtag(HASH_TAG("ExprBlock*")); - void * addr = block; - *this << addr; - if ( !writing && addr ) { + auto id = getSerializeId(block); + *this << id; + if ( !writing && id.ptr ) { block = ( ExprBlock * ) 1; - blockRefs.emplace_back(&block, (uint64_t) addr); + blockRefs.emplace_back(&block, id); } return *this; } @@ -1570,11 +1605,11 @@ namespace das { serializeBase(expr); if ( ser.writing ) { - void * thisBlock = expr; - ser << thisBlock; + auto thisBlockId = ser.getSerializeId(expr); + ser << thisBlockId; } else { - void * thisBlock = nullptr; ser << thisBlock; - ser.exprBlockMap.emplace((uint64_t) thisBlock, expr); + SerializeNodeId thisBlockId; ser << thisBlockId; + ser.exprBlockMap.emplace(thisBlockId, expr); } ser << expr->list << expr->finalList << expr->returnType << expr->arguments << expr->stackTop @@ -2044,8 +2079,8 @@ namespace das { uint64_t mnh = usedFun->getMangledNameHash(); ser << module << mnh; } else { - void * addr = usedFun; - ser << addr; + auto fid = ser.getSerializeId(usedFun); + ser << fid; } } } else { @@ -2066,8 +2101,8 @@ namespace das { SERIALIZER_VERIFYF(fun, "expected to find function"); f->useFunctions.emplace(fun); } else { - void * addr = nullptr; ser << addr; - auto fun = ser.smartFunctionMap[(uint64_t)(uintptr_t) addr]; + SerializeNodeId fid; ser << fid; + auto fun = ser.smartFunctionMap[fid]; SERIALIZER_VERIFYF(fun, "expected to find function"); f->useFunctions.emplace(fun); } @@ -2089,9 +2124,8 @@ namespace das { uint64_t mnh = usedFun->getMangledNameHash(); ser << module << mnh; } else { - // we serialize the address of the function (not the function itself - void * addr = usedFun; - ser << addr; + auto fid = ser.getSerializeId(usedFun); + ser << fid; } } } else { @@ -2112,8 +2146,8 @@ namespace das { SERIALIZER_VERIFYF(fun, "expected to find function"); f->useFunctions.emplace(fun); } else { - void * addr = nullptr; ser << addr; - auto fun = ser.smartFunctionMap[(uint64_t)(uintptr_t) addr]; + SerializeNodeId fid; ser << fid; + auto fun = ser.smartFunctionMap[fid]; SERIALIZER_VERIFYF(fun, "expected to find function"); f->useFunctions.emplace(fun); } @@ -2135,8 +2169,8 @@ namespace das { string varname = use->name; ser << module << varname; } else { - void * addr = use; - ser << addr; + auto vid = ser.getSerializeId(use); + ser << vid; } } } else { @@ -2157,8 +2191,8 @@ namespace das { SERIALIZER_VERIFYF(var, "expected to find variable '%s::%s'", pModule->name.c_str(), varname.c_str()); f->useGlobalVariables.emplace(var); } else { - void * addr = nullptr; ser << addr; - auto var = ser.smartVariableMap[(uint64_t)(uintptr_t) addr]; + SerializeNodeId vid; ser << vid; + auto var = ser.smartVariableMap[vid]; SERIALIZER_VERIFYF(var, "expected to find variable"); f->useGlobalVariables.emplace(var); } @@ -2180,8 +2214,8 @@ namespace das { string varname = use->name; ser << module << varname; } else { - void * addr = use; - ser << addr; + auto vid = ser.getSerializeId(use); + ser << vid; } } } else { @@ -2202,8 +2236,8 @@ namespace das { SERIALIZER_VERIFYF(var, "expected to find variable '%s::%s'", pModule->name.c_str(), varname.c_str()); f->useGlobalVariables.emplace(var); } else { - void * addr = nullptr; ser << addr; - auto var = ser.smartVariableMap[(uint64_t)(uintptr_t) addr]; + SerializeNodeId vid; ser << vid; + auto var = ser.smartVariableMap[vid]; SERIALIZER_VERIFYF(var, "expected to find variable"); f->useGlobalVariables.emplace(var); } @@ -2522,6 +2556,9 @@ namespace das { // Used in eden void AstSerializer::serializeProgram ( ProgramPtr program, ModuleGroup & libGroup ) noexcept { auto & ser = *this; + // Bump epoch so reused pointer addresses across program boundaries + // get distinct SerializeNodeIds on this persistent serializer. + ser.epoch++; ser << program->thisNamespace << program->thisModuleName; @@ -2620,7 +2657,13 @@ namespace das { } // drop ref_counts + smartEnumerationMap.clear(); + smartStructureMap.clear(); + smartVariableMap.clear(); + smartFunctionMap.clear(); + smartMakeStructMap.clear(); smartTypeDeclMap.clear(); + exprBlockMap.clear(); } uint32_t AstSerializer::getVersion () { @@ -2644,6 +2687,10 @@ namespace das { // Used in daNetGame currently void Program::serialize ( AstSerializer & ser ) { + // Bump epoch so reused pointer addresses across program boundaries + // get distinct SerializeNodeIds on this persistent serializer. + ser.epoch++; + ser << thisNamespace << thisModuleName; ser << totalFunctions << totalVariables << newLambdaIndex; @@ -2761,7 +2808,7 @@ namespace das { AstSerializerState * rtti_create_ast_serializer () { auto state = new AstSerializerState(); state->storage = make_unique(); - // state->serializer = make_unique(state->storage.get(), true); + state->serializer = make_unique(state->storage.get(), true); return state; } @@ -2769,13 +2816,13 @@ namespace das { auto state = new AstSerializerState(); state->storage = make_unique(); state->storage->buffer.assign(data.data, data.data + data.size); - // state->serializer = make_unique(state->storage.get(), false); + state->serializer = make_unique(state->storage.get(), false); return state; } void rtti_delete_ast_serializer ( AstSerializerState * state ) { if ( state ) { - //state->serializer->moduleLibrary = nullptr; + state->serializer->moduleLibrary = nullptr; delete state; } } @@ -2784,9 +2831,7 @@ namespace das { AstSerializerState * state, const smart_ptr & program ) { auto & prog = const_cast &>(program); - auto serializer = make_unique(state->storage.get(), true); - prog->serialize(*serializer); - serializer->moduleLibrary = nullptr; + prog->serialize(*state->serializer); return !prog->failToCompile; } @@ -2797,9 +2842,7 @@ namespace das { auto prog = make_smart(); { gc_guard deserialize_gc_scope; - auto serializer = make_unique(state->storage.get(), false); - prog->serialize(*serializer); - serializer->moduleLibrary = nullptr; + prog->serialize(*state->serializer); /* // THIS ONES ARE FROM THE "already exist" MODULES auto leftover = deserialize_gc_scope.guard_root.gc_count; From 384f9746ff137a6cb1d1ef98f6d2cbc4fa48d149 Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Wed, 6 May 2026 13:39:08 -0700 Subject: [PATCH 8/8] dasSQLITE chunk 14c: schema rebuild via [struct_convert] Final piece of the migration trio (after 14a spine, 14b typed ALTER) - the rebuild path for schema changes SQLite's narrow ALTER vocabulary cannot express in place (PK/FK/CHECK changes, type narrowing). User-side surface collapses the SQLite 12-step recipe to three lines: keep the historical struct as [sql_table(legacy=true)], write a [struct_convert]-annotated converter, and call db |> convert_and_rename(type, type) from the migration body. Also: utils/hygiene gains a collapse-blank-lines rule chained after the private-docstring trim. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../reference/tutorials/sql_43_migrations.rst | 120 +++++- modules/dasSQLITE/daslib/sqlite_boost.das | 329 ++++++++++++---- modules/dasSQLITE/daslib/sqlite_linq.das | 26 +- modules/dasSQLITE/daslib/sqlite_migrate.das | 362 ++++++++++++++++-- .../dasSQLITE/failed_sql_index_on_legacy.das | 15 + ...ed_struct_convert_new_field_no_default.das | 25 ++ .../migrate_80_struct_convert_trivial.das | 37 ++ .../migrate_81_struct_convert_option_flip.das | 42 ++ ...grate_82_struct_convert_primitive_cast.das | 52 +++ ...igrate_83_struct_convert_option_unwrap.das | 42 ++ ...migrate_84_struct_convert_option_cross.das | 38 ++ ...igrate_85_struct_convert_user_overload.das | 40 ++ ...uct_convert_readonly_mention_preserves.das | 45 +++ ...87_struct_convert_override_via_mention.das | 42 ++ .../migrate_88_create_table_name_override.das | 45 +++ .../migrate_89_insert_name_override.das | 25 ++ .../migrate_90_sql_table_legacy_marker.das | 44 +++ .../migrate_91_full_rebuild_end_to_end.das | 68 ++++ .../migrate_92_hostile_name_safe.das | 34 ++ .../migrate_93_rebuild_attached_schema.das | 49 +++ .../migrate_94_rebuild_custom_named_index.das | 46 +++ ..._rebuild_mismatched_table_names_panics.das | 39 ++ tutorials/sql/43-migrations.das | 77 +++- utils/hygiene/rule_collapse_blank_lines.das | 31 ++ utils/hygiene/rules.das | 3 +- utils/hygiene/test_hygiene.das | 68 ++++ 26 files changed, 1596 insertions(+), 148 deletions(-) create mode 100644 tests/dasSQLITE/failed_sql_index_on_legacy.das create mode 100644 tests/dasSQLITE/failed_struct_convert_new_field_no_default.das create mode 100644 tests/dasSQLITE/migrate_80_struct_convert_trivial.das create mode 100644 tests/dasSQLITE/migrate_81_struct_convert_option_flip.das create mode 100644 tests/dasSQLITE/migrate_82_struct_convert_primitive_cast.das create mode 100644 tests/dasSQLITE/migrate_83_struct_convert_option_unwrap.das create mode 100644 tests/dasSQLITE/migrate_84_struct_convert_option_cross.das create mode 100644 tests/dasSQLITE/migrate_85_struct_convert_user_overload.das create mode 100644 tests/dasSQLITE/migrate_86_struct_convert_readonly_mention_preserves.das create mode 100644 tests/dasSQLITE/migrate_87_struct_convert_override_via_mention.das create mode 100644 tests/dasSQLITE/migrate_88_create_table_name_override.das create mode 100644 tests/dasSQLITE/migrate_89_insert_name_override.das create mode 100644 tests/dasSQLITE/migrate_90_sql_table_legacy_marker.das create mode 100644 tests/dasSQLITE/migrate_91_full_rebuild_end_to_end.das create mode 100644 tests/dasSQLITE/migrate_92_hostile_name_safe.das create mode 100644 tests/dasSQLITE/migrate_93_rebuild_attached_schema.das create mode 100644 tests/dasSQLITE/migrate_94_rebuild_custom_named_index.das create mode 100644 tests/dasSQLITE/migrate_95_rebuild_mismatched_table_names_panics.das create mode 100644 utils/hygiene/rule_collapse_blank_lines.das diff --git a/doc/source/reference/tutorials/sql_43_migrations.rst b/doc/source/reference/tutorials/sql_43_migrations.rst index a288a24616..aa70effa85 100644 --- a/doc/source/reference/tutorials/sql_43_migrations.rst +++ b/doc/source/reference/tutorials/sql_43_migrations.rst @@ -211,11 +211,118 @@ What stays on raw ``db |> exec(...)``: - DROP COLUMN / RENAME COLUMN --- old names aren't in the current struct, so daslang has nothing to validate against. - PK / UNIQUE inline / generated columns added post-hoc --- - SQLite can't ALTER these in place; needs a table rebuild - (chunk 14c, ``struct_convert``). + SQLite can't ALTER these in place; the rebuild path below + handles them. - ``CHECK`` constraints, FK ADD/DROP, column type changes. - Anything ad-hoc that doesn't map to a struct field. +Schema rebuild via ``struct_convert`` +====================================== + +SQLite's ``ALTER`` vocabulary handles ADD COLUMN, RENAME COLUMN, +RENAME TABLE, and DROP COLUMN cleanly. Everything else --- PK +changes, type narrowing, FK alterations, ``CHECK`` changes --- +needs a *rebuild*: build a new table with the desired shape, copy +the rows through a converter, drop the old, rename. SQLite docs +call this the "12-step recipe"; daslang collapses it to three +lines of user code. + +Three pieces work together: + +1. ``[sql_table(name=..., legacy=true)]`` keeps the OLD shape as + a *historical* struct. Read-only --- usable with + ``select_from`` and ``drop_table_if_exists``, but the compiler + refuses ``create_table`` / ``insert`` / ``update`` / ``delete`` + on it (the write-side helpers are not emitted; the natural + unresolved-overload error fires at the call site). + +2. ``[struct_convert] def my_v1_to_v2(old : S; var dst : T) {...}`` + is a function annotation that walks T's fields. For each one + it emits a single dispatch call: + ``_::struct_convert_field(tgt.X, src.X_or_renamed)``. + + The conversion itself lives in an overloaded + ``struct_convert_field`` set. dasSQLITE ships: + + - **Identity** (``T -> T``) --- ``dst = src``. + - **``S -> Option`` wrap** --- recurses on the inner + ``S -> T``, then wraps with ``some()``. So ``int -> Option`` + works because the inner ``int -> string`` overload fires. + - **``Option -> T`` unwrap** --- NULL collapses to + ``default``, then recurses on the inner ``S -> T``. + - **``Option -> Option`` cross-payload** --- unwraps, + dispatches the inner conversion, re-wraps (NULL stays NULL). + - **Primitive targets** --- ``int`` / ``int64`` / ``float`` / + ``double`` / ``string`` from any source via the type's + constructor (``int(src)``, ``int64(src)``, ...) or string + interp. + + To extend, drop your own overload in your module --- the + macro emits ``_::struct_convert_field(...)`` so the call + resolves at the user-side overload set:: + + def struct_convert_field(var dst : MyEnum&; src : int) : void { + dst = MyEnum(src) + } + + Now ``int -> MyEnum`` auto-derives. The ``Option -> int`` + path also picks up your overload via the recursive dispatch. + + The macro itself still handles: ``@sql_renamed_from = "OldName"`` + lookup (read from old's renamed field), default-init for fields + absent from S (uses ``T``'s field initializer or ``none()`` for + ``Option``), body validation (must be a brace block), and + user-override detection. An explicit ``dst.X = ...`` (or ``<-``, + ``:=``) on the LHS in the body suppresses the auto-fill for X. + +3. ``db |> convert_and_rename(type, type)`` runs the + whole rebuild: CREATE staging table, copy rows through the + converter, DROP original, ALTER ... RENAME staging -> target. + Lower-level ``db |> convert(type, type, name=...)`` is + available if you only need the staging step (and want to + manage the swap separately). + +The rebuild runs inside ``migrate_to_latest``'s big transaction +--- a later migration failing rolls the rebuild back too. + +.. code-block:: das + + [sql_table(name = "users", legacy = true)] + struct UserV6 { + @sql_primary_key Id : int + Name : string + LegacyEmail : string + } + + [sql_table(name = "users")] + struct User { + @sql_primary_key Id : int + Name : string + Email : string + } + + [struct_convert] + def my_v6_to_v7(old : UserV6; var dst : User) { + dst.Email = (old.LegacyEmail != "") + ? old.LegacyEmail + : "unknown@example.com" + } + + [sql_migration(version = 7, description = "restructure users")] + def migration_007(db : SqlRunner) { + db |> convert_and_rename(type, type) + } + +The auto-rules pick up ``Id`` and ``Name`` (same-name same-type); +the user body's one line handles ``Email`` (the new field). +``LegacyEmail`` is in S but not in T --- it gets dropped on +conversion. After the rebuild, the table named ``users`` has the +new shape with rows preserved. + +Path 2 (no legacy struct): hand-written +``db |> exec("INSERT INTO users_new (...) SELECT (...) FROM users")`` +also works. The legacy struct is convenience, not a requirement. + Adopting migrations on an existing DB ====================================== @@ -290,15 +397,6 @@ What does NOT ship Future work, also dovetails with the eventual ``dasSQL`` abstraction layer. -- **Struct-to-struct rebuild support** (``struct_convert``, - ``[sql_table(legacy=true)]``, ``name=`` overrides). Coming - in chunk 14c. For now, schema rebuilds are hand-written - ``CREATE TABLE T_new`` + ``INSERT ... SELECT`` --- works, - just verbose. The typed ALTER surface above covers the - additive cases (ADD COLUMN, CREATE INDEX); ``DROP COLUMN`` / - ``RENAME COLUMN`` stay on raw ``db |> exec(...)`` until that - chunk lands. - .. seealso:: Full source: :download:`tutorials/sql/43-migrations.das <../../../../tutorials/sql/43-migrations.das>` diff --git a/modules/dasSQLITE/daslib/sqlite_boost.das b/modules/dasSQLITE/daslib/sqlite_boost.das index 25fda37a89..defdb39c86 100644 --- a/modules/dasSQLITE/daslib/sqlite_boost.das +++ b/modules/dasSQLITE/daslib/sqlite_boost.das @@ -87,6 +87,7 @@ def private _qualify_schema(sql : string; schema_name : string) : string { out = out |> replace("DROP VIEW IF EXISTS \"", "DROP VIEW IF EXISTS {q}\"") out = out |> replace("DROP VIEW \"", "DROP VIEW {q}\"") out = out |> replace("DROP INDEX IF EXISTS \"", "DROP INDEX IF EXISTS {q}\"") + out = out |> replace("ALTER TABLE \"", "ALTER TABLE {q}\"") return out } @@ -1081,6 +1082,31 @@ def create_table(db : SqlRunner; t : auto(TT)) : void { } } +[unused_argument(t), template(t)] +def try_create_table(db : SqlRunner; t : type; name : string) : SqlError { + //! Like ``try_create_table(type)`` but emits the DDL with table identifier ``name`` instead of T's + //! own ``[sql_table(name=...)]``. Use during a struct_convert rebuild to land the new schema in + //! ``users_new`` before the swap. Indexes auto-derive against the runtime name. + let err = db |> try_exec(_::_sql_create_table_sql(default, name)) + if (err |> is_some) { + return err + } + let idx = _::_sql_create_indexes_sql(default, name) + if (empty(idx)) { + return none(type) + } + return db |> try_exec(idx) +} + +[unused_argument(t), template(t)] +def create_table(db : SqlRunner; t : type; name : string) : void { + //! Strict variant of ``try_create_table(type, name)``. Panics with the libsqlite3 errmsg on failure. + let err = db |> try_create_table(type, name) + if (err |> is_some) { + panic(err |> unwrap) + } +} + [template(t)] def try_drop_table_if_exists(db : SqlRunner; t : auto(TT)) : SqlError { //! Emits DROP TABLE IF EXISTS for the type ``T``. @@ -1207,6 +1233,32 @@ def insert(db : SqlRunner; row : auto(TT)) : int64 { return r |> unwrap } +def try_insert(db : SqlRunner; row : auto(TT); name : string) : Result { + //! Inserts a single row into table identifier ``name`` instead of T's own ``[sql_table(name=...)]``. + //! Used during a struct_convert rebuild to write into the staging table before the rename swap. + //! Same PK semantics as ``try_insert(row)`` — zero PK triggers the no-PK INSERT for autoincrement. + let pk_unset = _::_sql_pk_is_unset(row) + let sql = (pk_unset + ? _::_sql_insert_no_pk_sql(default, name) + : _::_sql_insert_with_pk_sql(default, name)) + let r = db |> try_step_dml(sql, $(var stmt : sqlite3_stmt?) { + _::_sql_bind_row(stmt, row, !pk_unset) + }, "insert") + if (r |> is_err) { + return err(r |> unwrap_err, type) + } + return ok(sqlite3_last_insert_rowid(db.sqlite_handle), type) +} + +def insert(db : SqlRunner; row : auto(TT); name : string) : int64 { + //! Strict variant of ``try_insert(row, name)``. Panics with the libsqlite3 errmsg on failure. + let r = db |> try_insert(row, name) + if (r |> is_err) { + panic(r |> unwrap_err) + } + return r |> unwrap +} + def try_insert(db : SqlRunner; rows : array) : Result { //! Batched INSERT inside a single BEGIN IMMEDIATE / COMMIT transaction. //! All rows must agree on PK presence — mixed-PK arrays return ``Err`` without opening a txn. @@ -2143,7 +2195,7 @@ def private resolve_schema_from_path(p : string) : string { return path_join(get_das_root(), p) } -def private is_option_field_type(td : TypeDeclPtr) : bool { +def is_option_field_type(td : TypeDeclPtr) : bool { if (td == null) { return false } @@ -2163,7 +2215,7 @@ def private is_option_field_type(td : TypeDeclPtr) : bool { return false } -def private unwrap_option_payload_type(var td : TypeDeclPtr) : TypeDeclPtr { +def unwrap_option_payload_type(var td : TypeDeclPtr) : TypeDeclPtr { // Unwrap Option → T (or td unchanged); typeMacro carries T in dimExpr[1], structType in `_value`. if (td == null) { return td @@ -2180,14 +2232,16 @@ def private unwrap_option_payload_type(var td : TypeDeclPtr) : TypeDeclPtr { } return clone_type((td.dimExpr[1] as ExprTypeDecl).typeexpr) } - if (td.baseType == Type.tStructure - && td.structType != null - && td.structType.name == "Option" - && td.structType._module != null - && td.structType._module.name == "option") { - for (f in td.structType.fields) { - if ("{f.name}" == "_value") { - return clone_type(f._type) + // Post-instantiation form: tStructure named `Option` or `Option`. Mirrors is_option_field_type's + // shape detection — module identity doesn't load-bear once we're inside an Option-shaped struct; + // template instantiation downstream of "option" stamps the callee's _module, not "option". + if (td.baseType == Type.tStructure && td.structType != null) { + let sname = string(td.structType.name) + if (sname == "Option" || sname |> starts_with("Option<")) { + for (f in td.structType.fields) { + if ("{f.name}" == "_value") { + return clone_type(f._type) + } } } } @@ -2301,6 +2355,12 @@ class SqlIndexMacro : AstStructureAnnotation { //! Stackable struct-level annotation: ``[sql_index(fields="A"|("A","B"), unique=?, name=?)]``. //! Must appear after ``[sql_table]`` in the annotation list (it rewrites that helper's body). def override apply(var st : StructurePtr; var group : ModuleGroup; args : AnnotationArgumentList; var errors : das_string) : bool { + // Reject on legacy=true structs — the [sql_table] emits no CREATE TABLE, so the index would + // have no statement to attach to and would collide with the current struct's index on rebuild. + if (find_struct_helper_fn("_sql_legacy_table_marker", st) != null) { + errors := "[sql_index] on '{st.name}': not supported on [sql_table(legacy=true)] structs (they don't define schema). Drop the [sql_index] annotation; the current (non-legacy) struct's indexes are reapplied automatically by ``convert_and_rename``." + return false + } let cols = find_string_array_annotation(args, "fields") if (length(cols) == 0) { errors := "[sql_index]: missing or empty `fields=` argument. Use `fields=\"ColName\"` for a single-column index or `fields=(\"A\", \"B\")` for a composite." @@ -2414,6 +2474,8 @@ def private find_parent_table_and_pk(var st : StructurePtr; parent_name : string class SqlTableMacro : AstStructureAnnotation { def override apply(var st : StructurePtr; var group : ModuleGroup; args : AnnotationArgumentList; var errors : das_string) : bool { let table_name = find_annotation(args, "name", string(st.name)) + // legacy=true: emit read-side helpers only. Writes fail as unresolved-overload. + let is_legacy = find_annotation(args, "legacy", false) var schema_assert_exprs : array let schema_from_arg = find_arg(args, "schema_from") @@ -2607,44 +2669,78 @@ class SqlTableMacro : AstStructureAnnotation { return false } - // [sql_index] siblings rewrite the placeholder body in finish() (st.annotations not populated yet here). + // Read-side helpers are always emitted; write-side helpers are skipped for legacy=true structs + // so the natural overload-resolution error fires when a caller tries to write to one. // schema_assert_exprs ride along inside _sql_table_name's body as typecheck-time concept_asserts. + if (is_legacy) { + // Marker so [sql_index] can reject legacy structs with a precise error. + var fn_legacy_marker = qmacro_function("_sql_legacy_table_marker") $(typ : $t(st)) : bool { + return true + } + fn_legacy_marker.flags |= FunctionFlags.generated + fn_legacy_marker.body |> force_at(st.at) + compiling_module() |> add_function(fn_legacy_marker) + } var fn_table_name = make_table_name_fn(st, table_name, schema_assert_exprs) compiling_module() |> add_function(fn_table_name) + // DROP is read-side enough — devs may legitimately drop a legacy table during migration cleanup. + // 2-arg form must be added BEFORE the 1-arg form so find_struct_helper_fn returns the 1-arg form. + var fn_drop_named = make_drop_sql_named_fn(st) + compiling_module() |> add_function(fn_drop_named) var fn_drop = make_drop_sql_fn(st, table_name) compiling_module() |> add_function(fn_drop) - var fn_create = make_create_table_sql_fn(st, table_name, fields) - compiling_module() |> add_function(fn_create) - // Empty `_sql_create_indexes_sql` placeholder; [sql_index] siblings accumulate into it. - var fn_indexes = make_create_indexes_sql_fn(st, "") - compiling_module() |> add_function(fn_indexes) - var fn_ins_pk = make_insert_sql_fn(st, table_name, fields, true) - compiling_module() |> add_function(fn_ins_pk) - var fn_ins_nopk = make_insert_sql_fn(st, table_name, fields, false) - compiling_module() |> add_function(fn_ins_nopk) - var fn_pk_unset = make_pk_is_unset_fn(st, fields) - compiling_module() |> add_function(fn_pk_unset) - var fn_bind = make_bind_row_fn(st, fields) - compiling_module() |> add_function(fn_bind) + // SELECT and read-row stay so select_from(type) works on legacy structs. + var fn_select_all_named = make_select_all_sql_named_fn(st, fields) + compiling_module() |> add_function(fn_select_all_named) var fn_select_all = make_select_all_sql_fn(st, table_name, fields) compiling_module() |> add_function(fn_select_all) var fn_read = make_read_row_fn(st, fields) compiling_module() |> add_function(fn_read) - var fn_upd_pk = make_update_by_pk_sql_fn(st, table_name, fields) - compiling_module() |> add_function(fn_upd_pk) - var fn_del_pk = make_delete_by_pk_sql_fn(st, table_name, fields) - compiling_module() |> add_function(fn_del_pk) - var fn_bind_upd = make_bind_row_for_update_fn(st, fields) - compiling_module() |> add_function(fn_bind_upd) - var fn_bind_pk = make_bind_row_pk_only_fn(st, fields) - compiling_module() |> add_function(fn_bind_pk) var fn_col_info = make_column_info_fn(st, fields) compiling_module() |> add_function(fn_col_info) + // ---- Write-side helpers (skipped for [sql_table(legacy=true)]) ---- + if (!is_legacy) { + var fn_create_named = make_create_table_sql_named_fn(st, fields) + compiling_module() |> add_function(fn_create_named) + var fn_create = make_create_table_sql_fn(st, table_name, fields) + compiling_module() |> add_function(fn_create) + // 2-arg form must be added BEFORE the 1-arg form so find_struct_helper_fn (used by [sql_index] + // finish-pass) returns the 1-arg form whose body owns the burned-in SQL. + var fn_indexes_named = make_create_indexes_sql_named_fn(st, table_name) + compiling_module() |> add_function(fn_indexes_named) + // Empty `_sql_create_indexes_sql` placeholder; [sql_index] siblings accumulate into it. + var fn_indexes = make_create_indexes_sql_fn(st, "") + compiling_module() |> add_function(fn_indexes) + var fn_ins_pk_named = make_insert_sql_named_fn(st, fields, true) + compiling_module() |> add_function(fn_ins_pk_named) + var fn_ins_pk = make_insert_sql_fn(st, table_name, fields, true) + compiling_module() |> add_function(fn_ins_pk) + var fn_ins_nopk_named = make_insert_sql_named_fn(st, fields, false) + compiling_module() |> add_function(fn_ins_nopk_named) + var fn_ins_nopk = make_insert_sql_fn(st, table_name, fields, false) + compiling_module() |> add_function(fn_ins_nopk) + var fn_pk_unset = make_pk_is_unset_fn(st, fields) + compiling_module() |> add_function(fn_pk_unset) + var fn_bind = make_bind_row_fn(st, fields) + compiling_module() |> add_function(fn_bind) + var fn_upd_pk_named = make_update_by_pk_sql_named_fn(st, fields) + compiling_module() |> add_function(fn_upd_pk_named) + var fn_upd_pk = make_update_by_pk_sql_fn(st, table_name, fields) + compiling_module() |> add_function(fn_upd_pk) + var fn_del_pk_named = make_delete_by_pk_sql_named_fn(st, fields) + compiling_module() |> add_function(fn_del_pk_named) + var fn_del_pk = make_delete_by_pk_sql_fn(st, table_name, fields) + compiling_module() |> add_function(fn_del_pk) + var fn_bind_upd = make_bind_row_for_update_fn(st, fields) + compiling_module() |> add_function(fn_bind_upd) + var fn_bind_pk = make_bind_row_pk_only_fn(st, fields) + compiling_module() |> add_function(fn_bind_pk) + } + return true } - def apply_schema_from(var st : StructurePtr; raw_path : string; tbl_name : string; @@ -2770,9 +2866,21 @@ class SqlTableMacro : AstStructureAnnotation { } def make_drop_sql_fn(var st : StructurePtr; table_name : string) : FunctionPtr { - let sql = "DROP TABLE IF EXISTS \"{table_name}\"" + // 1-arg wrapper forwards via `default` so `typ` stays unread (avoids "type expression result is used"). var fn = qmacro_function("_sql_drop_table_if_exists_sql") $(typ : $t(st)) : string { - return $v(sql) + return _sql_drop_table_if_exists_sql(default<$t(st)>, $v(table_name)) + } + fn.flags |= FunctionFlags.generated + fn.body |> force_at(st.at) + return <- fn + } + + def make_drop_sql_named_fn(var st : StructurePtr) : FunctionPtr { + // 2-arg form: runtime ``name`` lets a caller drop a staging/aliased table identifier + // (used by struct_convert rebuild). [unused_argument(typ)] because daslang dispatches on T, + // but the body needs only ``name``. + var fn = qmacro_function("_sql_drop_table_if_exists_sql") $(typ : $t(st); name : string) : string { + return "DROP TABLE IF EXISTS " + sql_quote_id(name) } fn.flags |= FunctionFlags.generated fn.body |> force_at(st.at) @@ -2790,12 +2898,44 @@ class SqlTableMacro : AstStructureAnnotation { return <- fn } + def make_create_indexes_sql_named_fn(var st : StructurePtr; table_name : string) : FunctionPtr { + //! 2-arg form: rewrites the 1-arg SQL onto ``name``. Auto-named ``idx__`` follows; user-explicit + //! ``[sql_index(name=...)]`` is preserved. + let orig_quoted = "\"{table_name}\"" + let orig_idx_prefix = "\"idx_{table_name}_" + var fn = qmacro_function("_sql_create_indexes_sql") $(typ : $t(st); name : string) : string { + let escaped_name = name |> replace("\"", "\"\"") + return (_sql_create_indexes_sql(default<$t(st)>) + |> replace($v(orig_quoted), "\"" + escaped_name + "\"") + |> replace($v(orig_idx_prefix), "\"idx_" + escaped_name + "_")) + } + fn.flags |= FunctionFlags.generated + fn.body |> force_at(st.at) + return <- fn + } + def make_create_table_sql_fn(var st : StructurePtr; table_name : string; fields : array) : FunctionPtr { + // 1-arg form: delegates to the 2-arg form with the struct's own [sql_table(name=...)] identifier. + // See make_drop_sql_fn for why we construct default<$t(st)> rather than forwarding typ. + var fn = qmacro_function("_sql_create_table_sql") $(typ : $t(st)) : string { + return _sql_create_table_sql(default<$t(st)>, $v(table_name)) + } + fn.flags |= FunctionFlags.generated + fn.body |> force_at(st.at) + return <- fn + } + + def make_create_table_sql_named_fn(var st : StructurePtr; fields : array) : FunctionPtr { + // 2-arg form: composes CREATE TABLE DDL against runtime ``name``. Per-field SQL is baked at macro time + // (column name, type, constraints), but the table identifier comes from the runtime parameter so a + // struct_convert rebuild can land the new schema in a staging table. var write_exprs : array - write_exprs |> reserve(3 * length(fields) + 2) + write_exprs |> reserve(3 * length(fields) + 4) - let header = "CREATE TABLE \"{table_name}\"(" - write_exprs |> push(qmacro_expr(${ writer |> write($v(header)); })) + // CREATE TABLE "" ( — name routed through sql_quote_id for `"` escaping. + write_exprs |> push(qmacro_expr(${ writer |> write("CREATE TABLE "); })) + write_exprs |> push(qmacro_expr(${ writer |> write(sql_quote_id(name)); })) + write_exprs |> push(qmacro_expr(${ writer |> write("("); })) for (i in range(length(fields))) { let info = fields[i] @@ -2830,7 +2970,7 @@ class SqlTableMacro : AstStructureAnnotation { } write_exprs |> push(qmacro_expr(${ writer |> write(")"); })) - var fn = qmacro_function("_sql_create_table_sql") $(typ : $t(st)) : string { + var fn = qmacro_function("_sql_create_table_sql") $(typ : $t(st); name : string) : string { return build_string() $(var writer) { $b(write_exprs) } @@ -2889,7 +3029,21 @@ class SqlTableMacro : AstStructureAnnotation { } def make_insert_sql_fn(var st : StructurePtr; table_name : string; fields : array; include_pk : bool) : FunctionPtr { - // Explicit column list skips GENERATED columns; falls back to `DEFAULT VALUES` when no bindable cols remain. + // 1-arg form: delegates to the same-named 2-arg form. The 1-arg call site historically takes + // either an instance OR `type`; the wrapper must not READ typ or the type-witness path errors. + let helper_name = include_pk ? "_sql_insert_with_pk_sql" : "_sql_insert_no_pk_sql" + var fn = qmacro_function(helper_name) $(typ : $t(st)) : string { + return $c(helper_name)(default<$t(st)>, $v(table_name)) + } + fn.flags |= FunctionFlags.generated + fn.body |> force_at(st.at) + return <- fn + } + + def make_insert_sql_named_fn(var st : StructurePtr; fields : array; include_pk : bool) : FunctionPtr { + // 2-arg form: composes the INSERT with runtime ``name``. The cols-list / placeholders chunk is + // burned at macro time; the table identifier is the runtime parameter so a struct_convert rebuild + // can target a staging table. let helper_name = include_pk ? "_sql_insert_with_pk_sql" : "_sql_insert_no_pk_sql" let cols = build_string() $(var w) { var first = true @@ -2919,11 +3073,13 @@ class SqlTableMacro : AstStructureAnnotation { first = false } } - let sql = (empty(cols) - ? "INSERT INTO \"{table_name}\" DEFAULT VALUES" - : "INSERT INTO \"{table_name}\" ({cols}) VALUES ({placeholders})") - var fn = qmacro_function(helper_name) $(typ : $t(st)) : string { - return $v(sql) + // Two-tail SQL: with explicit column list, or DEFAULT VALUES if every column is computed/excluded. + // name routed through sql_quote_id for `"` escaping; suffix carries the post-identifier tail only. + let suffix = (empty(cols) + ? " DEFAULT VALUES" + : " ({cols}) VALUES ({placeholders})") + var fn = qmacro_function(helper_name) $(typ : $t(st); name : string) : string { + return "INSERT INTO " + sql_quote_id(name) + $v(suffix) } fn.flags |= FunctionFlags.generated fn.body |> force_at(st.at) @@ -3001,6 +3157,17 @@ class SqlTableMacro : AstStructureAnnotation { } def make_select_all_sql_fn(var st : StructurePtr; table_name : string; fields : array) : FunctionPtr { + // 1-arg form: delegates to the 2-arg form. See make_drop_sql_fn for the default<$t(st)> rationale. + var fn = qmacro_function("_sql_select_all_sql") $(typ : $t(st)) : string { + return _sql_select_all_sql(default<$t(st)>, $v(table_name)) + } + fn.flags |= FunctionFlags.generated + fn.body |> force_at(st.at) + return <- fn + } + + def make_select_all_sql_named_fn(var st : StructurePtr; fields : array) : FunctionPtr { + // 2-arg form: column list is baked at macro time; table identifier is the runtime parameter. let col_list = build_string() $(var w) { for (i in range(length(fields))) { if (i > 0) { @@ -3011,9 +3178,9 @@ class SqlTableMacro : AstStructureAnnotation { w |> write("\"") } } - let sql = "SELECT {col_list} FROM \"{table_name}\"" - var fn = qmacro_function("_sql_select_all_sql") $(typ : $t(st)) : string { - return $v(sql) + let prefix = "SELECT {col_list} FROM " + var fn = qmacro_function("_sql_select_all_sql") $(typ : $t(st); name : string) : string { + return $v(prefix) + sql_quote_id(name) } fn.flags |= FunctionFlags.generated fn.body |> force_at(st.at) @@ -3059,12 +3226,24 @@ class SqlTableMacro : AstStructureAnnotation { } def make_update_by_pk_sql_fn(var st : StructurePtr; table_name : string; fields : array) : FunctionPtr { + // 1-arg form: delegates to the 2-arg form. See make_drop_sql_fn for the default<$t(st)> rationale. + // The PK-less / no-SET-columns runtime-panic stubs ride inside the 2-arg form. + var fn = qmacro_function("_sql_update_by_pk_sql") $(typ : $t(st)) : string { + return _sql_update_by_pk_sql(default<$t(st)>, $v(table_name)) + } + fn.flags |= FunctionFlags.generated + fn.body |> force_at(st.at) + return <- fn + } + + def make_update_by_pk_sql_named_fn(var st : StructurePtr; fields : array) : FunctionPtr { + // 2-arg form: composes the UPDATE with runtime ``name``. SET-clause is baked at macro time. // PK-less structs get a runtime-panic stub (concept_assert would fire on every [sql_table] decl). let pk_idx = find_pk_field(fields) if (pk_idx < 0) { let st_name = string(st.name) let msg = "[sql_table] {st_name}: update(row) / delete_(row) / delete_by_id are unavailable — no @sql_primary_key field. Use the macro forms `_sql_update(type<{st_name}>, where, set)` / `_sql_delete(type<{st_name}>, where)` instead." - var fn = qmacro_function("_sql_update_by_pk_sql") $(typ : $t(st)) : string { + var fn = qmacro_function("_sql_update_by_pk_sql") $(typ : $t(st); name : string) : string { panic($v(msg)) return "" } @@ -3092,7 +3271,7 @@ class SqlTableMacro : AstStructureAnnotation { if (empty(set_clause)) { let st_name = string(st.name) let msg = "[sql_table] {st_name}: update(row) has nothing to SET — every non-PK column is missing or @sql_computed. Add at least one writable non-PK column, or use raw `exec`." - var fn = qmacro_function("_sql_update_by_pk_sql") $(typ : $t(st)) : string { + var fn = qmacro_function("_sql_update_by_pk_sql") $(typ : $t(st); name : string) : string { panic($v(msg)) return "" } @@ -3100,9 +3279,10 @@ class SqlTableMacro : AstStructureAnnotation { fn.body |> force_at(st.at) return <- fn } - let sql = "UPDATE \"{table_name}\" SET {set_clause} WHERE \"{pk_col}\" = ?" - var fn = qmacro_function("_sql_update_by_pk_sql") $(typ : $t(st)) : string { - return $v(sql) + let prefix = "UPDATE " + let suffix = " SET {set_clause} WHERE \"{pk_col}\" = ?" + var fn = qmacro_function("_sql_update_by_pk_sql") $(typ : $t(st); name : string) : string { + return $v(prefix) + sql_quote_id(name) + $v(suffix) } fn.flags |= FunctionFlags.generated fn.body |> force_at(st.at) @@ -3110,11 +3290,22 @@ class SqlTableMacro : AstStructureAnnotation { } def make_delete_by_pk_sql_fn(var st : StructurePtr; table_name : string; fields : array) : FunctionPtr { + // 1-arg form: delegates to the 2-arg form. See make_drop_sql_fn for the default<$t(st)> rationale. + var fn = qmacro_function("_sql_delete_by_pk_sql") $(typ : $t(st)) : string { + return _sql_delete_by_pk_sql(default<$t(st)>, $v(table_name)) + } + fn.flags |= FunctionFlags.generated + fn.body |> force_at(st.at) + return <- fn + } + + def make_delete_by_pk_sql_named_fn(var st : StructurePtr; fields : array) : FunctionPtr { + // 2-arg form: composes the DELETE with runtime ``name``. WHERE-clause + PK column baked at macro time. let pk_idx = find_pk_field(fields) if (pk_idx < 0) { let st_name = string(st.name) let msg = "[sql_table] {st_name}: delete_(row) / delete_by_id require a @sql_primary_key field. Use `_sql_delete(type<{st_name}>, where)` instead." - var fn = qmacro_function("_sql_delete_by_pk_sql") $(typ : $t(st)) : string { + var fn = qmacro_function("_sql_delete_by_pk_sql") $(typ : $t(st); name : string) : string { panic($v(msg)) return "" } @@ -3123,9 +3314,10 @@ class SqlTableMacro : AstStructureAnnotation { return <- fn } let pk_col = fields[pk_idx].col_name - let sql = "DELETE FROM \"{table_name}\" WHERE \"{pk_col}\" = ?" - var fn = qmacro_function("_sql_delete_by_pk_sql") $(typ : $t(st)) : string { - return $v(sql) + let prefix = "DELETE FROM " + let suffix = " WHERE \"{pk_col}\" = ?" + var fn = qmacro_function("_sql_delete_by_pk_sql") $(typ : $t(st); name : string) : string { + return $v(prefix) + sql_quote_id(name) + $v(suffix) } fn.flags |= FunctionFlags.generated fn.body |> force_at(st.at) @@ -3685,7 +3877,6 @@ class private SqlFts5Macro : AstStructureAnnotation { let fname = string(field.name) let is_optional = is_option_field_type(field._type) let is_fts_rank = find_annotation(field.annotation, "sql_fts_rank", false) - // Reject every annotation that doesn't apply to a virtual table. for (rej in FTS5_REJECTED_ANNOTATIONS) { var present = false @@ -3956,7 +4147,6 @@ def private make_fts5_column_info_fn(var st : StructurePtr; fields : array exec(…)`). [macro_function] def private resolve_sql_table(prog : ProgramPtr; at : LineInfo; var typeArg : ExpressionPtr; macroName : string; var st : StructurePtr&; var table_name : string&) : bool { - // Decode `type` arg: T must be a struct carrying the [sql_table]-generated - // `_sql_table_name` helper, otherwise we have no table identifier to ALTER. + // T must carry [sql_table]'s _sql_table_name helper. if (typeArg._type == null) { macro_error(prog, at, "{macroName}: `type` argument has no inferred type yet") return false @@ -4313,11 +4498,6 @@ def private decode_string_const(e : ExpressionPtr) : tuple add_column(type, "FieldName" [, defaultLiteral])``. - //! Emits ``ALTER TABLE "" ADD COLUMN "" [ NOT NULL][ DEFAULT …]``. - //! Honors @sql_column rename, @sql_json / @sql_blob storage. Rejects PK / UNIQUE / - //! computed / @sql_default_fn at macro time (those need a table rebuild). def override visit(prog : ProgramPtr; mod : Module?; var call : ExprCallMacro?) : Expression? { // call.arguments = [db, type, "FieldName"] (3) or [..., defaultLit] (4) after the `db |>` desugar. let nArgs = call.arguments |> length @@ -4426,8 +4606,7 @@ class private AddColumnMacro : AstCallMacro { def private resolve_field_columns(prog : ProgramPtr; at : LineInfo; var st : StructurePtr; fieldsArg : ExpressionPtr; macroName : string; var sql_cols : array&) : bool { - // Accept a single string literal ("Email") or a tuple literal of string literals ("A", "B"). - // Array literals like ["A", "B"] parse as `to_array_move(fixed_array(…))` — too hairy to AST-match. + // Single string literal or a tuple of string literals. Array literals are AST-hairy and rejected. if (fieldsArg == null) { macro_error(prog, at, "{macroName}: missing fields argument") return false @@ -4525,9 +4704,6 @@ def private emit_create_index(prog : ProgramPtr; var call : ExprCallMacro?; is_u [call_macro(name="create_index")] class private CreateIndexMacro : AstCallMacro { - //! Migration-context typed CREATE INDEX. Form: - //! ``db |> create_index(type, "Field" | ["A", "B"] [, "idx_name"])``. - //! Auto-name follows the [sql_index] convention: ``idx_
__``. def override visit(prog : ProgramPtr; mod : Module?; var call : ExprCallMacro?) : Expression? { return emit_create_index(prog, call, false) } @@ -4535,9 +4711,6 @@ class private CreateIndexMacro : AstCallMacro { [call_macro(name="create_unique_index")] class private CreateUniqueIndexMacro : AstCallMacro { - //! Migration-context typed CREATE UNIQUE INDEX. Same shape as ``create_index`` plus - //! the UNIQUE constraint. SQLite emits a duplicate-violation panic at INSERT time - //! if existing rows already conflict; wrap in a migration body for atomic rollback. def override visit(prog : ProgramPtr; mod : Module?; var call : ExprCallMacro?) : Expression? { return emit_create_index(prog, call, true) } diff --git a/modules/dasSQLITE/daslib/sqlite_linq.das b/modules/dasSQLITE/daslib/sqlite_linq.das index c8a3368c88..4648fafafb 100644 --- a/modules/dasSQLITE/daslib/sqlite_linq.das +++ b/modules/dasSQLITE/daslib/sqlite_linq.das @@ -42,27 +42,14 @@ def _first_opt(arr : array) : Option { } def _first(arr : array) : TT -const -& { - //! Compat-mode fallback: delegates to ``daslib/linq.das`` ``first``, - //! which panics with "sequence contains no elements" on empty and - //! handles non-copyable ``TT`` via ``clone_to_move``. Inside `_sql(...)` - //! the macro intercepts this call before evaluation and emits ` LIMIT 1` - //! with the One materializer. + //! Compat-mode fallback to ``linq.first`` (panics on empty). Inside ``_sql(...)`` + //! the macro intercepts and emits ``LIMIT 1`` with the One materializer. return first(arr) } - // ===== to_sql_literal — runtime stringifier for `_create_view` body inlining ===== -// -// SQLite stores view bodies as text in `sqlite_schema` and rejects `?` placeholders -// inside `CREATE VIEW`, so any value referenced by a view body must be inlined as a -// SQL literal at view-creation time. `_create_view` emits `_::to_sql_literal()` -// per bound expression; `_::` resolves at the user's call site, so a user can extend -// the set with their own one-liner overload (`def to_sql_literal(s : Status) : string => "{int(s)}"`). -// -// Defaults cover all numeric primitives, bool, and string. Enums are handled by -// the `to_sql_literal(auto(TT))` catch-all below (emitted as their underlying -// integer); other types hit the catch-all's `concept_assert` with a "define a -// one-line overload in YourType's module" pointer. +// `_::` resolution lets users extend with a one-line overload in their own module +// (`def to_sql_literal(s : Status) : string => "{int(s)}"`). def to_sql_literal(v : int) : string => "{v}" def to_sql_literal(v : int8) : string => "{v}" @@ -89,7 +76,6 @@ def to_sql_literal(value : auto(TT)) : string { } } - // ===== SqlQuery — accumulator for chain analysis ===== let private INT_MAX_PHASE : int = int(0x7fffffff) @@ -202,9 +188,9 @@ struct private SqlQuery { argSourceIdx : array dbExpr : ExpressionPtr upsertMode : bool - inView : bool //! `_create_view` body context — rejects `[sql_function(directonly=true)]` UDF calls + inView : bool hadError : bool - lastError : string //! First `pred_fail` message, re-emitted by call_macros if their deeper macro_error gets eaten by AST cascade + lastError : string // Multi-Q lowering: phase-order conflict (e.g. `take(n) |> _where(p)`) snapshots // the inner subtree's SQL+binds here; outer FROM renders as `() AS t0`. innerSql : string // null/empty = base-table FROM; else nested SELECT body diff --git a/modules/dasSQLITE/daslib/sqlite_migrate.das b/modules/dasSQLITE/daslib/sqlite_migrate.das index 09d53803b9..8ee87fdb54 100644 --- a/modules/dasSQLITE/daslib/sqlite_migrate.das +++ b/modules/dasSQLITE/daslib/sqlite_migrate.das @@ -20,7 +20,6 @@ require daslib/macro_boost require daslib/templates require daslib/templates_boost - // ===== Public types ===== struct PendingMigration { @@ -40,7 +39,6 @@ struct MigrationRecord { applied_at : int64 } - // ===== Private registry types ===== struct private MigrationEntry { @@ -53,12 +51,10 @@ struct private MigrationEntry { module_name : string } - // ===== Audit table constants ===== let private AUDIT_CREATE_DDL = "CREATE TABLE IF NOT EXISTS __schema_version (version INTEGER PRIMARY KEY, description TEXT NOT NULL DEFAULT '', applied_at INTEGER NOT NULL)" - // ===== Process-global registry ===== // Populated by per-module [init] thunks emitted by the [sql_migration] macro. @@ -66,7 +62,6 @@ let private AUDIT_CREATE_DDL = "CREATE TABLE IF NOT EXISTS __schema_version (ver // by additional module loads. var private _registry : array - def _add_migration_entry(version : int; description : string; vacuum : bool; analyze : bool; body : function<(db : SqlRunner) : void>; func_name : string; module_name : string) : void { @@ -83,7 +78,6 @@ def _add_migration_entry(version : int; description : string; vacuum : bool; ana _registry |> emplace(entry) } - def private _registry_max_version() : int { var m = 0 for (e in _registry) { @@ -94,14 +88,12 @@ def private _registry_max_version() : int { return m } - def private _registry_sorted() : array { var out := _registry sort(out, $(a, b) => a.version < b.version) return <- out } - def private _registry_find_description(version : int) : string { for (e in _registry) { if (e.version == version) { @@ -111,7 +103,6 @@ def private _registry_find_description(version : int) : string { return "" } - def private _verify_no_duplicate_versions() : void { // Belt-and-suspenders runtime defense for dynamic plugin loading past compile-time visibility. var seen : table @@ -124,33 +115,28 @@ def private _verify_no_duplicate_versions() : void { } } - // ===== Internal helpers — audit table I/O and diagnostics ===== let private ADOPTION_HINT = "this DB has existing tables/views/indexes but __schema_version is empty (no migration history yet). If your first migration creates tables that already exist, it will fail. To adopt migrations on an existing DB, call `db |> baseline(version = N)` before `migrate_to_latest()` to mark v1..vN as already applied. See tut 43 § \"Adopting migrations on an existing DB\"." - def private _scalar_int_or_zero(db : SqlRunner; sql : string) : int { // Treats query failure (e.g. "no such table") as 0 — natural default for COUNT/MAX probes. let r = db |> try_query_scalar(sql, type) return (r |> is_ok) ? (r |> unwrap) : 0 } - def private _has_user_objects(db : SqlRunner) : bool { // sqlite_* names are reserved for internals (sqlite_sequence, sqlite_autoindex_*). return _scalar_int_or_zero(db, "SELECT COUNT(*) FROM sqlite_master WHERE name NOT LIKE 'sqlite_%' AND name != '__schema_version'") > 0 } - def private _audit_insert(db : SqlRunner; version : int; description : string) : SqlError { return db |> try_exec( "INSERT INTO __schema_version (version, description, applied_at) VALUES (?, ?, unixepoch())", version, description) } - def private _format_versions_csv(pending : array; applied_count : int) : string { // "v4, v5" — every applied migration (inside the txn) plus the failing one — all rolled back. var parts : array @@ -166,7 +152,6 @@ def private _format_versions_csv(pending : array; applied_count return parts |> join(", ") } - def private _build_enriched_err(failed_version : int; failed_desc : string; underlying : string; rolled_back : string; layer2_hint : bool) : string { var msg = "migration v{failed_version} '{failed_desc}' failed:\n {underlying}\n\nnote: this migrate_to_latest call's transaction was rolled back — {rolled_back} are NOT applied. Fix the migration body and re-run; the corrected migration set will replay on the next call. The DB does not need to be reset." @@ -176,14 +161,12 @@ def private _build_enriched_err(failed_version : int; failed_desc : string; unde return msg } - def private _log_warn_on_some(err : SqlError) : void { if (err |> is_some) { to_log(LOG_WARNING, "{err |> unwrap}\n") } } - // ===== Internal runner — _migrate_inner ===== def private _migrate_inner(db : SqlRunner) : Result { @@ -294,7 +277,6 @@ def private _migrate_inner(db : SqlRunner) : Result { return ok(applied_count, type) } - // ===== Inspection — pure reads, no side effects ===== // Never create the audit table or write a row; absent __schema_version → 0 / all-pending / []. @@ -308,14 +290,12 @@ def private _try_audit_table_exists(db : SqlRunner) : Result { return ok((r |> unwrap) > 0, type) } - def private _audit_table_exists(db : SqlRunner) : bool { // Strict-form callers swallow transient errors (treat as "absent" → return 0/empty). let r <- _try_audit_table_exists(db) return (r |> is_ok) && (r |> unwrap) } - def current_schema_version(db : SqlRunner) : int { //! Highest applied migration version, or 0 if no migrations were applied, the audit table is absent, or a transient SQLite error (e.g. ``SQLITE_BUSY``) blocks the read. //! Use ``try_current_schema_version`` to distinguish "nothing applied" from "couldn't tell". @@ -325,7 +305,6 @@ def current_schema_version(db : SqlRunner) : int { return _scalar_int_or_zero(db, "SELECT COALESCE(MAX(version), 0) FROM __schema_version") } - def try_current_schema_version(db : SqlRunner) : Result { //! ``ok(0)`` when the audit table is absent or empty; ``err`` on transient SQLite errors //! (lock contention etc.) — including failures of the sqlite_master probe. @@ -339,7 +318,6 @@ def try_current_schema_version(db : SqlRunner) : Result { return <- db |> try_query_scalar("SELECT COALESCE(MAX(version), 0) FROM __schema_version", type) } - def try_pending_migrations(db : SqlRunner) : Result, string> { //! Registered migrations whose ``version > current_schema_version(db)``, in ascending order. //! Description sourced from the annotation (current code), not the audit table. @@ -358,7 +336,6 @@ def try_pending_migrations(db : SqlRunner) : Result, str return move_ok(out, type) } - def pending_migrations(db : SqlRunner) : array { //! Strict variant. Panics on transient errors; ``empty`` when no pending migrations. var r <- db |> try_pending_migrations() @@ -368,7 +345,6 @@ def pending_migrations(db : SqlRunner) : array { return <- r._value } - def try_migration_history(db : SqlRunner) : Result, string> { //! Every row in ``__schema_version``, in ascending version order. Description here is the //! frozen-at-apply text from the audit row (NOT the current annotation). ``ok([])`` when the @@ -391,7 +367,6 @@ def try_migration_history(db : SqlRunner) : Result, strin }) } - def migration_history(db : SqlRunner) : array { //! Strict variant of ``try_migration_history``. var r <- db |> try_migration_history() @@ -401,7 +376,6 @@ def migration_history(db : SqlRunner) : array { return <- r._value } - // ===== Public runner surfaces ===== def migrate_to_latest(db : SqlRunner) : int { @@ -414,7 +388,6 @@ def migrate_to_latest(db : SqlRunner) : int { return r |> unwrap } - def try_migrate_to_latest(db : SqlRunner) : Result { //! Non-panic variant of ``migrate_to_latest``. Returns ``ok(count)`` on success or //! ``err(enriched_message)`` for both soft failures and body panics. ROLLBACK is @@ -422,7 +395,6 @@ def try_migrate_to_latest(db : SqlRunner) : Result { return <- _migrate_inner(db) } - // ===== Adoption — baseline ===== def private _audit_has_version(db : SqlRunner; version : int) : bool { @@ -430,7 +402,6 @@ def private _audit_has_version(db : SqlRunner; version : int) : bool { return _scalar_int_or_zero(db, "SELECT COUNT(*) FROM __schema_version WHERE version = {version}") > 0 } - def try_baseline(db : SqlRunner; version : int) : Result { //! Stamps v1..``version`` as already-applied without running their bodies. Idempotent //! (versions already in the audit are skipped). Returns ``ok(stamped_count)`` or ``err``. @@ -485,7 +456,6 @@ def try_baseline(db : SqlRunner; version : int) : Result { return ok(stamped, type) } - def baseline(db : SqlRunner; version : int) : void { //! Strict variant of ``try_baseline``. Panics on error. let r <- db |> try_baseline(version) @@ -494,7 +464,6 @@ def baseline(db : SqlRunner; version : int) : void { } } - // ===== with_latest_sqlite — convenience wrapper ===== def with_latest_sqlite(path : string; blk : block<(db : SqlRunner) : void>) : void { @@ -508,7 +477,6 @@ def with_latest_sqlite(path : string; blk : block<(db : SqlRunner) : void>) : vo } } - // ===== Compile-time migration registry helpers (used by [sql_migration] dup detection) ===== struct private RegisteredMigration { @@ -517,7 +485,6 @@ struct private RegisteredMigration { mod_name : string } - def private _collect_registered_migrations(var out : array) : void { // Single pass over compiling_program() — collects (version, fn, mod) for every [sql_migration]. let prog = compiling_program() |> get_ptr() @@ -544,7 +511,6 @@ def private _collect_registered_migrations(var out : array) }) } - // ===== [sql_migration] annotation ===== [function_macro(name="sql_migration")] @@ -640,3 +606,331 @@ class SqlMigrationMacro : AstFunctionAnnotation { return true } } + +// ===== struct_convert_field — overloaded conversion dispatch ===== +// User extension: declare ``def struct_convert_field(var dst : MyEnum&; src : int) { ... }`` in your module. + +def struct_convert_field(var dst : auto(SCF_T)&; src : SCF_T) : void { + //! Identity — same source and target type. + dst = src +} + +def struct_convert_field(var dst : Option&; src : auto(SCF_S)) : void { + //! ``S → Option`` auto-wrap; inner S→T dispatches recursively so any S→T overload fires. + var inner : SCF_T + _::struct_convert_field(inner, src) + dst = some(inner) +} + +def struct_convert_field(var dst : auto(SCF_T)&; src : Option) : void { + //! ``Option → T``. NULL collapses to ``default``; inner S→T dispatches recursively. + //! Override in your module for different NULL-handling semantics. + _::struct_convert_field(dst, src ?? default) +} + +def struct_convert_field(var dst : Option&; src : Option) : void { + //! ``Option → Option`` cross-payload — unwraps src, dispatches S→T inner, re-wraps. + if (src |> is_some) { + var inner : SCF_T + _::struct_convert_field(inner, src |> unwrap) + dst = some(inner) + } else { + dst = none(type) + } +} + +// Target-typed conversion via the type's constructor. ``int(string)``, ``int(double)``, etc. dispatch +// through daslang's existing primitive constructors. Identity above wins for same-source-target. +def struct_convert_field(var dst : int&; src : auto(SCF_S)) : void { + dst = int(src) +} +def struct_convert_field(var dst : int64&; src : auto(SCF_S)) : void { + dst = int64(src) +} +def struct_convert_field(var dst : float&; src : auto(SCF_S)) : void { + dst = float(src) +} +def struct_convert_field(var dst : double&; src : auto(SCF_S)) : void { + dst = double(src) +} +def struct_convert_field(var dst : string&; src : auto(SCF_S)) : void { + dst = "{src}" +} + +// Disambiguators — primitive target from Option source. Beats both auto(T)←Option and primitive←auto(S) +// at the typer's specificity check, so ``Option → int`` (etc.) routes here cleanly with NULL→default. +def struct_convert_field(var dst : int&; src : Option) : void { + dst = int(src ?? default) +} +def struct_convert_field(var dst : int64&; src : Option) : void { + dst = int64(src ?? default) +} +def struct_convert_field(var dst : float&; src : Option) : void { + dst = float(src ?? default) +} +def struct_convert_field(var dst : double&; src : Option) : void { + dst = double(src ?? default) +} +def struct_convert_field(var dst : string&; src : Option) : void { + dst = "{src ?? default}" +} + + +// ===== [struct_convert] annotation ===== + +class private OverrideCollector : AstVisitor { + // Records `dst.X` ONLY when it appears on the LHS of `=`/`<-`/`:=`. + // Read-only mentions don't count (would silently leave dst.X at default). + target_var_name : string + overrides : table + def OverrideCollector(name : string) { + target_var_name = name + } + def record_lhs(lhs : ExpressionPtr) : void { + if (lhs == null || !(lhs is ExprField)) { + return + } + let f = lhs as ExprField + if (f.value == null || !(f.value is ExprVar)) { + return + } + let v = f.value as ExprVar + if (v.name == target_var_name) { + overrides |> insert(string(f.name), true) + } + } + def override preVisitExprCopy(expr : ExprCopy?) : void { + record_lhs(expr.left) + } + def override preVisitExprMove(expr : ExprMove?) : void { + record_lhs(expr.left) + } + def override preVisitExprClone(expr : ExprClone?) : void { + record_lhs(expr.left) + } +} + +def private collect_overrides(var body : ExpressionPtr; target_var_name : string) : table { + var v = new OverrideCollector(target_var_name) + make_visitor(*v) $(adapter) { + visit(body, adapter) + } + var result <- v.overrides + unsafe { + delete v + } + return <- result +} + +def private make_struct_convert_helper(var src_st : StructurePtr; var tgt_st : StructurePtr; + fn_name : string; var auto_exprs : array; + at : LineInfo) : FunctionPtr { + // ``src`` / ``tgt`` literals — ``new`` is a daslang keyword. + var fn = qmacro_function(fn_name) $(src : $t(src_st); var tgt : $t(tgt_st)) : void { + $b(auto_exprs) + } + fn.flags |= FunctionFlags.generated + fn.flags |= FunctionFlags.privateFunction + fn.body |> force_at(at) + return <- fn +} + +def private make_sct_dispatch(var src_st : StructurePtr; var tgt_st : StructurePtr; + user_fn_name : string; at : LineInfo) : FunctionPtr { + var fn = qmacro_function("_sct") $(src : $t(src_st); var tgt : $t(tgt_st)) : void { + $c(user_fn_name)(src, tgt) + } + fn.flags |= FunctionFlags.generated + fn.body |> force_at(at) + return <- fn +} + +[function_macro(name="struct_convert")] +class SqlStructConvertMacro : AstFunctionAnnotation { + //! Marks ``def fn(old : S; var dst : T) : void`` for auto-derived field-by-field translation. + //! Same-name-same-type / ``T → Option`` / ``INT↔STRING↔FLOAT`` cast / ``@sql_renamed_from`` are + //! filled in; an explicit ``dst.X = …`` (or ``<-``/``:=``) on the LHS suppresses the auto-fill for X. + def override apply(var func : FunctionPtr; var group : ModuleGroup; + args : AnnotationArgumentList; var errors : das_string) : bool { + if (func.fromGeneric != null) { + errors := "[struct_convert]: not supported on generic functions" + return false + } + // Signature: def fn(old : S; var new : T) : void + let nArgs = func.arguments |> length + if (nArgs != 2) { + errors := "[struct_convert] '{func.name}': must take exactly two arguments — `old : S` and `var new : T`. Got {nArgs}." + return false + } + let arg0_type = func.arguments[0]._type + let arg1_type = func.arguments[1]._type + if (arg0_type.structType == null) { + errors := "[struct_convert] '{func.name}': first argument '{func.arguments[0].name}' must be a struct type. Got '{describe(arg0_type)}'." + return false + } + if (arg1_type.structType == null) { + errors := "[struct_convert] '{func.name}': second argument '{func.arguments[1].name}' must be a struct type. Got '{describe(arg1_type)}'." + return false + } + if (arg1_type.flags.constant) { + errors := "[struct_convert] '{func.name}': second argument '{func.arguments[1].name}' must be a `var` parameter so the body can assign to its fields. Write `var {func.arguments[1].name} : {describe(arg1_type)}`." + return false + } + if (func.result.baseType != Type.tVoid && func.result.baseType != Type.autoinfer) { + errors := "[struct_convert] '{func.name}': must return void. Got '{describe(func.result)}'." + return false + } + if (func.body == null || !(func.body is ExprBlock)) { + errors := "[struct_convert] '{func.name}': body must be a brace-block (the macro prepends auto-fill statements). Use `def {func.name}(...) \{ ... \}`, not the `=> expr` short form." + return false + } + // Strip const off the field-accessed StructurePtrs so `$t($t(...))` reification accepts them. + // The parsing pipeline gives us const Structure? from a TypeDeclPtr field walk; the gc_node + // representation is the same pointer either way — reinterpret is safe. + var src_st : StructurePtr = unsafe(reinterpret(arg0_type.structType)) + var tgt_st : StructurePtr = unsafe(reinterpret(arg1_type.structType)) + let new_param_name = string(func.arguments[1].name) + + // Collect the user-overrides set by walking the existing body. + let overrides <- collect_overrides(func.body, new_param_name) + + // Build auto-derived assignments for fields in T order, skipping user-overrides. + // Nested for-loops because handled-type FieldDeclaration doesn't support indexed-let access — + // for (f in fields) is the supported iteration form. + var auto_exprs : array + auto_exprs |> reserve(length(tgt_st.fields)) + var failed = false + for (t_field in tgt_st.fields) { + if (failed) { + break + } + let tname = string(t_field.name) + if (key_exists(overrides, tname)) { + continue + } + // Honor @sql_renamed_from on T's field — read from old's instead of . + let renamed_arg = find_arg(t_field.annotation, "sql_renamed_from") + let renamed_from = renamed_arg is tString ? string(renamed_arg as tString) : "" + let lookup_name = empty(renamed_from) ? tname : renamed_from + let t_type = t_field._type + let t_is_option = is_option_field_type(t_type) + // Find matching field in S by lookup_name; nested loop because indexed access on handled FieldDeclaration arrays needs unsafe. + var s_found = false + for (s_field in src_st.fields) { + if (s_field.name != lookup_name) { + continue + } + s_found = true + // Emit a dispatch call. ``_::`` resolves at the user's module so they can extend the + // overload set with their own (S_type, T_type) pairs (custom enums, value-types, etc.). + auto_exprs |> push(qmacro_expr(${ + _::struct_convert_field(tgt.$f(tname), src.$f(lookup_name)); + })) + break + } + if (failed) { + break + } + if (!s_found) { + // Field absent from S — use T's default initializer or none() for Option. + if (t_field.init != null) { + let init_expr = t_field.init + auto_exprs |> push(qmacro_expr(${ + tgt.$f(tname) = $e(clone_expression(init_expr)); + })) + } elif (t_is_option) { + let t_payload = unwrap_option_payload_type(unsafe(reinterpret(t_type))) + auto_exprs |> push(qmacro_expr(${ + tgt.$f(tname) = none(type<$t(t_payload)>); + })) + } else { + errors := "[struct_convert] '{func.name}': new field '{tname}' has no default initializer and is not Option. Provide a default like `{tname} : {describe(t_type)} = …` on the struct, or write `{new_param_name}.{tname} = …` in the body." + failed = true + } + } + } + if (failed) { + return false + } + + // Emit the per-(S,T) helper carrying the auto-derived assignments. + let helper_name = "_struct_convert_helper`{func.name}" + var helper_fn = make_struct_convert_helper(src_st, tgt_st, helper_name, auto_exprs, func.at) + compiling_module() |> add_function(helper_fn) + + // Prepend a call to the helper at the top of the user's body. Build a fresh ExprBlock + // that contains [helper_call, ...user_body.list] and replace func.body. ExprVar lookups + // by name resolve to the user's actual params at typecheck time. + let call_args <- array( + new ExprVar(at = func.at, name := func.arguments[0].name), + new ExprVar(at = func.at, name := func.arguments[1].name) + ) + var user_body_stmts : array + let user_body_block = func.body as ExprBlock + for (s in user_body_block.list) { + user_body_stmts |> push(clone_expression(s)) + } + var combined <- qmacro_block() { + $c(helper_name)($a(call_args)); + $b(user_body_stmts); + } + func.body = combined + func.body |> force_at(func.at) + + // Emit `_sct(S, T)` dispatch overload. + var sct_fn = make_sct_dispatch(src_st, tgt_st, string(func.name), func.at) + compiling_module() |> add_function(sct_fn) + + return true + } +} + +// ===================================================================== +// `convert` and `convert_and_rename` — db-driving primitives +// ===================================================================== + +[unused_argument(t1, t2), template(t1, t2)] +def convert(db : SqlRunner; t1 : type; t2 : type) : void { + //! Streams every row of S through the [struct_convert] converter into T's own table. + //! For the same-name legacy/current convention, use ``convert_and_rename`` instead. + for (old in db |> select_from(type)) { + var new_row : TT = default + _::_sct(old, new_row) + db |> insert(new_row) + } +} + +[unused_argument(t1, t2), template(t1, t2)] +def convert(db : SqlRunner; t1 : type; t2 : type; name : string) : void { + //! Like ``convert(type, type)`` but inserts rows into table identifier ``name`` instead + //! of T's own ``[sql_table(name=…)]``. Used during ``convert_and_rename`` to land rows in the + //! staging table before the swap. + for (old in db |> select_from(type)) { + var new_row : TT = default + _::_sct(old, new_row) + db |> insert(new_row, name) + } +} + +[unused_argument(t1, t2), template(t1, t2)] +def convert_and_rename(db : SqlRunner; t1 : type; t2 : type) : void { + //! Full schema rebuild: CREATE staging, copy via [struct_convert], DROP original, RENAME. + //! S and T must share the same ``[sql_table(name=…)]`` (legacy/current convention). + let source = _::_sql_table_name(default) + let target = _::_sql_table_name(default) + if (source != target) { + panic("convert_and_rename: source '{source}' and target '{target}' must share the same [sql_table(name=...)]. Use `db |> convert(type, type, name=...)` for cross-table conversions.") + } + let staging = "{target}_new" + // Staging table is created WITHOUT indexes — explicit `[sql_index(name=...)]` would + // collide with the original's still-existing index of the same name (SQLite index + // names are schema-global). Indexes are recreated on the renamed target below. + db |> exec(_::_sql_create_table_sql(default, staging)) + db |> convert(type, type, staging) + db |> exec("DROP TABLE {sql_quote_id(target)}") + db |> exec("ALTER TABLE {sql_quote_id(staging)} RENAME TO {sql_quote_id(target)}") + let idx_sql = _::_sql_create_indexes_sql(default) + if (!empty(idx_sql)) { + db |> exec(idx_sql) + } +} diff --git a/tests/dasSQLITE/failed_sql_index_on_legacy.das b/tests/dasSQLITE/failed_sql_index_on_legacy.das new file mode 100644 index 0000000000..527fcf288c --- /dev/null +++ b/tests/dasSQLITE/failed_sql_index_on_legacy.das @@ -0,0 +1,15 @@ +options gen2 + +// `[sql_index]` on a `legacy=true` struct is rejected: legacy structs are read-only +// and don't define schema, so the index would have no CREATE TABLE to attach to and +// would collide with the current struct's index of the same name on rebuild. +expect 30111:1 + +require sqlite/sqlite_migrate + +[sql_table(name = "users", legacy = true), + sql_index(name = "ux_users_name", fields = "Name", unique)] +struct UserV1 { + @sql_primary_key Id : int + Name : string +} diff --git a/tests/dasSQLITE/failed_struct_convert_new_field_no_default.das b/tests/dasSQLITE/failed_struct_convert_new_field_no_default.das new file mode 100644 index 0000000000..6d155eba76 --- /dev/null +++ b/tests/dasSQLITE/failed_struct_convert_new_field_no_default.das @@ -0,0 +1,25 @@ +options gen2 + +// `[struct_convert]` rejects new T fields that are not Option and have no default initializer +// when the user provides no override — there's no auto-derivation rule that can populate them. +expect 30111:1 + +require sqlite/sqlite_migrate + +[sql_table(name = "users", legacy = true)] +struct UserV1 { + @sql_primary_key Id : int + Name : string +} + +[sql_table(name = "users")] +struct User { + @sql_primary_key Id : int + Name : string + Email : string // new field, not Option, no default — needs an override +} + +[struct_convert] +def my_v1_to_v2(old : UserV1; var dst : User) { + pass +} diff --git a/tests/dasSQLITE/migrate_80_struct_convert_trivial.das b/tests/dasSQLITE/migrate_80_struct_convert_trivial.das new file mode 100644 index 0000000000..1afbf8fc51 --- /dev/null +++ b/tests/dasSQLITE/migrate_80_struct_convert_trivial.das @@ -0,0 +1,37 @@ +options gen2 + +require dastest/testing_boost public +require sqlite/sqlite_migrate + +[sql_table(name = "users", legacy = true)] +struct UserV1 { + @sql_primary_key Id : int + Name : string + Score : int +} + +[sql_table(name = "users")] +struct User { + @sql_primary_key Id : int + Name : string + Score : int +} + +[struct_convert] +def my_v1_to_v2(old : UserV1; var dst : User) { + pass +} + +[test] +def test_trivial_round_trip(t : T?) { + var inscope db = open_sqlite(":memory:") + db |> exec("CREATE TABLE users (Id INTEGER PRIMARY KEY, Name TEXT NOT NULL, Score INTEGER NOT NULL DEFAULT 0)") + db |> exec("INSERT INTO users (Id, Name, Score) VALUES (1, 'alice', 42), (2, 'bob', 7)") + db |> convert_and_rename(type, type) + let n = db |> query_scalar("SELECT COUNT(*) FROM users", type) + t |> equal(n, 2, "row count preserved") + let alice_score = db |> query_scalar("SELECT Score FROM users WHERE Name='alice'", type) + t |> equal(alice_score, 42, "alice's Score copied verbatim") + let bob_score = db |> query_scalar("SELECT Score FROM users WHERE Name='bob'", type) + t |> equal(bob_score, 7, "bob's Score copied verbatim") +} diff --git a/tests/dasSQLITE/migrate_81_struct_convert_option_flip.das b/tests/dasSQLITE/migrate_81_struct_convert_option_flip.das new file mode 100644 index 0000000000..ce30000cac --- /dev/null +++ b/tests/dasSQLITE/migrate_81_struct_convert_option_flip.das @@ -0,0 +1,42 @@ +options gen2 + +require dastest/testing_boost public +require sqlite/sqlite_migrate +require daslib/option public + +// T → Option: source has bare Score, target has Option. Auto-rule wraps with some(). + +[sql_table(name = "users", legacy = true)] +struct UserV1 { + @sql_primary_key Id : int + Name : string + Score : int +} + +[sql_table(name = "users")] +struct User { + @sql_primary_key Id : int + Name : string + Score : Option +} + +[struct_convert] +def my_v1_to_v2(old : UserV1; var dst : User) { + pass +} + +[test] +def test_t_to_option_wraps_with_some(t : T?) { + var inscope db = open_sqlite(":memory:") + db |> exec("CREATE TABLE users (Id INTEGER PRIMARY KEY, Name TEXT NOT NULL, Score INTEGER NOT NULL DEFAULT 0)") + db |> exec("INSERT INTO users (Id, Name, Score) VALUES (1, 'alice', 42)") + db |> convert_and_rename(type, type) + var rows : array + rows |> reserve(2) + for (row in db |> select_from(type)) { + rows |> emplace(row) + } + t |> equal(length(rows), 1, "row preserved") + t |> success(rows[0].Score |> is_some, "Option wraps to some(...)") + t |> equal(rows[0].Score |> unwrap, 42, "wrapped value preserved") +} diff --git a/tests/dasSQLITE/migrate_82_struct_convert_primitive_cast.das b/tests/dasSQLITE/migrate_82_struct_convert_primitive_cast.das new file mode 100644 index 0000000000..d6fea5c522 --- /dev/null +++ b/tests/dasSQLITE/migrate_82_struct_convert_primitive_cast.das @@ -0,0 +1,52 @@ +options gen2 + +require dastest/testing_boost public +require sqlite/sqlite_migrate + +// [struct_convert] dispatches to overloaded ``struct_convert_field``. This test exercises the +// numeric/string conversion overloads — int↔int64, float↔double, string→numeric, numeric→string. + +[sql_table(name = "items", legacy = true)] +struct ItemV1 { + @sql_primary_key Id : int + Count : int // → int64 (widening) + Score : int64 // → int (narrowing — silent, user opted in) + Ratio : float // → double (widening) + Avg : double // → float (narrowing) + LabelInt : int // → string (numeric → string) + PriceStr : string // → double (string → double parse) +} + +[sql_table(name = "items")] +struct Item { + @sql_primary_key Id : int + Count : int64 + Score : int + Ratio : double + Avg : float + LabelInt : string + PriceStr : double +} + +[struct_convert] +def convert_item_v1_to_v2(old : ItemV1; var dst : Item) {} + +[test] +def test_primitive_casts_through_dispatch(t : T?) { + var inscope db = open_sqlite(":memory:") + db |> exec("CREATE TABLE items (Id INTEGER PRIMARY KEY, Count INTEGER NOT NULL, Score INTEGER NOT NULL, Ratio REAL NOT NULL, Avg REAL NOT NULL, LabelInt INTEGER NOT NULL, PriceStr TEXT NOT NULL)") + db |> exec("INSERT INTO items (Id, Count, Score, Ratio, Avg, LabelInt, PriceStr) VALUES (1, 42, 9999, 1.5, 2.75, 7, '3.14')") + db |> convert_and_rename(type, type) + let count_int64 = db |> query_scalar("SELECT Count FROM items WHERE Id=1", type) + t |> equal(count_int64, 42l, "int → int64 widening preserved") + let score_int = db |> query_scalar("SELECT Score FROM items WHERE Id=1", type) + t |> equal(score_int, 9999, "int64 → int narrowing preserved (within range)") + let ratio_double = db |> query_scalar("SELECT Ratio FROM items WHERE Id=1", type) + t |> equal(ratio_double, 1.5lf, "float → double widening preserved") + let avg_float = db |> query_scalar("SELECT Avg FROM items WHERE Id=1", type) + t |> equal(avg_float, 2.75f, "double → float narrowing preserved") + let label_str = db |> query_scalar("SELECT LabelInt FROM items WHERE Id=1", type) + t |> equal(label_str, "7", "int → string interp") + let price_double = db |> query_scalar("SELECT PriceStr FROM items WHERE Id=1", type) + t |> equal(price_double, 3.14lf, "string → double parse") +} diff --git a/tests/dasSQLITE/migrate_83_struct_convert_option_unwrap.das b/tests/dasSQLITE/migrate_83_struct_convert_option_unwrap.das new file mode 100644 index 0000000000..c5953071bb --- /dev/null +++ b/tests/dasSQLITE/migrate_83_struct_convert_option_unwrap.das @@ -0,0 +1,42 @@ +options gen2 + +require dastest/testing_boost public +require sqlite/sqlite_migrate + +// `Option → T` dispatches through the shipped ``struct_convert_field`` overload — +// NULL collapses to ``default`` (0 / "" / 0.0). Override in your module for different +// NULL-handling semantics. + +[sql_table(name = "users", legacy = true)] +struct UserV1 { + @sql_primary_key Id : int + Name : Option + Score : Option +} + +[sql_table(name = "users")] +struct User { + @sql_primary_key Id : int + Name : string + Score : int +} + +[struct_convert] +def m_v1_to_v2(old : UserV1; var dst : User) {} + +[test] +def test_option_unwrap_default_on_null(t : T?) { + var inscope db = open_sqlite(":memory:") + db |> exec("CREATE TABLE users (Id INTEGER PRIMARY KEY, Name TEXT, Score INTEGER)") + db |> exec("INSERT INTO users (Id, Name, Score) VALUES (1, 'alice', 42)") + db |> exec("INSERT INTO users (Id, Name, Score) VALUES (2, NULL, NULL)") + db |> convert_and_rename(type, type) + let alice_name = db |> query_scalar("SELECT Name FROM users WHERE Id=1", type) + let alice_score = db |> query_scalar("SELECT Score FROM users WHERE Id=1", type) + t |> equal(alice_name, "alice", "Some(string) → string preserved") + t |> equal(alice_score, 42, "Some(int) → int preserved") + let bob_name = db |> query_scalar("SELECT Name FROM users WHERE Id=2", type) + let bob_score = db |> query_scalar("SELECT Score FROM users WHERE Id=2", type) + t |> equal(bob_name, "", "NULL string → default=\"\"") + t |> equal(bob_score, 0, "NULL int → default=0") +} diff --git a/tests/dasSQLITE/migrate_84_struct_convert_option_cross.das b/tests/dasSQLITE/migrate_84_struct_convert_option_cross.das new file mode 100644 index 0000000000..501fd2d6e2 --- /dev/null +++ b/tests/dasSQLITE/migrate_84_struct_convert_option_cross.das @@ -0,0 +1,38 @@ +options gen2 + +require dastest/testing_boost public +require sqlite/sqlite_migrate + +// ``Option → Option`` cross-payload — the macro emits ``struct_convert_field(dst, src)`` +// and the cross-payload Option overload unwraps src, dispatches S→T inner, re-wraps. + +[sql_table(name = "users", legacy = true)] +struct UserV1 { + @sql_primary_key Id : int + Score : Option +} + +[sql_table(name = "users")] +struct User { + @sql_primary_key Id : int + Score : Option +} + +[struct_convert] +def m_v1_to_v2(old : UserV1; var dst : User) {} + +[test] +def test_option_cross_payload(t : T?) { + var inscope db = open_sqlite(":memory:") + db |> exec("CREATE TABLE users (Id INTEGER PRIMARY KEY, Score INTEGER)") + db |> exec("INSERT INTO users (Id, Score) VALUES (1, 42), (2, NULL)") + db |> convert_and_rename(type, type) + let alice = db |> query_scalar("SELECT Score FROM users WHERE Id=1", type) + t |> equal(alice, "42", "Option(some) → Option(some) via int→string inner") + // SQLite returns "" for NULL when target is non-nullable string column; with Option + // target the rebuild stores actual NULL, but query_scalar collapses to default. + let n_null = db |> query_scalar( + "SELECT COUNT(*) FROM users WHERE Id=2 AND Score IS NULL", + type) + t |> equal(n_null, 1, "Option(none) → Option(none) preserved as NULL") +} diff --git a/tests/dasSQLITE/migrate_85_struct_convert_user_overload.das b/tests/dasSQLITE/migrate_85_struct_convert_user_overload.das new file mode 100644 index 0000000000..3e68455779 --- /dev/null +++ b/tests/dasSQLITE/migrate_85_struct_convert_user_overload.das @@ -0,0 +1,40 @@ +options gen2 + +require dastest/testing_boost public +require sqlite/sqlite_migrate + +// User-side overload extension: declare ``struct_convert_field`` for ``int → string`` in +// your module and the [struct_convert] macro picks it up automatically — your overload +// shadows the shipped ``string ← auto(S)`` catch-all because it's more specific. + +[sql_table(name = "items", legacy = true)] +struct ItemV1 { + @sql_primary_key Id : int + Score : int +} + +[sql_table(name = "items")] +struct Item { + @sql_primary_key Id : int + Score : string +} + +// User-side dispatch overload — formats the int with a domain-specific prefix instead of +// the default ``"{src}"`` interp. The macro's ``_::`` resolves at user's module, so this +// overload wins for the int→string field of [struct_convert]. +def struct_convert_field(var dst : string&; src : int) : void { + dst = "score={src}" +} + +[struct_convert] +def m_v1_to_v2(old : ItemV1; var dst : Item) {} + +[test] +def test_user_overload_extends_dispatch(t : T?) { + var inscope db = open_sqlite(":memory:") + db |> exec("CREATE TABLE items (Id INTEGER PRIMARY KEY, Score INTEGER NOT NULL)") + db |> exec("INSERT INTO items (Id, Score) VALUES (1, 42)") + db |> convert_and_rename(type, type) + let landed = db |> query_scalar("SELECT Score FROM items WHERE Id=1", type) + t |> equal(landed, "score=42", "user-defined overload wins over shipped string<-auto(S) catch-all") +} diff --git a/tests/dasSQLITE/migrate_86_struct_convert_readonly_mention_preserves.das b/tests/dasSQLITE/migrate_86_struct_convert_readonly_mention_preserves.das new file mode 100644 index 0000000000..efd4acb66b --- /dev/null +++ b/tests/dasSQLITE/migrate_86_struct_convert_readonly_mention_preserves.das @@ -0,0 +1,45 @@ +options gen2 + +require dastest/testing_boost public +require sqlite/sqlite_migrate + +// Read-only mentions of `dst.X` (e.g. `if (dst.X == "")`) must NOT suppress the +// auto-derived `dst.X = old.X`. A previous broader visitor catching ANY ExprField +// under `dst` would silently leave dst.X at default — copying produced empty rows +// instead of the source data. + +[sql_table(name = "users", legacy = true)] +struct OldUser { + @sql_primary_key Id : int + Name : string + Email : string +} + +[sql_table(name = "users")] +struct NewUser { + @sql_primary_key Id : int + Name : string + Email : string +} + +[struct_convert] +def convert_readonly_mention(old : OldUser; var dst : NewUser) { + // Read-only mention of dst.Name — auto-fill MUST still emit `dst.Name = old.Name`. + if (dst.Name == "") { + // Path is unreachable in practice; the conditional exists to anchor the read mention. + } + // Read-only mention of dst.Email used as RHS — same: auto-fill MUST still copy. + let _peek = dst.Email +} + +[test] +def test_readonly_mentions_preserve_auto_fill(t : T?) { + var inscope db = open_sqlite(":memory:") + db |> exec("CREATE TABLE users (Id INTEGER PRIMARY KEY, Name TEXT NOT NULL, Email TEXT NOT NULL)") + db |> exec("INSERT INTO users (Id, Name, Email) VALUES (1, 'alice', 'alice@x')") + db |> convert_and_rename(type, type) + let landed_name = db |> query_scalar("SELECT Name FROM users WHERE Id=1", type) + let landed_email = db |> query_scalar("SELECT Email FROM users WHERE Id=1", type) + t |> equal(landed_name, "alice", "Name preserved (read-only mention did NOT suppress auto-copy)") + t |> equal(landed_email, "alice@x", "Email preserved (read-only RHS mention did NOT suppress auto-copy)") +} diff --git a/tests/dasSQLITE/migrate_87_struct_convert_override_via_mention.das b/tests/dasSQLITE/migrate_87_struct_convert_override_via_mention.das new file mode 100644 index 0000000000..02b0261e0a --- /dev/null +++ b/tests/dasSQLITE/migrate_87_struct_convert_override_via_mention.das @@ -0,0 +1,42 @@ +options gen2 + +require dastest/testing_boost public +require sqlite/sqlite_migrate + +// Override-detection records `dst.X` ONLY when it appears on the LHS of an +// assignment (`=`, `<-`, `:=`). The visitor sees `dst.Score = …` and suppresses +// the auto-derive that would otherwise macro_error on `Option → int` +// (existing rows may be NULL). + +[sql_table(name = "users", legacy = true)] +struct UserV1 { + @sql_primary_key Id : int + Name : string + Score : Option +} + +[sql_table(name = "users")] +struct User { + @sql_primary_key Id : int + Name : string + Score : int +} + +[struct_convert] +def my_v1_to_v2(old : UserV1; var dst : User) { + // LHS write recorded; auto-derive suppressed; user expression resolves the Option. + dst.Score = old.Score ?? 0 +} + +[test] +def test_lhs_write_suppresses_auto_derive(t : T?) { + // The compile passing IS the test — without LHS-detection [struct_convert] + // would macro_error on Option → T. With it, the user's body resolves the + // Option and the macro stays out of the way. + var inscope db = open_sqlite(":memory:") + db |> exec("CREATE TABLE users (Id INTEGER PRIMARY KEY, Name TEXT NOT NULL, Score INTEGER)") + db |> exec("INSERT INTO users (Id, Name, Score) VALUES (1, 'alice', 42)") + db |> convert_and_rename(type, type) + let alice_score = db |> query_scalar("SELECT Score FROM users WHERE Name='alice'", type) + t |> equal(alice_score, 42, "alice's some(42) unwrapped via the body's resolution") +} diff --git a/tests/dasSQLITE/migrate_88_create_table_name_override.das b/tests/dasSQLITE/migrate_88_create_table_name_override.das new file mode 100644 index 0000000000..faf2b86951 --- /dev/null +++ b/tests/dasSQLITE/migrate_88_create_table_name_override.das @@ -0,0 +1,45 @@ +options gen2 + +require dastest/testing_boost public +require sqlite/sqlite_migrate + +[sql_table(name = "users"), sql_index(fields = "Name")] +struct User { + @sql_primary_key Id : int + Name : string +} + +[test] +def test_create_table_emits_with_alternate_name(t : T?) { + var inscope db = open_sqlite(":memory:") + db |> create_table(type, "users_new") + let count = db |> query_scalar( + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='users_new'", type) + t |> equal(count, 1, "users_new table created via name override") + let original_count = db |> query_scalar( + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='users'", type) + t |> equal(original_count, 0, "original 'users' table not auto-created") +} + +[test] +def test_create_table_emits_full_schema(t : T?) { + var inscope db = open_sqlite(":memory:") + db |> create_table(type, "users_new") + let pk = db |> query_scalar( + "SELECT pk FROM pragma_table_info('users_new') WHERE name='Id'", type) + t |> equal(pk, 1, "Id column is PRIMARY KEY in users_new") + let notnull = db |> query_scalar( + "SELECT \"notnull\" FROM pragma_table_info('users_new') WHERE name='Name'", type) + t |> equal(notnull, 1, "Name column is NOT NULL") +} + +[test] +def test_create_table_emits_indexes_with_runtime_name(t : T?) { + // [sql_index] auto-name uses the runtime table name, so indexes for the staging table + // are named `idx_users_new_` — no collision with original table's `idx_users_`. + var inscope db = open_sqlite(":memory:") + db |> create_table(type, "users_new") + let idx = db |> query_scalar( + "SELECT COUNT(*) FROM sqlite_master WHERE type='index' AND tbl_name='users_new' AND name='idx_users_new_Name'", type) + t |> equal(idx, 1, "auto-named index targets the staging table") +} diff --git a/tests/dasSQLITE/migrate_89_insert_name_override.das b/tests/dasSQLITE/migrate_89_insert_name_override.das new file mode 100644 index 0000000000..c8350f928f --- /dev/null +++ b/tests/dasSQLITE/migrate_89_insert_name_override.das @@ -0,0 +1,25 @@ +options gen2 + +require dastest/testing_boost public +require sqlite/sqlite_migrate + +[sql_table(name = "users")] +struct User { + @sql_primary_key Id : int + Name : string + Email : string +} + +[test] +def test_insert_lands_in_named_table(t : T?) { + var inscope db = open_sqlite(":memory:") + db |> create_table(type, "users_new") + let u = User(Name = "alice", Email = "alice@x") + let id = db |> insert(u, "users_new") + t |> success(id > 0l, "insert returned a row id") + let n = db |> query_scalar("SELECT COUNT(*) FROM users_new", type) + t |> equal(n, 1, "row landed in users_new, not users") + let landed_email = db |> query_scalar( + "SELECT Email FROM users_new WHERE Name='alice'", type) + t |> equal(landed_email, "alice@x", "row content preserved") +} diff --git a/tests/dasSQLITE/migrate_90_sql_table_legacy_marker.das b/tests/dasSQLITE/migrate_90_sql_table_legacy_marker.das new file mode 100644 index 0000000000..0009723c70 --- /dev/null +++ b/tests/dasSQLITE/migrate_90_sql_table_legacy_marker.das @@ -0,0 +1,44 @@ +options gen2 + +require dastest/testing_boost public +require sqlite/sqlite_migrate + +[sql_table(name = "users", legacy = true)] +struct UserV1 { + @sql_primary_key Id : int + Name : string + LegacyEmail : string +} + +[sql_table(name = "users")] +struct User { + @sql_primary_key Id : int + Name : string + Email : string +} + +[test] +def test_legacy_struct_select_from_works(t : T?) { + var inscope db = open_sqlite(":memory:") + db |> exec("CREATE TABLE users (Id INTEGER PRIMARY KEY, Name TEXT NOT NULL, LegacyEmail TEXT NOT NULL DEFAULT '')") + db |> exec("INSERT INTO users (Id, Name, LegacyEmail) VALUES (1, 'alice', 'alice@x')") + var rows : array + rows |> reserve(2) + for (row in db |> select_from(type)) { + rows |> emplace(row) + } + t |> equal(length(rows), 1, "select_from on legacy struct returns rows") + t |> equal(rows[0].LegacyEmail, "alice@x", "legacy field readable") +} + +[test] +def test_legacy_struct_drop_works(t : T?) { + // DROP TABLE IF EXISTS is read-side-enough — devs may legitimately drop a legacy table + // during migration cleanup. Tests that the helper is emitted for legacy structs. + var inscope db = open_sqlite(":memory:") + db |> exec("CREATE TABLE users (Id INTEGER PRIMARY KEY, Name TEXT NOT NULL, LegacyEmail TEXT NOT NULL DEFAULT '')") + db |> drop_table_if_exists(type) + let n = db |> query_scalar( + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='users'", type) + t |> equal(n, 0, "drop_table_if_exists works on legacy struct") +} diff --git a/tests/dasSQLITE/migrate_91_full_rebuild_end_to_end.das b/tests/dasSQLITE/migrate_91_full_rebuild_end_to_end.das new file mode 100644 index 0000000000..e195cbc738 --- /dev/null +++ b/tests/dasSQLITE/migrate_91_full_rebuild_end_to_end.das @@ -0,0 +1,68 @@ +options gen2 + +require dastest/testing_boost public +require sqlite/sqlite_migrate + +// The flagship rebuild example: legacy + current pair, [struct_convert] with one override line, +// migration body collapses to a single `convert_and_rename` call. Verifies that the entire +// rebuild dance (CREATE staging + copy + DROP + RENAME) preserves rows with field translation. + +[sql_table(name = "users", legacy = true)] +struct UserV6 { + @sql_primary_key Id : int + Name : string + LegacyEmail : string +} + +[sql_table(name = "users")] +struct User { + @sql_primary_key Id : int + Name : string + Email : string +} + +[struct_convert] +def my_v6_to_v7(old : UserV6; var dst : User) { + dst.Email = (old.LegacyEmail != "") ? old.LegacyEmail : "unknown@example.com" +} + +[sql_migration(version = 1, description = "create v6 schema with fixture rows")] +def m_001(db : SqlRunner) { + db |> exec("CREATE TABLE users (Id INTEGER PRIMARY KEY, Name TEXT NOT NULL, LegacyEmail TEXT NOT NULL DEFAULT '')") + db |> exec("INSERT INTO users (Id, Name, LegacyEmail) VALUES (1, 'alice', 'alice@x'), (2, 'bob', '')") +} + +[sql_migration(version = 2, description = "rebuild users with new shape")] +def m_002(db : SqlRunner) { + db |> convert_and_rename(type, type) +} + +[test] +def test_full_rebuild_preserves_rows(t : T?) { + var inscope db = open_sqlite(":memory:") + db |> migrate_to_latest() + let n = db |> query_scalar("SELECT COUNT(*) FROM users", type) + t |> equal(n, 2, "row count preserved across rebuild") +} + +[test] +def test_full_rebuild_translates_fields(t : T?) { + var inscope db = open_sqlite(":memory:") + db |> migrate_to_latest() + let alice_email = db |> query_scalar( + "SELECT Email FROM users WHERE Name='alice'", type) + t |> equal(alice_email, "alice@x", "alice's LegacyEmail mapped to Email") + let bob_email = db |> query_scalar( + "SELECT Email FROM users WHERE Name='bob'", type) + t |> equal(bob_email, "unknown@example.com", "bob's empty LegacyEmail picked the fallback") +} + +[test] +def test_rebuild_inside_migration_txn(t : T?) { + // The whole migrate_to_latest call runs inside a single BEGIN IMMEDIATE / COMMIT. + // Test by introspecting the audit table. + var inscope db = open_sqlite(":memory:") + db |> migrate_to_latest() + let v = db |> current_schema_version() + t |> equal(v, 2, "audit recorded both migrations") +} diff --git a/tests/dasSQLITE/migrate_92_hostile_name_safe.das b/tests/dasSQLITE/migrate_92_hostile_name_safe.das new file mode 100644 index 0000000000..8c5e2b903a --- /dev/null +++ b/tests/dasSQLITE/migrate_92_hostile_name_safe.das @@ -0,0 +1,34 @@ +options gen2 + +require dastest/testing_boost public +require sqlite/sqlite_migrate + +// Identifier injection guard: every runtime ``name : string`` overload +// (create_table / insert / select_from / drop / update / delete + the +// migrate path) must route through sql_quote_id so embedded `"` is +// doubled. Without escaping, a `"` in name would close the identifier +// and execute attacker-controlled SQL through sqlite3_exec. + +[sql_table(name = "users")] +struct User { + @sql_primary_key Id : int + Name : string +} + +[test] +def test_hostile_name_does_not_inject(t : T?) { + var inscope db = open_sqlite(":memory:") + let hostile = "evil\" (Injected INTEGER); CREATE TABLE hacked(z TEXT); --" + db |> create_table(type, hostile) + let landed = User(Name = "alice") + let id = db |> insert(landed, hostile) + t |> success(id > 0l, "insert into hostile-named table succeeded") + let n_landed = db |> query_scalar( + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name={sql_quote_lit(hostile)}", + type) + t |> equal(n_landed, 1, "hostile name landed verbatim as a table identifier") + let n_hacked = db |> query_scalar( + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='hacked'", + type) + t |> equal(n_hacked, 0, "no injected `hacked` table was created") +} diff --git a/tests/dasSQLITE/migrate_93_rebuild_attached_schema.das b/tests/dasSQLITE/migrate_93_rebuild_attached_schema.das new file mode 100644 index 0000000000..52e73a6091 --- /dev/null +++ b/tests/dasSQLITE/migrate_93_rebuild_attached_schema.das @@ -0,0 +1,49 @@ +options gen2 + +require dastest/testing_boost public +require sqlite/sqlite_migrate + +// _qualify_schema must rewrite ALTER TABLE so that convert_and_rename +// rebuilds run inside an attached schema, not silently against `main`. + +[sql_table(name = "users", legacy = true)] +struct UserV1 { + @sql_primary_key Id : int + Name : string + LegacyEmail : string +} + +[sql_table(name = "users")] +struct User { + @sql_primary_key Id : int + Name : string + Email : string +} + +[struct_convert] +def convert_v1_to_v2(old : UserV1; var dst : User) { + dst.Email = (old.LegacyEmail != "") ? old.LegacyEmail : "unknown@example.com" +} + +[test] +def test_rebuild_runs_in_attached_schema(t : T?) { + var inscope main = open_sqlite(":memory:") + main |> with_attached("file:rebuild_attached_test?mode=memory&cache=shared", "att") $(att) { + att |> exec("DROP TABLE IF EXISTS \"users\"") + att |> exec("DROP TABLE IF EXISTS \"users_new\"") + att |> exec("CREATE TABLE \"users\" (Id INTEGER PRIMARY KEY, Name TEXT NOT NULL, LegacyEmail TEXT NOT NULL DEFAULT '')") + att |> exec("INSERT INTO \"users\" (Id, Name, LegacyEmail) VALUES (1, 'alice', 'alice@x')") + att |> convert_and_rename(type, type) + let n_in_att = main |> query_scalar( + "SELECT COUNT(*) FROM \"att\".sqlite_master WHERE type='table' AND name='users'", + type) + t |> equal(n_in_att, 1, "rebuilt `users` lives in attached schema") + let main_users = main |> query_scalar( + "SELECT COUNT(*) FROM main.sqlite_master WHERE type='table' AND name='users'", + type) + t |> equal(main_users, 0, "no rebuild side-effect in main") + let landed = att |> query_scalar( + "SELECT Email FROM users WHERE Id=1", type) + t |> equal(landed, "alice@x", "row content preserved through rebuild") + } +} diff --git a/tests/dasSQLITE/migrate_94_rebuild_custom_named_index.das b/tests/dasSQLITE/migrate_94_rebuild_custom_named_index.das new file mode 100644 index 0000000000..6787d2d014 --- /dev/null +++ b/tests/dasSQLITE/migrate_94_rebuild_custom_named_index.das @@ -0,0 +1,46 @@ +options gen2 + +require dastest/testing_boost public +require sqlite/sqlite_migrate + +// `[sql_index(name = "...")]` (explicit custom name) must survive convert_and_rename. +// Earlier rebuild path created the staging table WITH indexes, which collided on the +// custom name — SQLite index names are schema-global and the original still owned it. +// Fixed by deferring index creation to after the RENAME. + +[sql_table(name = "users", legacy = true)] +struct UserV1 { + @sql_primary_key Id : int + Name : string + Email : string +} + +[sql_table(name = "users"), + sql_index(name = "ux_users_name", fields = "Name", unique)] +struct User { + @sql_primary_key Id : int + Name : string + Email : string +} + +[struct_convert] +def convert_v1_to_v2(old : UserV1; var dst : User) {} + +[test] +def test_custom_named_index_survives_rebuild(t : T?) { + var inscope db = open_sqlite(":memory:") + db |> exec("CREATE TABLE users (Id INTEGER PRIMARY KEY, Name TEXT NOT NULL, Email TEXT NOT NULL DEFAULT '')") + db |> exec("CREATE UNIQUE INDEX ux_users_name ON users (Name)") + db |> exec("INSERT INTO users (Id, Name, Email) VALUES (1, 'alice', 'alice@x')") + db |> convert_and_rename(type, type) + let n_idx = db |> query_scalar( + "SELECT COUNT(*) FROM sqlite_master WHERE type='index' AND name='ux_users_name'", + type) + t |> equal(n_idx, 1, "custom-named unique index lives after rebuild") + let n_tab = db |> query_scalar( + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='users'", + type) + t |> equal(n_tab, 1, "renamed `users` table is the only one (no `users_new` left)") + let landed = db |> query_scalar("SELECT Name FROM users WHERE Id=1", type) + t |> equal(landed, "alice", "row preserved through rebuild") +} diff --git a/tests/dasSQLITE/migrate_95_rebuild_mismatched_table_names_panics.das b/tests/dasSQLITE/migrate_95_rebuild_mismatched_table_names_panics.das new file mode 100644 index 0000000000..42aa146489 --- /dev/null +++ b/tests/dasSQLITE/migrate_95_rebuild_mismatched_table_names_panics.das @@ -0,0 +1,39 @@ +options gen2 + +require dastest/testing_boost public +require sqlite/sqlite_migrate + +// `convert_and_rename` requires source and target structs to share the same +// ``[sql_table(name=...)]`` (legacy/current convention). Mismatched names would +// silently SELECT from the source table but DROP/RENAME the target — a footgun. +// The runtime guard panics with a clear message pointing at the proper escape +// hatch (``convert(type, type, name=...)``) for cross-table conversions. + +[sql_table(name = "items_old", legacy = true)] +struct ItemV1 { + @sql_primary_key Id : int + Name : string +} + +[sql_table(name = "items")] +struct Item { + @sql_primary_key Id : int + Name : string +} + +[struct_convert] +def m_v1_to_v2(old : ItemV1; var dst : Item) {} + +[test] +def test_convert_and_rename_panics_on_mismatched_table_names(t : T?) { + var inscope db = open_sqlite(":memory:") + db |> exec("CREATE TABLE items_old (Id INTEGER PRIMARY KEY, Name TEXT NOT NULL)") + var panicked = false + try { + db |> convert_and_rename(type, type) + } recover { + panicked = true + } + t |> success(panicked, + "convert_and_rename must panic when source/target [sql_table(name=...)] disagree") +} diff --git a/tutorials/sql/43-migrations.das b/tutorials/sql/43-migrations.das index 03ea9c77e3..bb4d3d1c24 100644 --- a/tutorials/sql/43-migrations.das +++ b/tutorials/sql/43-migrations.das @@ -30,7 +30,8 @@ require sqlite/sqlite_migrate // PART 1 — Define a schema and write a few migrations // ===================================================================== -// Current shape. Migrations 1-3 below describe the path from empty to here. +// Current shape. Migrations 1-3 below describe the path from empty to here; +// migration 6 (PART 5) shows the rebuild path that adds LastLoginAt. [sql_table(name = "users")] struct User { @@ -38,6 +39,27 @@ struct User { Name : string Email : string // added in migration 2 Score : int = 0 // added in migration 3 with default + LastLoginAt : Option // added in migration 6 via convert_and_rename +} + +// Pre-rebuild shape, kept around as a [sql_table(legacy=true)] historical +// reference. Read-only — usable with select_from inside the rebuild migration, +// but the compiler refuses create_table / insert / update / delete on it. +[sql_table(name = "users", legacy = true)] +struct UserV5 { + @sql_primary_key Id : int + Name : string + Email : string + Score : int = 0 +} + +// [struct_convert] walks T's fields and emits `_::struct_convert_field(tgt.X, src.X)` +// per field. Identity / Option-wrap / numeric / string conversions ship out of the box; +// drop your own overload in your module to extend (e.g. `int → MyEnum`). +// Body is empty because every field auto-derives — no per-field overrides needed. +[struct_convert] +def m_v5_to_v6(old : UserV5; var dst : User) { + pass } // Migration 1 — bootstrap. @@ -111,6 +133,28 @@ def migration_005(db : SqlRunner) { db |> create_unique_index(type, ("Email", "Name"), "ux_email_name") } +// Migration 6 — schema REBUILD via convert_and_rename. +// +// Adds LastLoginAt to User. Strictly speaking, `add_column(type, +// "LastLoginAt")` would also work for this case (SQLite handles ADD COLUMN +// on nullable types fine). Migration 6 uses convert_and_rename anyway to +// demonstrate the rebuild shape, which IS the only path for the cases ALTER +// can't reach: PK changes, type narrowing, FK alterations, CHECK changes. +// +// The dance: +// 1. CREATE TABLE users_new with the new shape. +// 2. SELECT every row of `users` (read via UserV5 — the legacy struct), +// run it through m_v5_to_v6 (auto-derived field copy + LastLoginAt +// defaults to none()), INSERT into users_new. +// 3. DROP TABLE users. +// 4. ALTER TABLE users_new RENAME TO users. +// Runs inside migrate_to_latest's α-shape transaction; failure rolls back. + +[sql_migration(version = 6, description = "rebuild: add LastLoginAt")] +def migration_006(db : SqlRunner) { + db |> convert_and_rename(type, type) +} + [export] def main() : int { // ================================================================= @@ -177,15 +221,40 @@ def main() : int { // - DROP COLUMN / RENAME COLUMN — old names aren't in the current // struct, so daslang has nothing to validate against. // - PK / UNIQUE inline / generated columns added post-hoc — SQLite - // can't ALTER these in place; needs a table rebuild - // (chunk 14c, struct_convert). + // can't ALTER these in place; needs a table rebuild — see + // PART 5 below for the struct_convert + convert_and_rename path. // - CHECK constraints, FK ADD/DROP, column type changes. // - Anything ad-hoc that doesn't map to a struct field. + // ================================================================ + // PART 5 — Schema rebuild: verify migration 6 landed + // ================================================================ + // + // Migration 6 ran inside migrate_to_latest. Two checks: + // - The new column is on the table. + // - alice survived the rebuild (Email preserved). + + let last_login_count = db |> query_scalar( + "SELECT COUNT(*) FROM pragma_table_info('users') WHERE name='LastLoginAt'", type) + print("post-rebuild LastLoginAt column present: {last_login_count == 1}\n") + let alice_post_rebuild = db |> query_scalar( + "SELECT Email FROM users WHERE Name='alice'", type) + print("alice survived rebuild: Email={alice_post_rebuild}\n") + + // GOTCHA: indexes created at runtime (migrations 4 and 5 used + // `create_index` / `create_unique_index`) are NOT reflected in + // [sql_index] annotations on the User struct, so convert_and_rename's + // `create_table(type, "users_new")` doesn't recreate them. + // After the rebuild, the indexes are gone: let n_idx = db |> query_scalar( "SELECT COUNT(*) FROM sqlite_master WHERE type='index' AND tbl_name='users' AND name NOT LIKE 'sqlite_%'", type) - print("user indexes after migration 5: {n_idx}\n") + print("user indexes after rebuild: {n_idx} (gone — see note above)\n") + // Two ways to avoid this: + // - Declare indexes via [sql_index(...)] siblings on the struct so + // create_table emits them during convert_and_rename. + // - Recreate indexes inside migration_006 after convert_and_rename + // via create_index / create_unique_index calls. } // ================================================================= diff --git a/utils/hygiene/rule_collapse_blank_lines.das b/utils/hygiene/rule_collapse_blank_lines.das new file mode 100644 index 0000000000..3077172d60 --- /dev/null +++ b/utils/hygiene/rule_collapse_blank_lines.das @@ -0,0 +1,31 @@ +options gen2 + +module rule_collapse_blank_lines shared private + +require strings +require daslib/strings_boost + + +def public collapse_blank_lines(src : string) : string { + var lines <- split(src, "\n") + var crlf = false + for (raw_line in lines) { + if (ends_with(raw_line, "\r")) { + crlf = true + break + } + } + let nl = crlf ? "\r\n" : "\n" + var out : array + var prev_blank = false + for (raw_line in lines) { + let line = ends_with(raw_line, "\r") ? slice(raw_line, 0, length(raw_line) - 1) : raw_line + let is_blank = empty(strip(line)) + if (is_blank && prev_blank) { + continue + } + out |> push(line) + prev_blank = is_blank + } + return out |> join(nl) +} diff --git a/utils/hygiene/rules.das b/utils/hygiene/rules.das index 9b80148d04..4c712ef0c1 100644 --- a/utils/hygiene/rules.das +++ b/utils/hygiene/rules.das @@ -3,8 +3,9 @@ options gen2 module rules shared private require rule_private_docs public +require rule_collapse_blank_lines public def public apply_all_rules(src : string) : string { - return trim_private_docstrings(src) + return collapse_blank_lines(trim_private_docstrings(src)) } diff --git a/utils/hygiene/test_hygiene.das b/utils/hygiene/test_hygiene.das index fc1295492d..bbed1b7221 100644 --- a/utils/hygiene/test_hygiene.das +++ b/utils/hygiene/test_hygiene.das @@ -4,6 +4,8 @@ require dastest/testing_boost public require strings require rule_private_docs +require rule_collapse_blank_lines +require rules require process @@ -186,3 +188,69 @@ def test_process_path_directory(t : T?) { let r = process_path(".", false) t |> equal(r.kind, ProcessResultKind.not_a_file) } + + +[test] +def test_collapse_double_blank(t : T?) { + let input = "a\n\n\nb\n" + let want = "a\n\nb\n" + t |> equal(collapse_blank_lines(input), want) +} + + +[test] +def test_collapse_many_blank(t : T?) { + let input = "a\n\n\n\n\nb\n" + let want = "a\n\nb\n" + t |> equal(collapse_blank_lines(input), want) +} + + +[test] +def test_preserve_single_blank(t : T?) { + let input = "a\n\nb\n" + t |> equal(collapse_blank_lines(input), input) +} + + +[test] +def test_no_blanks_unchanged(t : T?) { + let input = "a\nb\nc\n" + t |> equal(collapse_blank_lines(input), input) +} + + +[test] +def test_collapse_whitespace_only_blank(t : T?) { + // Lines containing only spaces/tabs count as blank for collapsing. + let input = "a\n \n\t\nb\n" + let want = "a\n \nb\n" + t |> equal(collapse_blank_lines(input), want) +} + + +[test] +def test_collapse_idempotent(t : T?) { + let input = "a\n\n\n\nb\n\n\nc\n" + let once = collapse_blank_lines(input) + let twice = collapse_blank_lines(once) + t |> equal(once, twice) +} + + +[test] +def test_collapse_preserves_crlf(t : T?) { + let input = "a\r\n\r\n\r\nb\r\n" + let want = "a\r\n\r\nb\r\n" + t |> equal(collapse_blank_lines(input), want) +} + + +[test] +def test_apply_all_rules_chains_collapse(t : T?) { + // Top-level double-blank between two public defs must be collapsed by + // the rule chain (collapse_blank_lines runs after trim_private_docstrings). + let input = "def a() \{\n return 1\n\}\n\n\n\ndef b() \{\n return 2\n\}\n" + let want = "def a() \{\n return 1\n\}\n\ndef b() \{\n return 2\n\}\n" + t |> equal(apply_all_rules(input), want) +}