Skip to content

Module cache: the serializer refuses a same-module ExprCallMacro inside an uninferred generic body (a macro-registered function called from a generic); cold run exits 1 #15

Closed

Description

@lookibed

Summary

Writing the module cache fails when a generic function's body holds an ExprCallMacro whose macro belongs to the module being written. The CallMacro * serializer refuses it:

SERIALIZER_VERIFYF ( !(ptr->module == thisModule), "did not expect to find macro from the current module" );

(src/builtin/module_builtin_ast_serialize.cpp:2618, reached from SerializeVisitor::preVisit(ExprCallMacro*)).

A call macro is replaced by its expansion only when a body is inferred. A generic's own body is stored uninferred in Module.generics, so the ExprCallMacro survives into the cache write. The shape is any annotation that registers a call macro in the compiling module from a function annotation's apply (a variadic-style [annotation] def f(...) whose call sites f(...) are rewritten by a macro) and a generic in the same module that calls f(...). A non-generic caller passes, and a caller that spells the macro's expansion directly passes.

Combined with the second report (a failed cache write fails the compile), a cold run of such a program exits 1 with no output under the default cache.

Reproduction

rename_and_register.das:

options gen2
module rename_and_register public

require daslib/ast
require daslib/ast_boost
require daslib/templates_boost

// `name(x)` is rewritten to `name`impl(x)`; the macro is added to the module being compiled
class ForwardCallMacro : AstCallMacro {
    target : string
    def override visit(prog : ProgramPtr; mod : Module?; var expr : ExprCallMacro?) : ExpressionPtr {
        return qmacro($c(target)($e(expr.arguments[0])))
    }
}

// `[rename_and_register]` renames the function to `name`impl` and registers the call macro
// `name` in the compiling module, the shape a variadic-style annotation takes
[function_macro(name = "rename_and_register")]
class RenameAndRegister : AstFunctionAnnotation {
    def override apply(var func : FunctionPtr; var group : ModuleGroup; var args : AnnotationArgumentList; var errors : das_string) : bool {
        let c_name = string(func.name)
        let target = "{c_name}`impl"
        func.name := target
        compiling_module() |> add_call_macro(make_call_macro(c_name, new ForwardCallMacro(target = target)))
        return true
    }
}

generic_call_macro.das:

options gen2
require rename_and_register

[rename_and_register]
def twice(x : int) : int {
    return x * 2
}

// a generic: its body is stored uninferred, with the ExprCallMacro still in it
def outer(a : auto) : int {
    return twice(a)
}

[export]
def main {
    print("{outer(5)}\n")
}

Run from a directory with no .cache/daslang/module_cache yet:

$ daslang generic_call_macro.das
$ echo $?
1
$ daslang generic_call_macro.das
10
$ daslang -no-module-cache generic_call_macro.das
10
$ daslang -module-cache x.dascache generic_call_macro.das
[W] das: serialize: program write failed: did not expect to find macro from the current module
ser: wrote 1379779 bytes to 'x.dascache'
$ echo $?
1
$ daslang -module-cache x.dascache generic_call_macro.das
ser: reparsing in place 'generic_call_macro.das' (registers at compile time)
deser: partial - 1 module(s) reparsed in place, rest from cache
10

Control: with def outer(a : int) : int (not generic) the cold run prints 10 and exits 0 under both the default and an explicit cache.

Expected

The cold run prints 10 and exits 0, as the warm run and -no-module-cache do. A module that registers macros is reparsed in place on every read anyway, so its uninferred generic bodies have no reason to be written: the serializer could skip the macro reference for the current module (or write the generic body as unparsed source), or preVisit(ExprCallMacro*) could treat a same-module macro as "this module is not cacheable" without failing.

Actual

The write fails with did not expect to find macro from the current module; with the default (quiet) cache the process exits 1 and prints nothing.

Environment

  • daScript commit c4e4906 (master), daslang 0.6.4, built with gcc 14.2.0 (Debian 14.2.0-19)
  • Debian GNU/Linux 13, Linux 6.12 x86_64

Activity

  1. lookibed commented on Nov 9, 2026

    @lookibed
    OwnerAuthor

    Re-checked by the owner from this issue's reproducer on daScript abf1d40 (0.6.5): fixed upstream (GaijinEntertainment/daScript 2131620 GaijinEntertainment#4472 dasLLVM, 1404d5a GaijinEntertainment#4480 optimizer, f386f92 GaijinEntertainment#4482 cache and tools). The c2das-re probes were re-pinned in the repin PRs (lookibed/c2das-re#142-GaijinEntertainment#147; ruling 38 addendum 4).

  2. lookibed commented on Nov 9, 2026

    @lookibed
    OwnerAuthor

    Fixed upstream: GaijinEntertainment#4447 is closed and the fix is in master abf1d40e35 (0.6.5). Verified by running this issue's repro on a build of that commit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions