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
279 changes: 279 additions & 0 deletions QUOTE_LOWERING.md

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions daslib/aot_cpp.das
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,15 @@ class NoAotMarker : AstVisitor {
}
}
}
// quote that was not lowered. without aot_macros infer already made the function noAot
// (then this stays silent); with aot_macros set it means daslib/quote lowering did not
// run — the module with the quote is missing daslib/templates_boost (or daslib/quote)
def override preVisitExprQuote(expr : ExprQuote?) {
if (func != null && !func.flags.noAot) {
func.flags.noAot = true
to_log(LOG_ERROR, "{describe(expr.at)}: quote( ) survived to AOT with aot_macros enabled — daslib/quote lowering did not run; require daslib/templates_boost (or daslib/quote) in the module containing the quote. Function '{func.name}' excluded from AOT.\n")
}
}
};

class public PrologueMarker : AstVisitor {
Expand Down Expand Up @@ -1814,6 +1823,13 @@ class public CppAot : AstVisitor {
write(*ss, ")");
return that;
}
// quote
def override visitExprQuote(var that : ExprQuote?) : ExpressionPtr {
// unreachable: NoAotMarker excludes functions with surviving quotes. a panic here
// would be swallowed by runMacroFunction, so write a hard #error into the C++ instead
write(*ss, "\n#error quote( ) was not lowered (daslib/quote) at {describe(that.at)}\n")
return that;
}
// op1
def outPolicy(decl : TypeDeclPtr) {
if (decl.baseType != Type.tHandle){
Expand Down
109 changes: 101 additions & 8 deletions daslib/quote.das
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,7 @@ def public cvt_to_mks(var args) : MakeStruct? {
var res = new MakeStruct(uninitialized);
*res |> resize(length(args))
for (arg, i in args, count()) {
{
move_new((*res)[i]) <| arg;
}
(*res)[i] = arg;
}
return res;
}
Expand All @@ -148,6 +146,18 @@ def public clone_file_info(b : FileInfoInitData) : FileInfo? {
return clone_file_info(b.name, b.tabSize)
}

var private g_resolved_file_infos : table<string; FileInfo?>

def public resolve_file_info(b : FileInfoInitData) : FileInfo? {
//! Stable FileInfo for reconstructed LineInfos: one interned dummy per (file name, tab size)
//! (per context), instead of a fresh allocation per evaluation.
let key = "{b.name}:{b.tabSize}"
if (!key_exists(g_resolved_file_infos, key)) {
g_resolved_file_infos[key] = clone_file_info(b.name, b.tabSize)
}
return g_resolved_file_infos[key]
}

let blacklist = [
("ast_core::Function", "inferStack"),
("ast_core::Function", "annotations"),
Expand All @@ -156,6 +166,7 @@ let blacklist = [
("ast_core::ExprVar", "pBlock"), // loop
("ast_core::EnumEntry", "value"), // loop
("ast_core::EnumEntry", "at"), // ignore it for now
("ast_core::ExprBlock", "stackCleanVars"), // post-infer bookkeeping (allocateStack); always empty in quoted trees, and its vector<pair> type has no AOT mapping
("rtti_core::AnnotationArgument", "at"),
("ast_core::CaptureEntry", "at"), // ignore it for now
("ast_core::MakeFieldDecl", "at"), // ignore it for now
Expand All @@ -169,6 +180,7 @@ let managed_types = [
"ast_core::Structure",
"ast_core::CallMacro",
"ast_core::Function",
"ast_core::Enumeration",
"rtti_core::FileInfo",
"ast_core::MakeStruct",
"rtti_core::LineInfo",
Expand Down Expand Up @@ -198,6 +210,31 @@ def private create_string_expr(name) {
return new ExprConstString(value := name);
}

def private fix_path_spelling(p : string) : string {
var r = p |> replace("\\", "/")
if (length(r) >= 2) {
r = r |> modify_data() $(var arr) {
let c = uint(arr[0])
if (uint(arr[1]) == uint(':') && c >= uint('a') && c <= uint('z')) {
arr[0] = uint8(c - uint('a') + uint('A'))
}
}
}
return r
}

def private normalize_source_path(p : string) : string {
// the same source file may be opened via different spellings at AOT-generation time
// and at runtime (slash form, drive-letter case, dasroot-absolute vs repo-relative);
// the path is baked into the lowered code as a string constant, so it must hash
// identically — canonicalize to dasroot-relative (machine-independent), else absolute
var root = fix_path_spelling(get_das_root())
if (!(root |> ends_with("/"))) {
root += "/"
}
return fix_path_spelling(p) |> trim_prefix(root)
}


def private to_array_move_expr(var mkArr : ExprMakeArray?, at : LineInfo) {
var mkToArrayMove = new ExprCall(at = at, name := "to_array_move")
Expand Down Expand Up @@ -227,7 +264,10 @@ def private convert_vec_expr(mod : Module?; field_values : uint8 const?; xtype :
for (idx in range(get_vector_length(field_values, src_tp))) {
{
var arg_expr = convert_quote_expr(mod, unsafe(reinterpret<uint8?>(get_vector_ptr_at_index(field_values, src_tp, idx))), src_tp, at);
if (dst_tp.baseType != Type.tString && dst_tp != src_tp) {
// retarget the element make-struct to the InitData wrapper type when get_dst_type
// substituted one; pointer compare (dst_tp != src_tp) is always true after clone_type,
// and writing makeType through a reinterpret of a non-ExprMakeStruct stomps the node
if (dst_tp.baseType != Type.tString && describe(dst_tp) != describe(src_tp) && arg_expr is ExprMakeStruct) {
var make_str = unsafe(reinterpret<ExprMakeStruct?>(arg_expr));
make_str.makeType = clone_type(dst_tp);
args |> emplace(make_str);
Expand Down Expand Up @@ -298,6 +338,10 @@ def private convert_quote_struct_field(mod : Module?, data : uint8 const?,
res_field = new ExprCall(at = parent_loc, name := "clone_line_info",
arguments := array<Expression?>(prev))
}
if (tstr == "rtti_core::FileInfo" && name == "name" && res_field is ExprConstString) {
var cs = unsafe(reinterpret<ExprConstString?>(res_field))
cs.value := normalize_source_path(string(cs.value))
}

return create_field_expr(name, clone(res_field), parent_loc, flags);
}
Expand All @@ -315,15 +359,36 @@ def private convert_quote_structure(mod : Module?; data : uint8 const?;
var mkStr = new ExprMakeStruct(makeType = get_dst_type(clone_type(info)), at = at)
emplace(mkStr.structs, mkS1)
mkStr.makeStructFlags |= ExprMakeStructFlags.useInitializer | ExprMakeStructFlags.canShadowBlock;
if (describe(info) == "rtti_core::FileInfo") return new ExprCall(at = at, name := "clone_file_info",
if (describe(info) == "rtti_core::FileInfo") return new ExprCall(at = at, name := "resolve_file_info",
arguments := array<Expression?>(mkStr))
return mkStr;
}

def public make_alias_type_decl(name : string) : TypeDecl? {
//! Reconstruction helper: a by-name alias TypeDecl, resolved by re-infer at the splice site.
return new TypeDecl(baseType = Type.alias, alias := name)
}

def private convert_pointer_expr(mod; data; info; at) : Expression? {
assume pdata = *unsafe(reinterpret<uint8 const ??> data);
if (pdata == null) return new ExprConstPtr(at = at);
assume first_tstr = describe(info.firstType);
if (first_tstr == "ast_core::TypeDecl") {
// a type that references an enum/struct from an anonymous module (the program's
// main module) can't be reconstructed by name — rebuild it as the alias the parser
// would have produced for an unresolved name; splice-site re-infer resolves it
assume td = unsafe(reinterpret<TypeDecl?>(pdata));
var aliasName = ""
if (td.enumType != null && (td.enumType._module == null || empty(td.enumType._module.name))) {
aliasName = string(td.enumType.name)
} elif (td.structType != null && (td.structType._module == null || empty(td.structType._module.name))) {
aliasName = string(td.structType.name)
}
if (!empty(aliasName)) {
return new ExprCall(at = at, name := "make_alias_type_decl",
arguments := array<Expression?>(create_string_expr(aliasName)));
}
}
var final_type = clone_type(info.firstType);
if (first_tstr == "ast_core::Expression") {
assume annotation = mod |> module_find_type_annotation(unsafe(reinterpret<Expression?>(pdata)).__rtti);
Expand Down Expand Up @@ -365,6 +430,12 @@ def private convert_quote_expr(mod : Module?; var data : uint8 const?; info : Ty
} elif (tstr == "ast_core::Function") {
assume pdata = unsafe(reinterpret<Function?>(data));
return get_find_in_module_expr("find_unique_function_ptr", pdata._module.name, pdata.name, at);
} elif (tstr == "ast_core::Enumeration") {
assume pdata = unsafe(reinterpret<Enumeration?>(data));
// anonymous module (e.g. the program's main module) can't be found by name at
// reconstruction time; the TypeDecl-level alias fallback covers that shape
if (pdata._module == null || empty(pdata._module.name)) return null
return get_find_in_module_expr("module_find_enumeration", pdata._module.name, pdata.name, at);
} elif (tstr == "rtti_core::Module") {
assume cur_mod = unsafe(reinterpret<Module?>(data));
return find_module_expr(cur_mod.name, at);
Expand Down Expand Up @@ -396,9 +467,28 @@ def convert_quote_to_expression(var arg_expr : Expression?; at : LineInfo) : Exp
class QuoteConverter : AstVisitor {
//! AST visitor that converts quoted expressions to init data.
astChanged : bool = false
mod : Module?
counter : int = 0
def override visitExprQuote(var expr : ExprQuote?) : Expression? {
astChanged = true
return convert_quote_to_expression(expr.arguments[0], expr.at)
var conv = convert_quote_to_expression(expr.arguments[0], expr.at)
// wrap the construction in a per-quote generated function and replace the quote with
// a call: the (large) construction expression then occupies one non-recursive leaf
// frame instead of inflating every caller's frame (fatal in recursive macros)
var fname = ""
while (true) {
fname = "`quote`lowered`{counter}"
counter++
break if (find_unique_function(mod, fname, true) == null)
}
var fn = new Function(at = expr.at, atDecl = expr.at, name := fname,
flags = FunctionFlags.generated | FunctionFlags.privateFunction)
fn.result = new TypeDecl(baseType = Type.autoinfer)
var blk = new ExprBlock(at = expr.at)
emplace_new(blk.list) <| new ExprReturn(at = expr.at, subexpr = conv)
fn.body = blk
mod |> add_function(fn)
return new ExprCall(at = expr.at, name := fname)
}
}

Expand All @@ -407,8 +497,11 @@ class QuotePass : AstPassMacro {
//! Pass macro that processes quoted AST expressions.
def override apply(prog : ProgramPtr; mod : Module?) : bool {
// Unwrapping ExprQuote is slow and inefficient, do it only if necessary.
if (!compiling_program().policies.aot_macros) return false // nothing to do
var astVisitor = new QuoteConverter()
// Triggered by the aot_macros policy (`daslang -aot -aot-macros`) or per-module
// `options aot_macros` (self-contained tests, interpreted A/B).
let lower = compiling_program().policies.aot_macros || (prog._options |> find_arg("aot_macros") ?as tBool ?? false)
if (!lower) return false // nothing to do
var astVisitor = new QuoteConverter(mod = compiling_module())
make_visitor(*astVisitor) $(astVisitorAdapter) {
visit(prog, astVisitorAdapter)
}
Expand Down
3 changes: 3 additions & 0 deletions daslib/templates_boost.das
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ require daslib/ast
require daslib/ast_boost
require daslib/algorithm
require daslib/strings_boost
// quote lowering (QuotePass + runtime reconstruction helpers); public so the
// reconstruction calls generated into qmacro-using modules resolve there
require daslib/quote public

struct Template {
//! This structure contains collection of substitution rules for a template.
Expand Down
3 changes: 3 additions & 0 deletions dastest/dastest_clargs.das
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ struct DastestArgs {
@clarg_doc = "Compile and run tests using AOT (ahead-of-time) compilation, failing if AOT is unavailable"
use_aot : bool

@clarg_doc = "Force quote()/qmacro lowering (daslib/quote QuotePass) when compiling test files"
aot_macros : bool

@clarg_doc = "Enable benchmark execution (all of them)"
bench : bool

Expand Down
8 changes: 8 additions & 0 deletions dastest/suite.das
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ struct SuiteCtx {
verbose : bool = false
compile_only : bool = false
use_aot : bool = false
aot_macros : bool = false
cov_path : string = ""
debugger : bool = false
keep_alive : bool = false
Expand Down Expand Up @@ -86,6 +87,7 @@ def SuiteCtx(args : DastestArgs) : SuiteCtx {
ctx.projectPath = args.test_project
ctx.compile_only = args.compile_only
ctx.use_aot = args.use_aot || is_in_aot()
ctx.aot_macros = args.aot_macros
ctx.cov_path = args.cov_path
ctx.testNames := args.test_names
ctx.bench_names := args.bench_names
Expand Down Expand Up @@ -158,6 +160,12 @@ def test_file(file_name : string; var ctx : SuiteCtx; var file_ctx : FileCtx) :
cop.aot_module = true
cop.aot = ctx.use_aot
cop.fail_on_no_aot = ctx.use_aot
cop.aot_macros = ctx.aot_macros
if (ctx.aot_macros) {
// a lowered quote evaluates one large make-struct construction frame
// (same bump the `daslang -aot -aot-macros` flow applies)
cop.stack = 1024u * 1024u
}
cop.threadlock_context = true
cop.jit_enabled = jit_enabled()
cop.debugger = ctx.debugger
Expand Down
6 changes: 3 additions & 3 deletions doc/reflections/das2rst.das
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ def document_module_ast(_root : string) {
group_by_regex("Removal", mod, %regex~(remove.*)%%),
group_by_regex("Properties", mod, %regex~(is|has|get|can)_.*%%),
group_by_regex("Infer", mod, %regex~(infer_generic_type|update_alias_map)$%%),
group_by_regex("Module queries", mod, %regex~(module_find_annotation|module_find_type_annotation|module_find_structure|module_has_comment_reader|not_inferred)$%%),
group_by_regex("Module queries", mod, %regex~(module_find_annotation|module_find_type_annotation|module_find_structure|module_find_enumeration|module_has_comment_reader|not_inferred)$%%),
group_by_regex("Debug info helpers", mod, %regex~(debug_helper_.*)%%),
group_by_regex("AOT support", mod, %regex~(aot_require|aot_type_ann_get_field_ptr|aot_need_type_info|aot_previsit_get_field_ptr|aot_previsit_get_field|aot_visit_get_field|write_aot_body|write_aot_suffix|write_aot_macro_suffix|write_aot_macro_prefix|macro_aot_infix|getInitSemanticHashWithDep|get_aot_hash_comment)$%%),
group_by_regex("String builder writer", mod, %regex~(string_builder_str|string_builder_clear)$%%),
Expand Down Expand Up @@ -1269,8 +1269,8 @@ def document_module_coverage(_root : string) {
def document_module_quote(_root : string) {
var mod = find_module("quote")
var groups <- array<DocGroup>(
group_by_regex("Clone operations", mod, %regex~(clone|clone_line_info|clone_file_info)$%%),
group_by_regex("Conversion", mod, %regex~(cvt_to_mks)$%%)
group_by_regex("Clone operations", mod, %regex~(clone|clone_line_info|clone_file_info|resolve_file_info)$%%),
group_by_regex("Conversion", mod, %regex~(cvt_to_mks|make_alias_type_decl)$%%)
)
document("AST quasiquoting infrastructure", mod, "quote.rst", groups)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Finds an enumeration by name in the given module. The module must not be null (panics otherwise); returns null when the module does not declare an enumeration with that name.
1 change: 1 addition & 0 deletions include/daScript/simulate/aot_builtin_ast.h
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,7 @@ namespace das {
DAS_CC_API Annotation * get_expression_annotation ( Expression * expr, Context * context, LineInfoArg * at );
DAS_CC_API Structure * find_unique_structure ( smart_ptr_raw<Program> prog, const char * name, Context * context, LineInfoArg * at );
DAS_CC_API Structure * module_find_structure ( const Module* module, const char * name, Context * context, LineInfoArg * at );
DAS_CC_API Enumeration * module_find_enumeration ( const Module* module, const char * name, Context * context, LineInfoArg * at );
DAS_CC_API void get_use_global_variables ( Function * func, const TBlock<void,VariablePtr> & block, Context * context, LineInfoArg * at );
DAS_CC_API void get_use_functions ( Function * func, const TBlock<void,FunctionPtr> & block, Context * context, LineInfoArg * at );
DAS_CC_API Structure::FieldDeclaration * ast_findStructureField ( Structure * structType, const char * field, Context * context, LineInfoArg * at );
Expand Down
5 changes: 5 additions & 0 deletions modules/dasLLVM/daslib/llvm_jit.das
Original file line number Diff line number Diff line change
Expand Up @@ -3389,6 +3389,11 @@ class public LlvmJitVisitor : AstVisitor {
unsupported(expr, "try-catch")
}

// ExprQuote
def override preVisitExprQuote(expr : ExprQuote?) : void {
unsupported(expr, "quote( ) is not jit-able without aot_macros lowering (daslib/quote)")
}

// ExprIfThenElse
/*
if_cond_at:
Expand Down
1 change: 1 addition & 0 deletions skills/aot_testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ Functions with `SideEffects::none` can be **constant-folded** at compile time. I
| Dependency hash differs | A dependency function's SIM tree changed (different module state in batch) | Compare dep hashes in the hash comment; inspect the specific dependency |
| AOT hash differs but own + all deps match | Dependency list differs (extra or missing deps) | Check if batch processing adds/removes function instantiations |
| All hashes match but still fails | Stale generated C++ — `.cpp` file wasn't regenerated | Delete `_aot_generated/*.cpp` and rebuild |
| Own hash differs after editing a quote-lowered file (`options aot_macros`) | ANY edit — even comment-only — shifts line numbers, and lowered code bakes LineInfos as integer constants | Purge that directory's `_aot_generated/` and rebuild `test_aot` (the custom commands don't track daslib deps either — same purge after `daslib/quote.das` edits) |

### Batch AOT processing

Expand Down
14 changes: 14 additions & 0 deletions skills/writing_tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ comment + issue link on both. Glob exclusion alone is NOT enough — test_aot st
the file and trips 50101 on its missing stubs; `options no_aot` is what makes the runtime
skip AOT linking for it.

## Per-folder sweep gating (`tests/.das_test`)

`tests/.das_test` defines `can_visit_folder(folder_name, result)` — dastest consults it
per subfolder during file collection (only for the `.das_test` at the `--test <root>`
argument; directly naming a child folder bypasses it). It gates folders on module
availability (`dasHV`, `dasSQLITE`, …) and on sweep mode by scanning argv for `-jit` /
`--use-aot` (e.g. `ast`, `ast_match`, `no_aot`, `gc`, `quote` skip under `-jit` — runtime
quote/qmacro the JIT can't codegen). Two traps: a whole-folder JIT/AOT failure usually
means a missing entry here, NOT a per-file fix; and the `jit_cache_all_tests` prewarm
target (utils/CMakeLists.txt) does NOT consult it — its `--exclude` list mirrors the
`-jit` skips manually and must be updated in the same change. Per-function `[test, no_jit]`
(tests/template/test_push_block_list.das) is the finer-grained alternative when only some
tests in a kept folder can't JIT.

## Test file structure

```das
Expand Down
5 changes: 3 additions & 2 deletions src/ast/ast_infer_type.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1437,9 +1437,10 @@ namespace das {
expr->type = new TypeDecl(Type::tPointer);
expr->type->firstType = new TypeDecl(Type::tHandle);
expr->type->firstType->annotation = (TypeAnnotation *)Module::require("ast_core")->findAnnotation("Expression");
// mark quote as noAot
// mark quote as noAot, unless daslib/quote lowering will replace it
// (aot_macros policy or per-module `options aot_macros` — same gate as QuotePass)
if (func) {
if (!program->policies.aot_macros) {
if (!program->policies.aot_macros && !program->options.getBoolOption("aot_macros", false)) {
func->noAot = true;
}
}
Expand Down
Loading
Loading