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
6 changes: 6 additions & 0 deletions .github/workflows/wasm_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ name: wasm_build

on:
push:
branches: [master]
pull_request:
workflow_dispatch:

Expand Down Expand Up @@ -122,3 +123,8 @@ jobs:
cd cmake_temp
cmake -DCMAKE_BUILD_TYPE:STRING=${{ matrix.cmake_preset }} -G Ninja -DCMAKE_TOOLCHAIN_FILE=../emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake ../
ninja

- name: "Test: hello world via Node.js"
run: |
cd web
node test/dastest_wasm.js ../ ./output
2 changes: 1 addition & 1 deletion daslib/aot_cpp.das
Original file line number Diff line number Diff line change
Expand Up @@ -2935,7 +2935,7 @@ class public CppAot : AstVisitor {
tab ++;
if (!expr.makeStructFlags.isNewHandle) {
if (!needTempSrc(expr)) {
let expr_type = describeCppType(expr._type, DescribeConfig(skip_ref = true, cross_platform = cross_platform));
let expr_type = describeCppType(expr._type, DescribeConfig(skip_ref = true, skip_const = true, cross_platform = cross_platform));
write(*ss, "{tabs()}{expr_type} {mksName(expr)}");
if (expr.constructor != null) {
write(*ss, " = ");
Expand Down
15 changes: 8 additions & 7 deletions daslib/option.das
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ require daslib/typemacro_boost

//! Tagged ``some``/``none`` pair over payload type ``T``.
//!
//! When ``_has_value`` is ``false``, ``value`` holds the zero value of ``T`` and
//! must not be read. Accessor functions should be used for reads/writes.
[template_structure(T), safe_when_uninitialized]
//! Resolves to ``tuple<_has_value : bool; _value : T>`` — a structural type with
//! no module home, so ``Option<T>`` reached through any chain of ``require ...
//! public`` collapses to one canonical ``tTuple`` candidate (issue #2598).
//! When ``_has_value`` is ``false``, ``_value`` holds the zero value of ``T``
//! and must not be read. Use the accessor functions below for reads / writes.
[template_tuple(T)]
struct template Option {
_has_value : bool = false
@safe_when_uninitialized
_value : T
@safe_when_uninitialized _value : T
}

//! Construct ``Option<T>`` carrying ``v``. Payload type is inferred from ``v``.
Expand All @@ -55,8 +57,7 @@ def some(v : auto(TT)) {
//! clone; for workhorse types (``int``, ``float``, ``string``, …) ``some`` is
//! equivalent and cheaper to read.
def move_some(var v : auto(TT)) {
typedef OT = $Option < TT - const >
return <- OT(_has_value = true, _value <- v)
return <- (_has_value = true, _value <- v)
}

//! Construct an empty ``Option<T>``. Pass the payload type as a witness::
Expand Down
25 changes: 12 additions & 13 deletions daslib/result.das
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,18 @@ require daslib/option public

//! Tagged ``ok``/``err`` pair over payload ``T`` and error ``E``.
//!
//! When ``is_ok`` is ``true``, ``value`` is meaningful and ``error`` holds the zero
//! value of ``E``. When ``is_ok`` is ``false``, ``error`` is meaningful and ``value``
//! holds the zero value of ``T``. Prefer the accessor functions below over direct
//! field reads.
[template_structure(T, E)]
//! Resolves to ``tuple<_is_ok : bool; _value : T; _error : E>`` — a structural
//! type with no module home, so ``Result<T, E>`` reached through any chain of
//! ``require ... public`` collapses to one canonical ``tTuple`` candidate
//! (issue #2598). When ``is_ok`` is ``true``, ``value`` is meaningful and
//! ``error`` holds the zero value of ``E``; when ``is_ok`` is ``false``,
//! ``error`` is meaningful and ``value`` holds the zero value of ``T``. Prefer
//! the accessor functions below over direct field reads.
[template_tuple(T, E)]
struct template Result {
_is_ok : bool = false
@safe_when_uninitialized
_value : T
@safe_when_uninitialized
_error : E
@safe_when_uninitialized _value : T
@safe_when_uninitialized _error : E
}

//! Construct ``Result<T, E>`` carrying successful ``v``. Pass the error type as a witness::
Expand Down Expand Up @@ -64,8 +65,7 @@ def ok(v : auto(TT); etype : auto(EE)) {
//! payloads where ``ok`` would clone.
[template(etype)]
def move_ok(var v : auto(TT); etype : auto(EE)) {
typedef RT = $Result < TT - const; EE - const >
return <- RT(_is_ok = true, _value <- v)
return <- (_is_ok = true, _value <- v, _error = default<EE>)
}

//! Construct ``Result<T, E>`` carrying error ``e``. Pass the value type as a witness::
Expand Down Expand Up @@ -93,8 +93,7 @@ def err(e : auto(EE); ttype : auto(TT)) {
//! payloads where ``err`` would clone.
[template(ttype)]
def move_err(var e : auto(EE); ttype : auto(TT)) {
typedef RT = $Result < TT - const; EE - const >
return <- RT(_is_ok = false, _error <- e)
return <- (_is_ok = false, _value = default<TT>, _error <- e)
}

//! ``true`` iff the result is a successful ``ok``.
Expand Down
128 changes: 128 additions & 0 deletions daslib/typemacro_boost.das
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,134 @@ class TemplateStructure : AstStructureAnnotation {
}
}

[structure_macro(name="template_tuple")]
class TemplateTuple : AstStructureAnnotation {
//! Like ``[template_structure]``, but produces a typemacro that returns a
//! named tuple instead of cloning a structure. Result is structural — no
//! module home, no per-consumer cloning. Use for sum-type / discriminated-
//! pair shapes (``Option``, ``Result``) where structural identity across
//! transitive ``require ... public`` is the desired behavior (issue #2598).
//!
//! Field types may be arbitrary type expressions referencing the template
//! arguments — ``T``, ``array<T>``, ``tuple<int; T; E>``, etc. The runtime
//! body uses ``apply_template`` (templates_boost) to walk each field's
//! TypeDecl and substitute alias slots with the corresponding typemacro arg.
//!
//! ``@safe_when_uninitialized`` on a source field propagates to the
//! corresponding tuple element's ``TypeDeclFlags.safeWhenUninitialized``,
//! so ``default<Foo<T>>`` succeeds even when that field's payload type is
//! itself unsafe-when-uninitialized. The tuple's recursive ``unsafeInit``
//! walks its element types, so per-element marking gives the same effect
//! as the legacy struct-level ``[safe_when_uninitialized]`` did.
def override apply(var st : StructurePtr; var group : ModuleGroup; args : AnnotationArgumentList; var errors : das_string) : bool {
if (length(args) == 0) {
errors := "expecting at least one template argument name"
return false
}
var argNames : array<string>
var ctxFnArguments : array<VariablePtr>
for (arg in args) {
if (arg.basicType != Type.tBool) {
errors := "template_tuple only supports type-arg names; got '{arg.name}' of {arg.basicType}"
return false
}
argNames |> push(string(arg.name))
var argVar <- new Variable(name := string(arg.name), at = st.at)
argVar._type = new TypeDecl(baseType = Type.alias, alias := "TypeDeclPtr")
ctxFnArguments.emplace(argVar)
}
let typeMacroFunctionName = string(st.name)
let nFields = length(st.fields)
// Alias TypeDecl for the source struct — at typemacro-execution time
// ``typeinfo ast_typedecl(type<$t(aliasT)>)`` resolves through the
// module-scoped alias to the source struct, giving us its field list
// (with template aliases like ``T`` / ``array<T>`` preserved).
var aliasT = new TypeDecl(baseType = Type.alias, alias := string(st.name))
// Per-field argName checks — guard the generic path before the
// recursive unifier runs.
var perFieldArgNameChecks : array<ExpressionPtr>
for (i, fld in iter_range(st.fields), st.fields) {
let fname = string(fld.name)
perFieldArgNameChecks |> push(qmacro_block() {
if (passArgument.argNames[$v(i)] != $v(fname)) {
return null
}
})
}
// Rules-block statements — substitute each template-arg alias
// (``T``, ``E``, ...) with the runtime arg. The same rules drive
// both paths: in the concrete path the runtime arg is a fully
// resolved type (``int``), and in the generic path it's
// ``auto(<caller-name>)`` — exactly the binding-slot the recursive
// unifier (``inferGenericType`` / ``updateAliasMap``) needs.
var rulesStmts : array<ExpressionPtr>
for (an in argNames) {
rulesStmts |> push(qmacro_expr() {
rules |> replaceTypeWithTypeDecl($v(an), clone_type($i(an)));
})
}
var typeMacroFunction = qmacro_function(typeMacroFunctionName) $(macroArgument, passArgument : TypeDeclPtr; $a(ctxFnArguments)) : TypeDeclPtr {
if (passArgument != null) {
if (passArgument.baseType != Type.tTuple) {
return null
}
if (length(passArgument.argTypes) != $v(nFields)) {
return null
}
if (length(passArgument.argNames) != $v(nFields)) {
return null
}
$b(perFieldArgNameChecks)
// Build (argument_type, inferred_type) pairs from the source
// struct's fields and let ``infer_template_types`` run the
// standard unifier loop. ``apply_template`` substitutes each
// alias (``T``, ``E``, ...) with the runtime arg — concrete
// type when caller passed one, ``auto(<caller-name>)`` in
// generic context. Either form is what
// ``infer_template_types`` expects in ``argument_type``.
var src_type_g = typeinfo ast_typedecl(type<$t(aliasT)>)
var src_g = src_type_g.structType
var template_arguments : array<TypeMacroTemplateArgument>
for (i, fld in iter_range(src_g.fields), src_g.fields) {
var fldType <- clone_type(fld._type)
var fldAuto <- apply_template(fld.at, fldType) <| $(var rules : Template) {
$b(rulesStmts)
}
template_arguments |> push(TypeMacroTemplateArgument(
name = string(fld.name),
argument_type = fldAuto,
inferred_type = clone_type(passArgument.argTypes[i])))
}
return <- infer_template_types(passArgument, template_arguments)
}
// Concrete path: walk the source struct's fields and substitute
// template aliases with the runtime args via templates_boost's
// existing TypeDecl visitor — handles array<T>, tuple<int; T>, etc.
var src_type = typeinfo ast_typedecl(type<$t(aliasT)>)
var src = src_type.structType
var tt = new TypeDecl(baseType = Type.tTuple, at = macroArgument.at)
tt.argNames |> resize($v(nFields))
for (i, fld in iter_range(src.fields), src.fields) {
var fldType <- clone_type(fld._type)
var elt <- apply_template(fld.at, fldType) <| $(var rules : Template) {
$b(rulesStmts)
}
tt.argTypes |> emplace_new <| elt
tt.argNames[i] := string(fld.name)
if (!(find_arg(fld.annotation, "safe_when_uninitialized") is nothing)) {
tt.argTypes[i].flags |= TypeDeclFlags.safeWhenUninitialized
}
}
return <- tt
}
append_annotation(typeMacroFunction, "typemacro_boost", "typemacro_function", [])
if (!(compiling_module() |> add_function(typeMacroFunction))) {
panic("can't add function {typeMacroFunctionName}")
}
return true
}
}

[macro_function]
def public is_custom_work_done(structType : Structure?) : bool {
//! Returns true if custom work has already been performed on the template structure.
Expand Down
7 changes: 7 additions & 0 deletions dastest/dastest.das
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require daslib/ast_boost
require daslib/json_boost
require daslib/jobque_boost
require daslib/rtti
require daslib/strings_boost

require fs
require suite
Expand Down Expand Up @@ -328,6 +329,12 @@ def main() : int {
if (!collect_files(inputPaths, files)) {
return 1
}
if (!empty(test_args.exclude_files)) {
files |> erase_if() $(f) {
let base = base_name(f)
return (find_index_if(test_args.exclude_files) $(pat) => base |> contains(pat)) >= 0
}
}

timingOutliers = test_args.timing_outliers
jsonFilePath = test_args.json_file
Expand Down
4 changes: 4 additions & 0 deletions dastest/dastest_clargs.das
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ struct DastestArgs {
@clarg_doc = "Path to the folder with scripts or single script to test"
test_files : array<string>

@clarg_name = "exclude"
@clarg_doc = "Skip test files whose base name contains this substring (repeatable)"
exclude_files : array<string>

@clarg_doc = "Run top-level tests matching this name prefix"
test_names : array<string>

Expand Down
1 change: 1 addition & 0 deletions include/daScript/ast/ast_typedecl.h
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ namespace das {
bool explicitRef : 1;
bool isPrivateAlias : 1; // this is a private alias. only matters in the context of module aliasTypes (for now)
bool autoToAlias : 1; // this allows conversion of auto to alias
bool safeWhenUninitialized : 1; // exempt this type from "Uninitialized variable/field is unsafe" — analog of struct-level safe_when_uninitialized for tuples / variants / aliases
};
uint32_t flags = 0;
};
Expand Down
41 changes: 20 additions & 21 deletions modules/dasSQLITE/daslib/sqlite_boost.das
Original file line number Diff line number Diff line change
Expand Up @@ -2203,20 +2203,23 @@ def is_option_field_type(td : TypeDeclPtr) : bool {
if (td.baseType == Type.typeMacro && length(td.dimExpr) > 0 && (td.dimExpr[0] is ExprConstString)) {
return (td.dimExpr[0] as ExprConstString).value == "Option"
}
// Post-instantiation form: tStructure named `Option<T>` (the [template_structure] machinery stamps
// the type-arg into the struct name). Templates instantiated downstream show _module=<callee>, not
// the original "option" module — match on the name shape instead.
if (td.baseType == Type.tStructure && td.structType != null) {
let sname = string(td.structType.name)
if (sname == "Option" || sname |> starts_with("Option<")) {
return true
}
// Post-instantiation form: structural named tuple ``tuple<_has_value:bool; _value:T>``.
// Option is built by daslib/option's typemacro_function as a structural tuple — no
// module home, no Structure clone. Match the canonical shape.
if (td.baseType == Type.tTuple
&& length(td.argTypes) == 2
&& length(td.argNames) == 2
&& td.argNames[0] == "_has_value"
&& td.argNames[1] == "_value"
&& td.argTypes[0].baseType == Type.tBool) {
return true
}
return false
}

def unwrap_option_payload_type(var td : TypeDeclPtr) : TypeDeclPtr {
// Unwrap Option<T> → T (or td unchanged); typeMacro carries T in dimExpr[1], structType in `_value`.
// Unwrap Option<T> → T (or td unchanged); typeMacro carries T in dimExpr[1], the
// structural tuple form carries T in argTypes[1] (the `_value` slot).
if (td == null) {
return td
}
Expand All @@ -2232,18 +2235,14 @@ def unwrap_option_payload_type(var td : TypeDeclPtr) : TypeDeclPtr {
}
return clone_type((td.dimExpr[1] as ExprTypeDecl).typeexpr)
}
// Post-instantiation form: tStructure named `Option` or `Option<T>`. 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)
}
}
}
// Post-instantiation form: structural named tuple ``tuple<_has_value:bool; _value:T>``.
if (td.baseType == Type.tTuple
&& length(td.argTypes) == 2
&& length(td.argNames) == 2
&& td.argNames[0] == "_has_value"
&& td.argNames[1] == "_value"
&& td.argTypes[0].baseType == Type.tBool) {
return clone_type(td.argTypes[1])
}
return td
}
Expand Down
39 changes: 30 additions & 9 deletions src/ast/ast_infer_type.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4479,16 +4479,26 @@ namespace das {
} else {
auto clashAlias = findAlias(name);
if (clashAlias) {
string extra;
if (verbose) {
auto atClash = clashAlias->getDeclarationLocation();
if (!atClash.empty()) {
extra = "previously declarated at " + atClash.describe();
// Silent dedupe: if `assume X : T` redeclares an alias that already
// resolves to the exact same type, treat it as a no-op. This covers
// the auto-injected `assume TT : T` placed at the start of a generic
// instance body when an outer scope (e.g. a typemacro-expanded
// result type carrying ``int aka TT``) already binds TT to T.
if (expr->assumeType && expr->assumeType->isSameType(*clashAlias,
RefMatters::no, ConstMatters::no, TemporaryMatters::no)) {
// fall through — the duplicate is harmless
} else {
string extra;
if (verbose) {
auto atClash = clashAlias->getDeclarationLocation();
if (!atClash.empty()) {
extra = "previously declared at " + atClash.describe();
}
}
error("can't assume " + name + ", type or alias name is already used", extra, "",
expr->at, CompilationError::already_declared_assume_alias);
return;
}
error("can't assume " + name + ", type or alias name is already used", extra, "",
expr->at, CompilationError::already_declared_assume_alias);
return;
}
if (!expr->assumeType) {
error("assume without subexpression must have type", "", "",
Expand Down Expand Up @@ -4562,7 +4572,18 @@ namespace das {
}
assume.push_back(AssumeEntry{expr, move(collector.vars)});
} else {
assumeType.emplace_back(expr);
// Skip recording when preVisit silently deduped a same-type
// re-assume — keeps assumeType free of self-shadowing duplicates
// that would otherwise add findAlias lookup cost and confuse
// diagnostics.
auto existing = findAlias(expr->alias);
if (existing && expr->assumeType && expr->assumeType->isSameType(*existing,
RefMatters::no, ConstMatters::no, TemporaryMatters::no)) {
// already bound to the same type — preVisit let it through,
// and we leave assumeType untouched so the original wins.
} else {
assumeType.emplace_back(expr);
}
}
return expr;
}
Expand Down
Loading
Loading