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
49 changes: 42 additions & 7 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ jobs:
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
matrix: ${{ steps.matrix.outputs.matrix }}
nightly: ${{ steps.nightly.outputs.nightly }}
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
Expand All @@ -50,6 +51,38 @@ jobs:
concurrent_skipping: 'same_content'
do_not_skip: '["pull_request", "workflow_dispatch", "release"]'

- id: nightly
env:
GH_TOKEN: ${{ github.token }}
run: |
set -eu
nightly=false
case "${{ github.event_name }}" in
schedule|workflow_dispatch) nightly=true ;;
pull_request)
if gh api graphql -f owner='${{ github.repository_owner }}' -f name='${{ github.event.repository.name }}' \
-F number=${{ github.event.pull_request.number }} -f query='
query($owner:String!,$name:String!,$number:Int!){
repository(owner:$owner,name:$name){
pullRequest(number:$number){
body
labels(first:100){nodes{name}}
closingIssuesReferences(first:20){nodes{labels(first:100){nodes{name}}}}
}}}' \
--jq '.data.repository.pullRequest
| [ (.labels.nodes[].name),
(.closingIssuesReferences.nodes[].labels.nodes[].name) ] as $tags
| if ($tags | index("nightly-failure"))
or ($tags | index("run-nightly"))
or (((.body // "") | test("#nightly"; "i")))
then "armed" else empty end' | grep -q .
then
nightly=true
fi
;;
esac
echo "nightly=$nightly" >> "$GITHUB_OUTPUT"

# The build matrix is data in ci/ci_matrix.py, evaluated per event here (ci/test_ci_matrix.py pins it).
- uses: actions/checkout@v4
with:
Expand All @@ -58,7 +91,9 @@ jobs:
# two statements: a substitution inside echo would hide the script's exit code
run: |
set -eu
matrix=$(python3 ci/ci_matrix.py build '${{ github.event_name }}')
event='${{ github.event_name }}'
if [ '${{ steps.nightly.outputs.nightly }}' = 'true' ]; then event=schedule; fi
matrix=$(python3 ci/ci_matrix.py build "$event")
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"

- name: Cache LLVM
Expand Down Expand Up @@ -394,7 +429,7 @@ jobs:
# per-PR ctest is -L small, so only this step executes a generated
# context. memory_model_4gb allocates a real 4 GB chunk and stays
# local-only.
if: matrix.cmake_preset == 'Release' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
if: matrix.cmake_preset == 'Release' && needs.pre_job.outputs.nightly == 'true'
run: |
set -eux
case "${{ matrix.target }}${{ matrix.architecture }}" in
Expand All @@ -419,7 +454,7 @@ jobs:
# LLVM-AOT objects, and both standalone tiers - -ctx emits C++ a host
# compiles, -lib emits a native library dasbind loads back. Same nightly
# gate as the step above; 32-bit Windows has no AOT rail to sweep.
if: matrix.cmake_preset == 'Release' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
if: matrix.cmake_preset == 'Release' && needs.pre_job.outputs.nightly == 'true'
run: |
set -eux
case "${{ matrix.target }}${{ matrix.architecture }}" in
Expand All @@ -444,7 +479,7 @@ jobs:
needs: pre_job
if: >-
(github.event_name == 'schedule' && github.repository == 'GaijinEntertainment/daScript')
|| github.event_name == 'workflow_dispatch'
|| (github.event_name != 'schedule' && needs.pre_job.outputs.nightly == 'true')
runs-on: windows-latest
permissions:
contents: read
Expand Down Expand Up @@ -548,7 +583,7 @@ jobs:
needs: pre_job
if: >-
(github.event_name == 'schedule' && github.repository == 'GaijinEntertainment/daScript')
|| github.event_name == 'workflow_dispatch'
|| (github.event_name != 'schedule' && needs.pre_job.outputs.nightly == 'true')
runs-on: windows-latest
permissions:
contents: read
Expand Down Expand Up @@ -754,7 +789,7 @@ jobs:
# scheduled cron runs the toolchains only on the canonical repo, so forks
# don't run (and fail) the nightly — that would email every fork owner.
# Manual workflow_dispatch still runs them anywhere.
if: (github.event_name == 'schedule' && github.repository == 'GaijinEntertainment/daScript') || github.event_name == 'workflow_dispatch'
if: (github.event_name == 'schedule' && github.repository == 'GaijinEntertainment/daScript') || (github.event_name != 'schedule' && needs.pre_job.outputs.nightly == 'true')
runs-on: windows-latest
defaults:
run:
Expand Down Expand Up @@ -911,7 +946,7 @@ jobs:
# scheduled cron runs the toolchains only on the canonical repo, so forks
# don't run (and fail) the nightly — that would email every fork owner.
# Manual workflow_dispatch still runs them anywhere.
if: (github.event_name == 'schedule' && github.repository == 'GaijinEntertainment/daScript') || github.event_name == 'workflow_dispatch'
if: (github.event_name == 'schedule' && github.repository == 'GaijinEntertainment/daScript') || (github.event_name != 'schedule' && needs.pre_job.outputs.nightly == 'true')
runs-on: windows-latest
steps:
- name: "SCM Checkout"
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/nightly_issue.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ jobs:
printf -- '* run: %s\n' "$RUN_URL"
printf -- '* commit: `%s`\n\n' "$GITHUB_SHA"
printf 'One issue for the whole nightly: while this one is open nothing else '
printf 'is filed, whichever lane fails. Close it once the nightly is green again.\n'
printf 'is filed, whichever lane fails. Close it once the nightly is green again.\n\n'
printf 'A PR that closes this issue runs the nightly shape of CI on itself, so the fix '
printf 'is proved by the lane that failed. Say `Closes #<this issue>` in the PR body '
printf 'before opening it, put `#nightly` anywhere in the body, or add the '
printf '`run-nightly` label and push again.\n'
} > "$BODY"

gh issue create --title "$TITLE" --label nightly-failure --body-file "$BODY"
48 changes: 46 additions & 2 deletions daslib/style_lint.das
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ class StyleLintVisitor : AstVisitor {
compile_time_errors : bool
comment_hygiene : bool = false
ascii_strings : bool = false
with_module_is_unsafe : bool = false
warning_count : int = 0
collect_warnings : bool = false
warnings : array<string>
Expand Down Expand Up @@ -2680,6 +2681,7 @@ class StyleLintVisitor : AstVisitor {
def override preVisitExprLet(expr : ExprLet?) : void {
//! Mirror `isLocalOrGlobal` check from ast_infer_type.cpp:4989 — `let v & = E` requires unsafe at statement level when `E` is non-local-non-temporary. Mark the let's frame so the enclosing `unsafe { ... }` block can detect it (STYLE025 must stay silent when narrowing would leave the let-ref binding unsatisfied).
if (expr.genFlags.generated) return
if (declaration_owns_unsafe(expr)) mark_unsafe_in_stack()
for (v in expr.variables) {
continue if (
v._type == null ||
Expand All @@ -2702,13 +2704,36 @@ class StyleLintVisitor : AstVisitor {
}

def override preVisitExprDelete(expr : ExprDelete?) : void {
if (!expr.genFlags.generated) mark_unsafe_in_stack()
if (expr.genFlags.generated) return
if (expr.subexpr == null || expr.subexpr._type == null || !expr.subexpr._type.isSafeToDelete) {
mark_unsafe_in_stack()
}
}

def is_lowered_unsafe_delete(expr : ExpressionPtr) : bool {
return expr is ExprCall && lowered_unsafe_delete(expr as ExprCall)
}

def variable_owns_unsafe(v : VariablePtr) : bool {
let vt = v._type
return false if (v.flags.generated || vt == null)
return ((!vt.flags.ref && (vt.hasClasses
|| (vt.isStructure && vt.structType != null && vt.structType.flags.isClass)))
|| (!vt.isLocal && !vt.isGoodBlockType)
|| (vt.flags.smartPtr && !v.flags.inScope)
|| (v.flags.inScope && vt.isPointer && vt.firstType != null
&& vt.firstType.isStructure && vt.firstType.structType != null
&& vt.firstType.structType.flags.isClass))
}

def declaration_owns_unsafe(expr : ExpressionPtr) : bool {
return false if (!(expr is ExprLet))
for (v in (expr as ExprLet).variables) {
return true if (variable_owns_unsafe(v))
}
return false
}

//! `unsafe(delete p)` does not parse, so a block holding a delete anywhere in it has no narrow
//! form to offer, whichever of its statements the one unsafe operation turns out to be.
def block_holds_delete(blk : ExprBlock?) : bool {
Expand All @@ -2734,7 +2759,11 @@ class StyleLintVisitor : AstVisitor {
def lowered_unsafe_delete(expr : ExprCall?) : bool {
return false if (expr.func == null || length(expr.arguments) != 1)
let fname = string(expr.func.name)
return (expr.func.flags.generated && fname == "finalize") || starts_with(fname, "builtin`finalize`")
let is_finalize = ((expr.func.flags.generated && fname == "finalize")
|| starts_with(fname, "builtin`finalize`") || starts_with(fname, "builtin`finalize_dim`"))
return false if (!is_finalize)
let at = expr.arguments[0]._type
return at == null || !at.isSafeToDelete
}

def override preVisitExprAddr(expr : ExprAddr?) : void {
Expand Down Expand Up @@ -2894,6 +2923,16 @@ class StyleLintVisitor : AstVisitor {
}
}

def is_module_with(expr : ExpressionPtr) : bool {
return expr is ExprWith && !empty((expr as ExprWith).moduleName)
}

def override preVisitExprWith(var expr : ExprWith?) : void {
if (with_module_is_unsafe && !expr.genFlags.generated && !empty(expr.moduleName)) {
mark_unsafe_in_stack()
}
}

def override visitExprUnsafe(var expr : ExprUnsafe?) : ExpressionPtr {
let n = length(unsafe_block_stack)
if (n > 0) {
Expand All @@ -2913,6 +2952,8 @@ class StyleLintVisitor : AstVisitor {
blk.list[0] is ExprNew ||
blk.list[0] is ExprClone ||
is_lowered_unsafe_clone(blk.list[0]) ||
declaration_owns_unsafe(blk.list[0]) ||
is_module_with(blk.list[0]) ||
block_holds_delete(blk))
&& !frame.has_non_local_let_ref) {
var narrowable = true
Expand Down Expand Up @@ -2998,6 +3039,7 @@ def public style_lint(prog : ProgramPtr; compile_time_errors : bool; disabled_co
astVisitor.max_complexity = max_complexity_for(prog)
astVisitor.max_function_length = max_function_length_for(prog)
astVisitor.ascii_strings = ascii_strings_for(prog)
astVisitor.with_module_is_unsafe = (prog._options |> find_arg("with_module_is_unsafe")) ?as tBool ?? false
astVisitor.this_module = prog.getThisModule
let empty_enabled : table<string>
astVisitor.analyze_requires = astVisitor.this_module != null && require_analysis_enabled(disabled_codes, empty_enabled)
Expand Down Expand Up @@ -3037,6 +3079,7 @@ def public style_lint_collect(prog : ProgramPtr; var warnings : array<string>;
astVisitor.max_complexity = max_complexity_for(prog)
astVisitor.max_function_length = max_function_length_for(prog)
astVisitor.ascii_strings = ascii_strings_for(prog)
astVisitor.with_module_is_unsafe = (prog._options |> find_arg("with_module_is_unsafe")) ?as tBool ?? false
astVisitor.this_module = prog.getThisModule
astVisitor.analyze_requires = astVisitor.this_module != null && require_analysis_enabled(disabled_codes, enabled_codes)
make_visitor(*astVisitor) $(astVisitorAdapter) {
Expand Down Expand Up @@ -3068,6 +3111,7 @@ def public style_lint_collect_issues(prog : ProgramPtr; var issues : array<LintI
astVisitor.max_complexity = max_complexity_for(prog)
astVisitor.max_function_length = max_function_length_for(prog)
astVisitor.ascii_strings = ascii_strings_for(prog)
astVisitor.with_module_is_unsafe = (prog._options |> find_arg("with_module_is_unsafe")) ?as tBool ?? false
astVisitor.this_module = prog.getThisModule
astVisitor.analyze_requires = astVisitor.this_module != null && require_analysis_enabled(disabled_codes, enabled_codes)
make_visitor(*astVisitor) $(astVisitorAdapter) {
Expand Down
35 changes: 23 additions & 12 deletions dastest/dastest.das
Original file line number Diff line number Diff line change
Expand Up @@ -425,13 +425,9 @@ def deserialize_path(var ctx : SuiteCtx, _files : array<string>, in_file : strin
return
}
var count = 0
unsafe {
_builtin_read(f, addr(count), typeinfo sizeof(count))
}
_builtin_read(f, unsafe(addr(count)), typeinfo sizeof(count))
var sz = 0l
unsafe {
_builtin_read(f, addr(sz), typeinfo sizeof(sz))
}
_builtin_read(f, unsafe(addr(sz)), typeinfo sizeof(sz))
var data : array<uint8>
data |> reserve(sz) // the blob size is known: exact reserve, and the resize never grows
data |> resize(sz)
Expand All @@ -441,9 +437,9 @@ def deserialize_path(var ctx : SuiteCtx, _files : array<string>, in_file : strin
// the reader's access: a stream carries none, and a program's late require takes its program's
var inscope access <- make_file_access(ctx.projectPath)
access |> add_file_access_root("dastest", ctx.dastestRoot)
using() $(var _mg : ModuleGroup) {
using() $(var mg : ModuleGroup) {
for (i in range(count)) {
deserialize_program(ser, access) $(ok, program, error) {
deserialize_program(ser, access, unsafe(addr(mg))) $(ok, program, error) {
if (!ok) {
log::error("Deserialization failed at program {i}: {error}")
res.errors++
Expand Down Expand Up @@ -550,9 +546,7 @@ def main() : int { // nolint:STYLE037,STYLE038 - CLI dispatch, one arm per mode
log::error("dastest: the suite ran past its --timeout of {timeout}s; the process ends here with exit code {TIMEOUT_EXIT_CODE} and no summary")
// not fio::exit - it runs the job queue teardown, which busy-waits on this very thread;
// and nothing here invokes into the main context, which the main thread is still running
unsafe {
fio::exit_now(TIMEOUT_EXIT_CODE)
}
unsafe(fio::exit_now(TIMEOUT_EXIT_CODE))
}
}
}
Expand All @@ -561,6 +555,23 @@ def main() : int { // nolint:STYLE037,STYLE038 - CLI dispatch, one arm per mode
var inputPaths := test_args.test_files
var files : array<string>
return 1 if (!collect_files(inputPaths, files))
if (!empty(test_args.test_list)) {
var keep : table<string>
fopen(test_args.test_list, "rb") $(fr) {
return if (fr == null)
fmap(fr) $(data) {
for (line in string(data) |> split("\n")) {
let one = line |> strip() |> replace("\\", "/")
keep |> insert(one) if (!empty(one))
}
}
}
if (keep |> empty()) {
log::error("dastest: --test-list {test_args.test_list} is empty or unreadable")
return 1
}
files |> erase_if() $(f) => !(keep |> key_exists(f |> replace("\\", "/")))
}
if (!empty(test_args.exclude_files)) {
files |> erase_if() $(f) {
let base = base_name(f)
Expand Down Expand Up @@ -692,7 +703,7 @@ def main() : int { // nolint:STYLE037,STYLE038 - CLI dispatch, one arm per mode
bSingles |> push(build_iso_cmd([clone_string(file)], cmdPrefix, cmdSuffix))
}
let bCmd = build_iso_cmd(bFiles, cmdPrefix, cmdSuffix)
unsafe { delete bFiles }
delete bFiles
batches |> emplace(IsoInput(uris <- bUris, batchCmd = clone_string(bCmd), singleCmds <- bSingles))
bidx = hi
}
Expand Down
4 changes: 4 additions & 0 deletions dastest/dastest_clargs.das
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ struct DastestArgs {
@clarg_doc = "Skip test files whose base name contains this substring (repeatable)"
exclude_files : array<string>

@clarg_name = "test-list"
@clarg_doc = "Path to a file listing one test path per line; restricts the collected set to those paths"
test_list : string

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

Expand Down
5 changes: 5 additions & 0 deletions doc/source/reference/language/lint.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2830,6 +2830,11 @@ unsafe, the block scope is too broad. Narrow it to the expression form
``unsafe(<sub-expr>)`` wrapping just the operation that requires it. When two
or more statements need unsafe the block is justified and stays silent.

The rule also stays silent where no narrow form exists. A declaration owns its
own permission and ``unsafe()`` wraps an expression, so a stack-constructed
class local, a ``smart_ptr`` local without ``inscope``, and an ``inscope``
local whose generated delete is unsafe all keep the block.

.. das-doc: alt
.. code-block:: das

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Returns whether a value of the given type is safe to delete, meaning that deleting it does not require an unsafe block. Raw pointers to a typed value, blocks and lambdas are never safe to delete, and a structure, tuple, variant, array or table is safe only when everything it holds is.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Deserializes the next program from the stream and calls ``block`` with it, the way the
three-argument form does, and gives the restored program ``access`` as its own: a stream carries
no file access, and a ``require_module_now`` issued from the program's macros or ``[init]``
walks through its program's access, so a reader that restores programs whose code requires
modules late passes the access it would have compiled them with. ``libGroup`` is the module
group the restored program finalizes its annotations against, the way ``compile_file`` takes
one: a function or block annotation's ``finish`` is handed this group, so it must be the
caller's own rather than an empty stand-in.
6 changes: 2 additions & 4 deletions examples/debugapi/allocation_tracking.das
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,8 @@ def main() {

// 4. Print summary — tracker lives in the agent context,
// so we invoke our helper there via the named agent
unsafe {
invoke_in_context(get_debug_agent_context("alloc_tracker"),
"print_tracker_summary")
}
unsafe(invoke_in_context(get_debug_agent_context("alloc_tracker"),
"print_tracker_summary"))

delete_debug_agent_context("alloc_tracker")
}
Loading
Loading