Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/extended_checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,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'
Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,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
Expand Down
3 changes: 2 additions & 1 deletion daslib/aot_cpp.das
Original file line number Diff line number Diff line change
Expand Up @@ -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<Program>; 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<Context>; serrors) {
if (!sok) {
to_log(LOG_ERROR, "failed to simulate {input}\n{serrors}\n")
panic("Failed to simulate {serrors}")
}
blk(program, pctx)
Expand Down
7 changes: 7 additions & 0 deletions dastest/dastest.das
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,13 @@ def serialize_path(ctx : SuiteCtx, files : array<string>, 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
Expand Down
41 changes: 36 additions & 5 deletions doc/source/reference/language/classes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -158,13 +183,19 @@ parent's finalizer on the current object:
}
}

The compiler rewrites ``delete super.self`` into ``delete cast<Base>(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 <structs>`.
The compiler rewrites ``delete super.self`` into ``delete cast<T>(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 <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 <options>`).
Expand Down
11 changes: 8 additions & 3 deletions doc/source/reference/language/structs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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<Foo>(self)``. See
:ref:`Classes <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<T>(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 <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:

Expand Down
120 changes: 109 additions & 11 deletions doc/source/reference/tutorials/sql_43_migrations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>`` wrap** --- recurses on the inner
``S -> T``, then wraps with ``some()``. So ``int -> Option<string>``
works because the inner ``int -> string`` overload fires.
- **``Option<S> -> T`` unwrap** --- NULL collapses to
``default<S>``, then recurses on the inner ``S -> T``.
- **``Option<S> -> Option<T>`` 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<MyEnum> -> 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<T>``), 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<S>, type<T>)`` runs the
whole rebuild: CREATE staging table, copy rows through the
converter, DROP original, ALTER ... RENAME staging -> target.
Lower-level ``db |> convert(type<S>, type<T>, 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<UserV6>, type<User>)
}

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
======================================

Expand Down Expand Up @@ -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>`
Expand Down
5 changes: 3 additions & 2 deletions include/daScript/ast/ast.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
namespace das
{
struct AstSerializer;
class Visitor;

class Function;
typedef Function * FunctionPtr;
Expand Down Expand Up @@ -691,7 +692,7 @@ 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;
TypeDeclPtr type = nullptr;
Expand Down Expand Up @@ -745,7 +746,7 @@ namespace das
virtual bool rtti_isConstant() const override { return true; }
template <typename QQ> QQ & cvalue() { return *((QQ *)&value); }
template <typename QQ> 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;
Expand Down
Loading
Loading