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
2 changes: 1 addition & 1 deletion daslib/aot_cpp.das
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ def hex_char(var Ch : int) {
}

def aotSuffixNameEx(funcName : das_string; suffix : string) {
var prefix = false;
var prefix = is_cpp_keyword(string(funcName));

let name = build_string() $(var writer) {
for (ch in string(funcName)) {
Expand Down
46 changes: 46 additions & 0 deletions doc/source/reference/language/tuples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,52 @@ tuple directly:
Mixing named and positional fields in the same literal is **not** supported —
either every field is named or none are.

Shorthand promotion
-------------------

When every element of a positional tuple literal is a bare variable reference,
the compiler may promote it to a named tuple by taking the field names from the
variables. This only fires when the target type is unambiguously a named tuple
and the variable names match the field names in order:

.. code-block:: das

let eid = 7
let distSq = 2.5
var t : tuple<eid:int; distSq:float> = (eid, distSq) // ok, promoted
var arr : array<tuple<eid:int; distSq:float>>
arr |> push((eid, distSq)) // ok, promoted

def make_hit(eid : int; distSq : float) : tuple<eid:int; distSq:float> {
return (eid, distSq) // ok, promoted
}

Promotion is a fallback: if a matching overload already exists for the unnamed
tuple type, that overload wins and no promotion happens. If you want the named
overload, use the explicit named-field literal:

.. code-block:: das

def overload_pick(x : tuple<int; float>) { return 1 }
def overload_pick(x : tuple<x:int; y:float>) { return 2 }
let x = 1
let y = 2.0
overload_pick((x, y)) // returns 1: unnamed overload wins
overload_pick((x=x, y=y)) // returns 2: explicit named literal

A name mismatch fails compilation rather than silently constructing the
unnamed tuple:

.. code-block:: das

let foo = 1
let bar = 1.1
var arr : array<tuple<eid:int; distSq:float>>
// arr |> push((foo, bar)) // error: function_not_found

Promotion does not fire when any element is not a bare variable reference, e.g.
``(a, a+1)`` stays unnamed.

Tuple elements can be accessed via nameless fields, i.e. _ followed by the 0 base field index:

.. code-block:: das
Expand Down
34 changes: 34 additions & 0 deletions doc/source/reference/tutorials/11_tuples_and_variants.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,40 @@ Give a tuple named fields for readability::
let p = Point2D(x=3.0, y=4.0)
print("{p.x}, {p.y}\n")

Shorthand: unnamed → named promotion
====================================

When the target type is a named tuple and every element of a positional literal
is a bare variable reference whose name matches the field name in order, the
compiler promotes the literal to the named tuple — no need to repeat the
field names::

let eid = 7
let distSq = 2.5
var hit : tuple<eid:int; distSq:float> = (eid, distSq) // ok, promoted

var hits : array<tuple<eid:int; distSq:float>>
hits |> push((eid, distSq)) // ok, promoted

It also fires in return position when the function's declared result is a
named tuple::

def make_hit(eid : int; distSq : float) : tuple<eid:int; distSq:float> {
return (eid, distSq) // ok, promoted
}

Promotion is fallback-only: if an unnamed-tuple overload already matches, it
wins. Use the explicit ``(name = value)`` literal to force the named overload::

def overload_pick(x : tuple<int; float>) { return 1 }
def overload_pick(x : tuple<x:int; y:float>) { return 2 }
overload_pick((x, y)) // 1 — unnamed overload wins
overload_pick((x=x, y=y)) // 2 — explicit named literal

A name mismatch fails compilation (no silent fallback to unnamed). Mixed
expressions like ``(a, a + 1)`` are not bare variable references and never
promote.

Destructuring
=============

Expand Down
1 change: 1 addition & 0 deletions include/daScript/ast/ast_expressions.h
Original file line number Diff line number Diff line change
Expand Up @@ -1494,6 +1494,7 @@ namespace das
virtual void gc_collect ( gc_root * target, gc_root * from ) override;
bool isKeyValue = false;
vector <string> recordNames;
vector <string> shorthandRecordNames;
};

struct DAS_API ExprArrayComprehension : Expression {
Expand Down
3 changes: 3 additions & 0 deletions include/daScript/ast/ast_infer_type.h
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ namespace das {

void findMatchingFunctionsAndGenerics(MatchingFunctions &resultFunctions, MatchingFunctions &resultGenerics, const string &name, const vector<TypeDeclPtr> &types, bool inferBlock = false, bool visCheck = true) const;

bool trySeedTupleShorthand(ExprLooksLikeCall *expr, bool visCheck);

void reportDualFunctionNotFound(const string &name, const string &extra,
const LineInfo &at, const MatchingFunctions &candidateFunctions,
const vector<TypeDeclPtr> &types, const vector<TypeDeclPtr> &types2, bool inferAuto, bool inferBlocks, bool reportDetails,
Expand Down Expand Up @@ -500,6 +502,7 @@ namespace das {

virtual void preVisit(ExprLet *expr) override;
virtual void preVisitLet(ExprLet *expr, const VariablePtr &var, bool last) override;
virtual void preVisitLetInit(ExprLet *expr, const VariablePtr &var, Expression *init) override;
bool isEmptyInit(const VariablePtr &var) const;
virtual VariablePtr visitLet(ExprLet *expr, const VariablePtr &var, bool last) override;
ExpressionPtr promoteToCloneToMove(const VariablePtr &var);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
---
slug: daslang-aot-cpp-emitter-mangles-keyword-and-operator-function-names
title: How does the daslang AOT C++ emitter mangle function names — for operator overloads and C++ keywords?
created: 2026-05-14
last_verified: 2026-05-14
links: []
---

The AOT C++ emitter lives in `daslib/aot_cpp.das` (NOT the emptied `src/ast/ast_aot_cpp.cpp`).

**The function-name mangler is `aotFuncName`** at `daslib/aot_cpp.das:928-935`:

```daslang
def public aotFuncName(func : Function?) {
if (func.hash != uint64(0)) {
return "{aotSuffixNameEx(func.name,"_Func")}_{func.hash:x}";
} else {
return "{aotSuffixNameEx(func.name,"_Func")}";
}
}
```

All daslang→C++ call sites and def sites go through this helper (verified: `daslib/aot_cpp.das:1084` def site, `:2950/:2966/:3459/:3785` call sites). No bypass paths.

**`aotSuffixNameEx`** (line 223-267) is where the rewriting happens:

```daslang
def aotSuffixNameEx(funcName : das_string; suffix : string) {
var prefix = is_cpp_keyword(string(funcName)); // <-- added 2026-05-14
let name = build_string() $(var writer) {
for (ch in string(funcName)) {
if (is_alnum(ch) || ch == '_') {
writer |> write_char(ch);
} else {
prefix = true;
var t_ch : string;
match (ch) {
if ('=') { t_ch = "Equ"; }
if ('+') { t_ch = "Add"; }
if ('-') { t_ch = "Sub"; }
if ('*') { t_ch = "Mul"; }
if ('/') { t_ch = "Div"; }
if ('%') { t_ch = "Mod"; }
if ('&') { t_ch = "And"; }
if ('|') { t_ch = "Or"; }
if ('^') { t_ch = "Xor"; }
if ('?') { t_ch = "Qmark"; }
if ('~') { t_ch = "Tilda"; }
if ('!') { t_ch = "Excl"; }
if ('>') { t_ch = "Greater"; }
if ('<') { t_ch = "Less"; }
if ('[') { t_ch = "Sqbl"; }
if (']') { t_ch = "Sqbr"; }
if ('.') { t_ch = "Dot"; }
if ('`') { t_ch = "Tick"; }
if (',') { t_ch = "Comma"; }
if (_) { t_ch = "_0x{hex(ch)}_" } // unmapped chars → hex escape
}
write(writer, "{t_ch}")
}
}
}
return prefix ? (suffix + name) : name; // suffix prepended (yes, prepended) iff prefix flag set
}
```

The `prefix` boolean controls whether the `suffix` arg ("_Func" for functions, "" for structs via `aotStructName`) is *prepended* to the mangled body. The flag fires in two conditions:

1. **Any non-alnum char in the name** (operator overloads, backtick-encoded names): the substitution loop sets `prefix = true` as a side effect.
2. **The name is a C++ keyword** (since 2026-05-14): `is_cpp_keyword(name)` seeds the flag.

**Example emissions:**

| daslang | C++ name |
|---|---|
| `def foo()` | `foo_<hash>` (no prefix — alnum, not keyword) |
| `def operator+()` | `_FuncAdd_<hash>` |
| `def \`testing\`equal\``() | `_FunctestingTickequalTick_<hash>` |
| `def float()` | `_Funcfloat_<hash>` (was rejected by lint pre-2026-05-14) |
| `def do()` | `_Funcdo_<hash>` |

**`aotStructName`** (line 269-271) uses the same helper with `suffix=""` — struct names need no prefix because the C++-keyword-name lint guard at `ast_lint.cpp:307` is still in force (relaxing struct names would need a different emit strategy, since struct names embed 1:1 into `struct Foo { ... }` syntax).

**`is_cpp_keyword(name : string) : bool`** is the daslang binding of C++ `isCppKeyword` (`src/builtin/module_builtin_ast.cpp:1572-1573`).

## Questions
- How does the daslang AOT emitter handle C++ keyword names like `def float`?
- How are operator overloads (`def +`, `def ==`) mangled in AOT C++ output?
- Where in daslang is the AOT C++ function-name mangler?
- What's `aotSuffixNameEx` and when does it prepend the `_Func` prefix?
- What does `_FuncAdd_<hash>` mean in generated AOT C++?
- Is there a way to find an AOT-emitted C++ symbol from a daslang function name?
- Can I call `is_cpp_keyword` from a daslang macro?
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
slug: dastest-failed-cant-invalid-prefix-expected-compile-failure-tests
title: How does dastest treat `failed_*`, `cant_*`, `invalid_*` test files?
created: 2026-05-14
last_verified: 2026-05-14
links: []
---

**Files in `tests/` whose basename starts with `failed_`, `cant_`, or `invalid_` are expected-compile-failure tests.** They declare which compile errors must fire, not what runtime behavior to test.

**dastest skip:** `dastest/dastest.das:160-166` (inside `serializeAst`):

```daslang
let base = base_name(file)
if (base |> starts_with("cant_") || base |> starts_with("failed_") || base |> starts_with("invalid_")) {
continue // skip AST serialization — they don't compile
}
```

**CMake skip:** Most `tests/<area>/CMakeLists.txt` GLOB filters exclude them from AOT compilation:
```cmake
list(FILTER AOT_FOO_FILES EXCLUDE REGEX "failed_")
```
because trying to AOT-compile a file that intentionally fails to compile would just break the build.

**The `expect` line** sits at the top of the file (after `options gen2`):

```daslang
options gen2
expect 30106:3, 30146, 30148, 30152, 30240, 30282
```

- Comma-separated list of expected error codes.
- `code:N` syntax = "exactly N occurrences" (default 1). Above: three 30106 errors plus one each of the rest.
- Order doesn't matter; line numbers don't matter; what matters is that the compiler emits exactly those codes those many times.
- PASS when the actual error set matches the expect set.

**Run via MCP:**

```
mcp__daslang__run_test tests/language/failed_reserved_names.das
```

emits the compile errors AND reports `PASS` (1 test, 1 passed) when codes match.

**Maintenance when you relax or remove a compile-time guard:**

1. If the now-passing case lives inside a `failed_*` file, delete that case from the file.
2. Drop the corresponding code from the `expect` line.
3. (Ideal) add a positive test elsewhere that exercises the now-legal form, ideally with AOT round-trip if codegen could break.

Example (2026-05-14): when the function-name C++-keyword guard was dropped, `def do(a : int)` moved from `tests/language/failed_reserved_names.das` (with `30163` dropped from expect) to a new `tests/aot/test_cpp_keyword_names.das` as a positive case (exercising interpreter + AOT + JIT).

The prefix is just convention so dastest and CMake know to skip them. `invalid_*` and `cant_*` behave identically to `failed_*`.

## Questions
- What's the convention for daslang tests that expect a compile error?
- How does the `expect` line work in dastest?
- Why do `failed_*.das` files in tests/ not get AOT-compiled?
- What does `expect 30106:3` mean in a daslang test file?
- Where in dastest are expected-failure tests skipped from AST serialization?
- How do I write a test that asserts the compiler emits a specific error code?
- What should I do with a `failed_*` test after I relax the guard it was checking?
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
slug: why-is-def-float-rejected-but-def-string-and-def-float2-allowed
title: Why is `def float(...)` rejected but `def string(...)` and `def float2(...)` allowed?
created: 2026-05-14
last_verified: 2026-05-14
links: []
---

The asymmetry is **not** in the parser. The grammar at `src/parser/ds2_parser.ypp:1316-1408` says:

```
function_name
: NAME // user identifiers
| DAS_OPERATOR ... // operator overloads
| das_type_name // any type-name TOKEN
;
```

`das_type_name` accepts every type-keyword token: `DAS_TBOOL`, `DAS_TSTRING`, `DAS_TINT`, `DAS_TINT2`, `DAS_TFLOAT`, `DAS_TFLOAT2`, `DAS_TDOUBLE`, etc. So at the parser level `def float`, `def int`, `def string`, `def float2`, `def double` all parse identically.

**The rejection is a post-parse lint guard at `src/ast/ast_lint.cpp:867-884`** (pre-2026-05-14):

```cpp
bool isValidFunctionName(const string & str) const {
return !isCppKeyword(str.c_str());
}
virtual void preVisit ( Function * fn ) override {
if (!isValidFunctionName(fn->name))
program->error("invalid function name " + fn->name, ..., CompilationError::invalid_function_name);
...
}
```

`isCppKeyword` (`src/builtin/module_builtin_ast.cpp:68-84`) is a hardcoded **C++**-keyword set. That set contains `float`, `int`, `double`, `bool`, `char`, `void`, `do`, `class`, `new`, `delete`, ... It does NOT contain `string` (that's `std::string` in C++ — qualified, not a keyword), nor `float2`/`int2`/`uint3`/etc. (daslang-specific typedefs, not C++ keywords). That's exactly the observed asymmetry.

The lint guard existed because the AOT C++ emitter would otherwise generate a literal `float(...)` C++ function which wouldn't compile.

**Status (2026-05-14, PR landed):** the function-name guard was dropped. `daslib/aot_cpp.das:223-267` (`aotSuffixNameEx`) now detects C++ keywords and applies the same `_Func` prefix it already used for operator-character names — so `def float` AOTs to `_Funcfloat_<hash>` (a valid C++ identifier). The other 5 `isCppKeyword` lint sites stay (module/enum/enum-value/struct/struct-field names) — those emit 1:1 into C++ and need the guard.

The 6 callsites that *still* reject C++ keywords (post-PR):
- `ast_lint.cpp:253` module name → error 30210
- `ast_lint.cpp:263` enumeration name → 30146
- `ast_lint.cpp:266` enumeration value → 30148
- `ast_lint.cpp:307` structure name → 30240
- `ast_lint.cpp:349` struct field / variable / arg name → 30152 / 30282 / 30106

`is_cpp_keyword(name : string) : bool` is exposed to daslang via `module_builtin_ast.cpp:1572-1573` if you ever need to call it from a macro or AOT emitter.

## Questions
- Why does `def float(...)` give error 30163 "invalid function name" but `def string(...)` doesn't?
- Where does daslang reject function names that are C++ keywords?
- Is the parser the gate for `def <type>` overloads, or is it the lint pass?
- Can I define a daslang function called `int` or `double`?
- What error code is "invalid function name" and where is it emitted?
1 change: 1 addition & 0 deletions src/ast/ast.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2869,6 +2869,7 @@ namespace das {
cexpr->values.push_back(val->clone());
}
cexpr->recordNames = recordNames;
cexpr->shorthandRecordNames = shorthandRecordNames;
if ( makeType ) {
cexpr->makeType = new TypeDecl(*makeType);
}
Expand Down
Loading
Loading