Files
workspace-info/tools/generate_cpp_y2s1.py
T

676 lines
39 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Generate the ten C++20 cards for Year 2, semester 1.
The lesson scope comes from /home/user/dev/keys/cxx.md. The generated cards
share the same runtime, React viewer and PDF contract as the existing STEM
cards, while every task contains an independently compilable C++ experiment.
"""
from __future__ import annotations
import json
import os
from pathlib import Path
import shutil
import stat
import textwrap
import uuid
ROOT = Path(__file__).resolve().parents[3]
OUTPUT = ROOT / "series" / "cpp"
TEMPLATE = ROOT / "series" / "console" / "lab-console-busybox"
UUID_NAMESPACE = uuid.UUID("1b43af4e-354f-51dc-a42a-67593c614624")
def clean(text: str) -> str:
return textwrap.dedent(text).lstrip()
LESSONS = [
{
"id": "CPP01",
"number": "01",
"repo": "lab-cpp-build-namespaces",
"slug": "build-namespaces",
"title": "Od C do C++: oddzielna kompilacja i przestrzenie nazw",
"topic": "Deklaracja, definicja, translation unit, linkage i namespace",
"chapter": "A Tour of C++ 3e: rozdziały 1 i 3",
"mission": "Zbudować program z wielu jednostek translacji i odczytać granice nazw oraz linkage.",
"tasks": [
("Deklaracja i definicja", "Rozdziel deklarację funkcji, definicję i punkt wejścia między trzy pliki.", "sum=7", {
"calculator.hpp": """#pragma once
namespace lab { int add(int left, int right); }
""",
"calculator.cpp": """#include "calculator.hpp"
namespace lab { int add(int left, int right) { return left + right; } }
""",
"main.cpp": """#include "calculator.hpp"
#include <iostream>
int main() { std::cout << "sum=" << lab::add(3, 4) << '\\n'; }
""",
}),
("Linkage zewnętrzny i wewnętrzny", "Porównaj symbol dostępny między plikami z nazwą ukrytą w anonimowej przestrzeni nazw.", "external=5 internal_bias_result=8", {
"api.hpp": """#pragma once
int global_value();
int shifted_value();
""",
"global.cpp": """#include "api.hpp"
int global_value() { return 5; }
""",
"shifted.cpp": """#include "api.hpp"
namespace { constexpr int bias = 3; }
int shifted_value() { return global_value() + bias; }
""",
"main.cpp": """#include "api.hpp"
#include <iostream>
int main() { std::cout << "external=" << global_value() << " internal_bias_result=" << shifted_value() << '\\n'; }
""",
}),
("Kwalifikacja przestrzeni nazw", "Użyj dwóch funkcji o tej samej nazwie bez kolizji i wywołaj je kwalifikowaną nazwą.", "sensor=21 network=204", {
"main.cpp": """#include <iostream>
namespace sensor { int read() { return 21; } }
namespace network { int read() { return 204; } }
int main() { std::cout << "sensor=" << sensor::read() << " network=" << network::read() << '\\n'; }
""",
}),
],
},
{
"id": "CPP02", "number": "02", "repo": "lab-cpp-types-initialization", "slug": "types-initialization",
"title": "Typy, inicjalizacja, auto, const i enum class",
"topic": "Bezpieczna inicjalizacja i typy definiowane przez użytkownika",
"chapter": "A Tour of C++ 3e: rozdziały 1 i 2",
"mission": "Rozpoznać typy wyrażeń i używać inicjalizacji, const oraz enum class bez niejawnego mieszania domen.",
"tasks": [
("Inicjalizacja i auto", "Zastosuj inicjalizację klamrową i potwierdź typ dedukowany przez auto.", "count=4 twice=8 type=int", {
"main.cpp": """#include <iostream>
#include <type_traits>
int main() {
int count{4};
auto twice = count * 2;
static_assert(std::is_same_v<decltype(twice), int>);
std::cout << "count=" << count << " twice=" << twice << " type=int\\n";
}
""",
}),
("const i constexpr", "Oddziel wartość obliczaną w czasie kompilacji od obiektu tylko do odczytu.", "square=49 limit=12", {
"main.cpp": """#include <iostream>
constexpr int square(int value) { return value * value; }
int main() {
constexpr int result = square(7);
const int limit{12};
static_assert(result == 49);
std::cout << "square=" << result << " limit=" << limit << '\\n';
}
""",
}),
("enum class i struktura", "Zbuduj typ rekordu oraz silnie typowany stan bez niejawnej konwersji do int.", "sample=23 state=ready", {
"main.cpp": """#include <iostream>
enum class State { idle, ready };
struct Sample { int value; State state; };
int main() {
Sample sample{23, State::ready};
std::cout << "sample=" << sample.value << " state=" << (sample.state == State::ready ? "ready" : "idle") << '\\n';
}
""",
}),
],
},
{
"id": "CPP03", "number": "03", "repo": "lab-cpp-pointers-references", "slug": "pointers-references",
"title": "Wskaźniki, referencje, tablice i nullptr",
"topic": "Tożsamość obiektu, dostęp pośredni i przekazywanie argumentów",
"chapter": "A Tour of C++ 3e: sekcje 1.7 i 3.4",
"mission": "Odróżnić wskaźnik od referencji i tablicę od wskaźnika podczas przekazywania danych.",
"tasks": [
("Wskaźnik i nullptr", "Sprawdź brak obiektu przed dereferencją, a następnie zmodyfikuj wskazywaną wartość.", "null=1 value=9", {
"main.cpp": """#include <iostream>
int main() {
int value{7};
int* pointer = nullptr;
const bool was_null = pointer == nullptr;
pointer = &value;
*pointer += 2;
std::cout << "null=" << was_null << " value=" << value << '\\n';
}
""",
}),
("Referencja jako alias", "Przekaż dwa obiekty przez referencję i zamień ich wartości.", "left=8 right=3", {
"main.cpp": """#include <iostream>
void swap_values(int& left, int& right) { const int temp = left; left = right; right = temp; }
int main() { int left{3}; int right{8}; swap_values(left, right); std::cout << "left=" << left << " right=" << right << '\\n'; }
""",
}),
("Tablica i jej rozmiar", "Policz elementy w referencji do tablicy, zanim argument zaniknie do wskaźnika.", "count=4 sum=20", {
"main.cpp": """#include <cstddef>
#include <iostream>
template <std::size_t N> int sum(const int (&values)[N]) { int result{}; for (int value : values) result += value; return result; }
int main() { int values[]{2, 4, 6, 8}; std::cout << "count=" << (sizeof(values) / sizeof(values[0])) << " sum=" << sum(values) << '\\n'; }
""",
}),
],
},
{
"id": "CPP04", "number": "04", "repo": "lab-cpp-functions-overloading", "slug": "functions-overloading",
"title": "Funkcje, przeciążanie, wartości zwracane i modułowość",
"topic": "Sygnatura funkcji, wybór przeciążenia, zakres i granica modułu",
"chapter": "A Tour of C++ 3e: rozdziały 1 i 3",
"mission": "Zaprojektować mały interfejs funkcji i sprawdzić wybór przeciążenia oraz zwracanie wartości.",
"tasks": [
("Przeciążanie", "Wywołaj dwie funkcje o tej samej nazwie i różnych typach parametrów.", "int=6 double=3.5", {
"main.cpp": """#include <iostream>
int twice(int value) { return value * 2; }
double twice(double value) { return value * 2.0; }
int main() { std::cout << "int=" << twice(3) << " double=" << twice(1.75) << '\\n'; }
""",
}),
("Zwracanie przez wartość", "Zwróć kompletny wynik obliczenia jako wartość, bez ujawniania obiektu lokalnego.", "quotient=3 remainder=2", {
"main.cpp": """#include <iostream>
struct Division { int quotient; int remainder; };
Division divide(int value, int by) { return {value / by, value % by}; }
int main() { const Division result = divide(17, 5); std::cout << "quotient=" << result.quotient << " remainder=" << result.remainder << '\\n'; }
""",
}),
("Interfejs modułu", "Oddziel publiczne deklaracje statystyk od ich implementacji.", "min=2 max=9", {
"stats.hpp": """#pragma once
namespace stats { int minimum(int left, int right); int maximum(int left, int right); }
""",
"stats.cpp": """#include "stats.hpp"
namespace stats { int minimum(int left, int right) { return left < right ? left : right; } int maximum(int left, int right) { return left > right ? left : right; } }
""",
"main.cpp": """#include "stats.hpp"
#include <iostream>
int main() { std::cout << "min=" << stats::minimum(9, 2) << " max=" << stats::maximum(9, 2) << '\\n'; }
""",
}),
],
},
{
"id": "CPP05", "number": "05", "repo": "lab-cpp-classes-objects", "slug": "classes-objects",
"title": "Klasa i obiekt: interfejs oraz implementacja",
"topic": "Pola, metody, kontrola dostępu i zachowanie obiektu",
"chapter": "A Tour of C++ 3e: rozdziały 2 i 5",
"mission": "Zamknąć stan i operacje w klasie oraz oddzielić interfejs publiczny od implementacji.",
"tasks": [
("Klasa w dwóch plikach", "Zdefiniuj interfejs licznika w nagłówku, a metody w osobnej jednostce translacji.", "counter=3", {
"counter.hpp": """#pragma once
class Counter { public: explicit Counter(int start); void increment(); int value() const; private: int value_; };
""",
"counter.cpp": """#include "counter.hpp"
Counter::Counter(int start) : value_{start} {}
void Counter::increment() { ++value_; }
int Counter::value() const { return value_; }
""",
"main.cpp": """#include "counter.hpp"
#include <iostream>
int main() { Counter counter{2}; counter.increment(); std::cout << "counter=" << counter.value() << '\\n'; }
""",
}),
("Stan prywatny", "Udostępnij operacje rachunku bez publicznego zapisu pola balance.", "accepted=1 rejected=1 balance=15", {
"main.cpp": """#include <iostream>
class Account {
public:
explicit Account(int balance) : balance_{balance} {}
bool deposit(int amount) { if (amount <= 0) return false; balance_ += amount; return true; }
int balance() const { return balance_; }
private: int balance_;
};
int main() { Account account{10}; const bool accepted = account.deposit(5); const bool rejected = !account.deposit(-2); std::cout << "accepted=" << accepted << " rejected=" << rejected << " balance=" << account.balance() << '\\n'; }
""",
}),
("Metody opisują zachowanie", "Przenieś obliczenie pola i skalowanie do obiektu prostokąta.", "area=24 scaled=96", {
"main.cpp": """#include <iostream>
class Rectangle { public: Rectangle(int width, int height) : width_{width}, height_{height} {} int area() const { return width_ * height_; } void scale(int factor) { width_ *= factor; height_ *= factor; } private: int width_; int height_; };
int main() { Rectangle rectangle{6, 4}; const int before = rectangle.area(); rectangle.scale(2); std::cout << "area=" << before << " scaled=" << rectangle.area() << '\\n'; }
""",
}),
],
},
{
"id": "CPP06", "number": "06", "repo": "lab-cpp-invariants", "slug": "invariants",
"title": "Hermetyzacja, niezmienniki i odpowiedzialność klasy",
"topic": "Poprawny stan obiektu przed i po każdej operacji publicznej",
"chapter": "A Tour of C++ 3e: sekcja 4.3 i rozdział 5",
"mission": "Zdefiniować niezmiennik klasy i utrzymywać go we wszystkich publicznych operacjach.",
"tasks": [
("Walidowana zmiana stanu", "Odrzuć temperaturę spoza przyjętego zakresu bez uszkodzenia obiektu.", "accepted=1 rejected=1 value=25", {
"main.cpp": """#include <iostream>
class Temperature { public: explicit Temperature(int value) : value_{value} {} bool set(int value) { if (value < -50 || value > 150) return false; value_ = value; return true; } int value() const { return value_; } private: int value_; };
int main() { Temperature temperature{20}; const bool accepted = temperature.set(25); const bool rejected = !temperature.set(400); std::cout << "accepted=" << accepted << " rejected=" << rejected << " value=" << temperature.value() << '\\n'; }
""",
}),
("Pojemność kolejki", "Nie dopuść, by liczba elementów przekroczyła pojemność lub spadła poniżej zera.", "pushes=2 overflow=1 pops=2 empty=1 depth=0", {
"main.cpp": """#include <iostream>
class QueueDepth { public: explicit QueueDepth(int capacity) : capacity_{capacity} {} bool push() { if (depth_ == capacity_) return false; ++depth_; return true; } bool pop() { if (depth_ == 0) return false; --depth_; return true; } int depth() const { return depth_; } private: int capacity_; int depth_{}; };
int main() { QueueDepth queue{2}; int pushes{}; pushes += queue.push(); pushes += queue.push(); const bool overflow = !queue.push(); int pops{}; pops += queue.pop(); pops += queue.pop(); const bool empty = !queue.pop(); std::cout << "pushes=" << pushes << " overflow=" << overflow << " pops=" << pops << " empty=" << empty << " depth=" << queue.depth() << '\\n'; }
""",
}),
("Normalizacja przedziału", "Skonstruuj przedział zawsze spełniający start <= end.", "start=3 end=10 width=7", {
"main.cpp": """#include <iostream>
class Interval { public: Interval(int first, int second) : start_{first < second ? first : second}, end_{first < second ? second : first} {} int start() const { return start_; } int end() const { return end_; } int width() const { return end_ - start_; } private: int start_; int end_; };
int main() { Interval interval{10, 3}; std::cout << "start=" << interval.start() << " end=" << interval.end() << " width=" << interval.width() << '\\n'; }
""",
}),
],
},
{
"id": "CPP07", "number": "07", "repo": "lab-cpp-construction-destruction", "slug": "construction-destruction",
"title": "Konstruktory, destruktory i inicjalizacja obiektu",
"topic": "Kolejność inicjalizacji, delegowanie konstruktorów i porządek destrukcji",
"chapter": "A Tour of C++ 3e: rozdziały 5 i 6",
"mission": "Zaobserwować pełny cykl tworzenia i niszczenia obiektu oraz kolejność jego części.",
"tasks": [
("Kolejność konstrukcji", "Zaobserwuj, że pole jest konstruowane przed ciałem konstruktora obiektu.", "member\nobject\nready", {
"main.cpp": """#include <iostream>
struct Member { Member() { std::cout << "member\\n"; } };
struct Object { Member member; Object() { std::cout << "object\\n"; } };
int main() { Object object; (void)object; std::cout << "ready\\n"; }
""",
}),
("Destrukcja w odwrotnej kolejności", "Utwórz dwa obiekty automatyczne i sprawdź kolejność destruktorów.", "enter\nconstruct=A\nconstruct=B\ndestroy=B\ndestroy=A\nleave", {
"main.cpp": """#include <iostream>
struct Trace { const char* name; explicit Trace(const char* value) : name{value} { std::cout << "construct=" << name << '\\n'; } ~Trace() { std::cout << "destroy=" << name << '\\n'; } };
void scope() { Trace first{"A"}; Trace second{"B"}; }
int main() { std::cout << "enter\\n"; scope(); std::cout << "leave\\n"; }
""",
}),
("Konstruktor delegujący", "Skieruj konstruktor domyślny do jednego konstruktora utrzymującego reguły inicjalizacji.", "point=0,0 other=3,4", {
"main.cpp": """#include <iostream>
class Point { public: Point() : Point{0, 0} {} Point(int x, int y) : x_{x}, y_{y} {} int x() const { return x_; } int y() const { return y_; } private: int x_; int y_; };
int main() { Point point; Point other{3, 4}; std::cout << "point=" << point.x() << ',' << point.y() << " other=" << other.x() << ',' << other.y() << '\\n'; }
""",
}),
],
},
{
"id": "CPP08", "number": "08", "repo": "lab-cpp-scope-lifetime", "slug": "scope-lifetime",
"title": "Zakres i czas życia obiektów",
"topic": "Obiekty automatyczne, statyczne i dynamiczne oraz przesłanianie nazw",
"chapter": "A Tour of C++ 3e: sekcja 1.5 i rozdział 6",
"mission": "Powiązać miejsce przechowywania obiektu z początkiem i końcem jego czasu życia.",
"tasks": [
("Automatyczny i statyczny", "Porównaj nowy obiekt automatyczny z zachowującym stan obiektem statycznym.", "auto=7 static_calls=1,2", {
"main.cpp": """#include <iostream>
int next_call() { static int calls{}; return ++calls; }
int main() { int automatic{7}; const int first = next_call(); const int second = next_call(); std::cout << "auto=" << automatic << " static_calls=" << first << ',' << second << '\\n'; }
""",
}),
("Obiekt dynamiczny", "Jawnie zestaw new z delete i obserwuj moment wykonania destruktora.", "construct\nvalue=11\ndestroy", {
"main.cpp": """#include <iostream>
struct Dynamic { explicit Dynamic(int value) : value{value} { std::cout << "construct\\n"; } ~Dynamic() { std::cout << "destroy\\n"; } int value; };
int main() { Dynamic* object = new Dynamic{11}; std::cout << "value=" << object->value << '\\n'; delete object; }
""",
}),
("Zakres i przesłanianie", "Pokaż, że nazwa wewnętrzna nie zmienia obiektu o tej samej nazwie w zakresie zewnętrznym.", "outer=7\ninner=9\nouter=7", {
"main.cpp": """#include <iostream>
int main() { int value{7}; std::cout << "outer=" << value << '\\n'; { int value{9}; std::cout << "inner=" << value << '\\n'; } std::cout << "outer=" << value << '\\n'; }
""",
}),
],
},
{
"id": "CPP09", "number": "09", "repo": "lab-cpp-raii", "slug": "raii",
"title": "RAII: zasób związany z czasem życia obiektu",
"topic": "Pozyskanie w konstruktorze, zwolnienie w destruktorze i wszystkie wyjścia z zakresu",
"chapter": "A Tour of C++ 3e: sekcja 6.3",
"mission": "Zapewnić zwolnienie zasobu przez strukturę programu, także przy wczesnym return.",
"tasks": [
("Uchwyt jako obiekt", "Zwiąż stan otwarcia uchwytu z konstruktorem i destruktorem.", "acquire\nopen=1\nrelease\nopen=0", {
"main.cpp": """#include <iostream>
bool resource_open{};
class Handle { public: Handle() { resource_open = true; std::cout << "acquire\\n"; } ~Handle() { resource_open = false; std::cout << "release\\n"; } Handle(const Handle&) = delete; Handle& operator=(const Handle&) = delete; };
int main() { { Handle handle; (void)handle; std::cout << "open=" << resource_open << '\\n'; } std::cout << "open=" << resource_open << '\\n'; }
""",
}),
("Wczesny return", "Potwierdź zwolnienie strażnika przy wyjściu z funkcji przed jej końcem.", "acquire\nwork=short\nrelease\nafter", {
"main.cpp": """#include <iostream>
struct Guard { Guard() { std::cout << "acquire\\n"; } ~Guard() { std::cout << "release\\n"; } };
void work(bool short_path) { Guard guard; if (short_path) { std::cout << "work=short\\n"; return; } std::cout << "work=long\\n"; }
int main() { work(true); std::cout << "after\\n"; }
""",
}),
("Zagnieżdżone zasoby", "Sprawdź zwalnianie zagnieżdżonych zasobów w odwrotnej kolejności.", "acquire=A\nacquire=B\ninside\nrelease=B\nrelease=A", {
"main.cpp": """#include <iostream>
struct Resource { const char* name; explicit Resource(const char* value) : name{value} { std::cout << "acquire=" << name << '\\n'; } ~Resource() { std::cout << "release=" << name << '\\n'; } };
int main() { Resource first{"A"}; { Resource second{"B"}; std::cout << "inside\\n"; } }
""",
}),
],
},
{
"id": "CPP10", "number": "10", "repo": "lab-cpp-copy-move", "slug": "copy-move",
"title": "Kopiowanie, przenoszenie i Rule of Zero/Five",
"topic": "Głębokie kopiowanie, transfer własności i typy składowe zarządzające zasobem",
"chapter": "A Tour of C++ 3e: sekcje 6.26.4",
"mission": "Rozróżnić kopię od przeniesienia i wybrać Rule of Zero dla bezpiecznego typu zasobowego.",
"tasks": [
("Głęboka kopia", "Zaimplementuj kopię bufora tak, aby dwa obiekty nie współdzieliły jednej komórki.", "original=7 copy=9 distinct=1", {
"main.cpp": """#include <iostream>
class Buffer { public: explicit Buffer(int value) : data_{new int{value}} {} ~Buffer() { delete data_; } Buffer(const Buffer& other) : data_{new int{*other.data_}} {} Buffer& operator=(const Buffer&) = delete; int value() const { return *data_; } void set(int value) { *data_ = value; } const int* address() const { return data_; } private: int* data_; };
int main() { Buffer original{7}; Buffer copy{original}; copy.set(9); std::cout << "original=" << original.value() << " copy=" << copy.value() << " distinct=" << (original.address() != copy.address()) << '\\n'; }
""",
}),
("Przeniesienie własności", "Przenieś bufor move-only i pozostaw obiekt źródłowy w poprawnym stanie pustym.", "src_empty=1 value=42", {
"main.cpp": """#include <iostream>
#include <utility>
class Buffer { public: explicit Buffer(int value) : data_{new int{value}} {} ~Buffer() { delete data_; } Buffer(const Buffer&) = delete; Buffer& operator=(const Buffer&) = delete; Buffer(Buffer&& other) noexcept : data_{std::exchange(other.data_, nullptr)} {} bool empty() const { return data_ == nullptr; } int value() const { return *data_; } private: int* data_; };
int main() { Buffer source{42}; Buffer target{std::move(source)}; std::cout << "src_empty=" << source.empty() << " value=" << target.value() << '\\n'; }
""",
}),
("Rule of Zero", "Zbuduj typ z bezpiecznych składowych standardowych bez własnego destruktora i operacji copy/move.", "copy=alpha original=beta moved=alpha", {
"main.cpp": """#include <iostream>
#include <string>
#include <utility>
struct Document { std::string text; };
int main() { Document original{"alpha"}; Document copy = original; original.text = "beta"; Document moved = std::move(copy); std::cout << "copy=" << moved.text << " original=" << original.text << " moved=" << moved.text << '\\n'; }
""",
}),
],
},
]
CARD_ACTION = r'''#!/usr/bin/env bash
set -euo pipefail
action="${1:?action is required}"
root="${STEM_REPO:-${CARD_ROOT:-$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)}}"
profile="${STEM_PROFILE:-native-amd64}"
target="${STEM_TARGET:-native}"
selector="${STEM_TASK:-task1}"
card_id="$(sed -n 's/^card: cpp\///p' "$root/lesson-flow.yaml" | head -n1)"
resolve_task() {
local value="${1,,}"
if [[ "$value" =~ ^(task|t)?[-_]?0*([1-3])($|[-_].*) ]]; then printf 'task%02d\n' "${BASH_REMATCH[2]}"; return; fi
[[ "$value" =~ ^task0[1-3]$ ]] && { printf '%s\n' "$value"; return; }
printf 'Unknown task selector: %s\n' "$1" >&2; exit 2
}
task="$(resolve_task "$selector")"
artifact_dir="$root/.stem/artifacts/$profile/$target/$task"
binary="$artifact_dir/program"
mkdir -p "$artifact_dir"
compile_task() {
local selected="$1" output="$2"
mapfile -t sources < <(find "$root/src/$selected" -maxdepth 1 -type f -name '*.cpp' | sort)
[[ ${#sources[@]} -gt 0 ]] || { printf 'No C++ sources for %s\n' "$selected" >&2; exit 2; }
"${CXX:-g++}" -std=c++20 -Wall -Wextra -Wpedantic -Werror -O0 -g3 -fno-omit-frame-pointer -I"$root/src/$selected" "${sources[@]}" -o "$output"
}
verify_task() {
local selected="$1" output="$2" actual="$artifact_dir/actual.txt"
compile_task "$selected" "$output"
"$output" >"$actual"
diff -u "$root/tests/expected/$selected.txt" "$actual"
}
case "$profile:$target:$action" in
native-amd64:native:build)
for item in task01 task02 task03; do
current="$root/.stem/artifacts/$profile/$target/$item"
mkdir -p "$current"
compile_task "$item" "$current/program"
done
printf 'BUILD %s standard=c++20 tasks=3\n' "$card_id"
;;
native-amd64:native:test|native-amd64:native:run)
verify_task "$task" "$binary"
printf 'PASS %s %s standard=c++20\n' "$card_id" "$task"
;;
native-amd64:native:debug)
verify_task "$task" "$binary"
trace="$artifact_dir/debug.log"
if command -v gdb >/dev/null 2>&1; then
gdb -q -batch -ex 'set pagination off' -ex 'set debuginfod enabled off' -ex 'break main' -ex run -ex 'info source' -ex 'info args' -ex continue "$binary" >"$trace" 2>&1
else
{ printf 'gdb unavailable; binary evidence follows\n'; "${NM:-nm}" -C "$binary" | head -n 80; "$binary"; } >"$trace" 2>&1
fi
cat "$trace"
[[ -s "$trace" ]]
printf 'TRACE %s %s log=%s\n' "$card_id" "$task" "$trace"
;;
*) printf 'Unsupported profile/target/action: %s/%s/%s\n' "$profile" "$target" "$action" >&2; exit 2 ;;
esac
'''
def write(path: Path, content: str, executable: bool = False) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(clean(content), encoding="utf-8")
if executable:
path.chmod(path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
def card_source(lesson: dict, document_uuid: str) -> dict:
prefix = lesson["id"]
sections = []
tasks = {}
scope_rows = []
for index, (title, prompt, expected, _files) in enumerate(lesson["tasks"], 1):
task_id = f"task{index:02d}"
expected_tex = expected.replace("_", "\\_")
sections.append({
"title": f"Task{index:02d} · {title}",
"content_kind": "prose",
"content_tex": f"{prompt} Najpierw przewidź wynik, następnie skompiluj program z ostrzeżeniami traktowanymi jako błędy i porównaj dokładny stdout z kontraktem: {expected_tex.replace(chr(10), '; ')}.",
"educational_requirement_refs": [f"{prefix}.WE01"],
"learning_effect_refs": [f"{prefix}.EN01", f"{prefix}.EK01"],
"assessment_criterion_refs": [f"{prefix}.KW01"],
"area_tree_refs": [f"{prefix}.WE01.OG.IP.01", f"{prefix}.WE01.TECH.I4.01"],
"steps": [
{"id": f"T{index:02d}-PREDICT", "title": "Zapisz przewidywany wynik i wskaż regułę języka.", "tree_refs": [f"{prefix}.WE01.OG.IP.01"]},
{"id": f"T{index:02d}-VERIFY", "title": "Uruchom test i zachowaj mierzalny dowód wykonania.", "tree_refs": [f"{prefix}.WE01.TECH.I4.01"]},
],
})
tasks[task_id] = {
"prompt_tex": prompt,
"criterion": expected_tex.replace("\n", "; "),
"assessment_criterion_ref": f"{prefix}.KW01",
}
scope_rows.append({"chapter": "1", "task": f"Task{index:02d}", "idea_tex": title, "priority": "obowiązkowe", "status": "ready", "version": "v00.01"})
return {
"$schema": "../../../../tools/card-layouts/schemas/card-source.schema.json",
"schema": "esc-card-source.v1",
"card": {
"id": f"mpabi-inf-cpp-y2s1-{lesson['number']}-{lesson['slug']}",
"series": "cpp",
"series_title": "C++ · Rok 2 · Semestr 1",
"number": lesson["number"], "count": "10", "slug": lesson["slug"],
"title": f"{lesson['id']} · {lesson['title']}", "topic": lesson["topic"],
"project": "C++20 · fundamenty języka i modelu obiektu", "subject": "Informatyka",
"level": "Rok 2 · Semestr 1", "revision_date": "2026-07-21T00:00:00+02:00",
"status": "Gotowa do wykonania", "version": "v00.01", "uuid": document_uuid,
"author": "M. Pabiszczak", "year": "2026",
},
"generated": {"tex": "doc/generated/main.tex", "html": "web/index.html", "html_css": "web/style.css", "react_app": True, "html_tree_inspector": False},
"template": "templates/karta-klasyczna.json",
"title_block": {
"category": "KARTA PRACY · INFORMATYKA · C++20", "standard": "ISO 7200",
"prepared_by": "M. Pabiszczak", "prepared_on": "21.07.2026", "checked_by": "testy automatyczne", "approved_by": "",
"url": "http://localhost:8080", "repository_url": f"https://zsl-gitea.mpabi.pl/edu-cpp/{lesson['repo']}",
"revision": "1", "issued_on": "21.07.2026", "series": f"CPP-Y2S1-{lesson['number']}", "document_type": "karta pracy",
"tool": "card-layouts", "sheet": "1 / 1", "show_qr": False, "show_repository_qr": False,
"height_cm": 3, "repeat_on_every_page": True, "replace_front_matter": True,
},
"front_page_scope": {"title": "Cel karty", "content_tex": lesson["mission"], "scope_title": "Zakres tasków", "scope_table": {"headers": ["Krok", "Numer tasku", "Najważniejsza idea", "Priorytet", "Status", "Version"], "rows": scope_rows}},
"learning_effects": {
f"{prefix}.EN01": {"text": f"Uczeń wyjaśnia reguły C++20 związane z tematem: {lesson['topic']}.", "label": "Model języka", "bloom_level": "Analiza", "assessment_criteria": [f"{prefix}.KW01"]},
f"{prefix}.EK01": {"text": "Uczeń implementuje, kompiluje i weryfikuje trzy małe eksperymenty C++20.", "label": "Eksperyment C++", "bloom_level": "Zastosowanie", "assessment_criteria": [f"{prefix}.KW01"]},
},
"assessment_criteria": {f"{prefix}.KW01": {"text": "Trzy taski kompilują się bez ostrzeżeń i zwracają dokładny, testowany wynik.", "learning_effects": [f"{prefix}.EN01", f"{prefix}.EK01"]}},
"educational_requirements": {
f"{prefix}.WE01": {"text": lesson["mission"], "label": lesson["topic"], "learning_effects": [f"{prefix}.EN01", f"{prefix}.EK01"],
"learning_tree": {"schema": "we-learning-tree.v1", "policy": "Każde twierdzenie o języku ma dowód w kodzie, wyniku programu albo symbolach binarnych.",
"ogolne": [{"tree_id": f"{prefix}.WE01.OG.IP.01", "effect_ref": f"{prefix}.EN01", "display": f"EN IP {prefix}", "text": "Uczeń przewiduje zachowanie programu na podstawie reguł języka.", "kw": [{"criterion_ref": f"{prefix}.KW01", "display": f"KW IP {prefix}", "text": "Uzasadnia przewidywany wynik."}]}],
"zawodowe": [{"tree_id": f"{prefix}.WE01.TECH.I4.01", "effect_ref": f"{prefix}.EK01", "display": f"EK INF04 {prefix}", "text": "Uczeń buduje i testuje program C++20.", "kw": [{"criterion_ref": f"{prefix}.KW01", "display": f"KW INF04 {prefix}", "text": "Uzyskuje trzy komunikaty PASS."}]}],
},
}
},
"sections": sections, "tasks": tasks, "tasks_order": ["task01", "task02", "task03"],
"mission": lesson["mission"],
"objectives": ["Wyjaśnisz badaną regułę C++20.", "Skompilujesz program z pełnym zestawem ostrzeżeń.", "Porównasz dokładny wynik i ślad debuggera."],
"agenda": [{"time": "0--15 min", "work": "Model i przewidywanie."}, {"time": "15--45 min", "work": "Trzy eksperymenty kodowe."}, {"time": "45--60 min", "work": "Debug, wnioski i raport."}],
"tech_stack": {"software": [{"name": "C++20", "use": "standard języka"}, {"name": "GCC/GDB", "use": "kompilacja i dowód wykonania"}, {"name": "stemctl", "use": "izolowany profil native-amd64"}], "hardware": [{"name": "Brak", "use": "laboratorium kontenerowe"}]},
"container_test": {"title": "Test w profilu native-amd64", "commands": [f"stemctl test native-amd64 cpp {lesson['id']} 1", f"stemctl test native-amd64 cpp {lesson['id']} 2", f"stemctl debug native-amd64 cpp {lesson['id']} 3"], "pass_condition": "Trzy wyniki zgodne z expected i niepusty debug.log."},
"safety_rules": [{"title": "Brak niezdefiniowanego zachowania", "body": "Kod testowy nie dereferencjonuje nullptr i nie używa obiektu po zakończeniu czasu życia."}, {"title": "Jedna zmienna naraz", "body": "Każdy task izoluje jedną regułę języka i ma deterministyczny stdout."}],
"hardware_procedure": [{"step": "1", "action": "Przewidź wynik.", "condition": "Reguła języka zapisana przed uruchomieniem."}, {"step": "2", "action": "Uruchom trzy taski.", "condition": "Trzy komunikaty PASS."}, {"step": "3", "action": "Odczytaj debug.log.", "condition": "Debugger zatrzymał program w main."}],
"references": [],
}
def generate_lesson(lesson: dict) -> None:
repo = OUTPUT / lesson["repo"]
if repo.exists() and (repo / ".git").exists():
raise SystemExit(f"Refusing to overwrite Git repository: {repo}")
if repo.exists():
shutil.rmtree(repo)
repo.mkdir(parents=True)
shutil.copytree(TEMPLATE / "scripts", repo / "scripts")
shutil.copy2(TEMPLATE / ".gitignore", repo / ".gitignore")
(repo / "web").mkdir(parents=True)
shutil.copy2(TEMPLATE / "web" / "app.css", repo / "web" / "app.css")
(repo / "doc" / "pdf").mkdir(parents=True)
write(repo / "doc" / "pdf" / ".gitkeep", "")
package = json.loads((TEMPLATE / "package.json").read_text(encoding="utf-8"))
package["name"] = f"mpabi-inf-cpp-y2s1-{lesson['number']}-{lesson['slug']}"
write(repo / "package.json", json.dumps(package, ensure_ascii=False, indent=2) + "\n")
lock = json.loads((TEMPLATE / "package-lock.json").read_text(encoding="utf-8"))
lock["name"] = package["name"]
if "packages" in lock and "" in lock["packages"]:
lock["packages"][""]["name"] = package["name"]
write(repo / "package-lock.json", json.dumps(lock, ensure_ascii=False, indent=2) + "\n")
write(repo / "stem-card.yaml", """
schema: 1
targets:
native: {profile: native-amd64, actions: [build, test, run, debug]}
actions:
build: [bash, tools/card-action.sh, build]
test: [bash, tools/card-action.sh, test]
run: [bash, tools/card-action.sh, run]
debug: [bash, tools/card-action.sh, debug]
artifacts: {directory: .stem/artifacts}
""")
write(repo / "tools" / "card-action.sh", CARD_ACTION, executable=True)
write(repo / "Makefile", """
TASK ?= task01
.PHONY: build test run trace clean
build:
\t@CARD_ROOT="$(CURDIR)" tools/card-action.sh build
test:
\t@CARD_ROOT="$(CURDIR)" STEM_TASK="$(TASK)" tools/card-action.sh test
run: test
trace:
\t@CARD_ROOT="$(CURDIR)" STEM_TASK="$(TASK)" tools/card-action.sh debug
clean:
\t@find .stem/artifacts -mindepth 1 -maxdepth 7 -type f -delete 2>/dev/null || true
""")
flow = ["schema: 1", f"card: cpp/{lesson['id']}", f"title: {lesson['title']}", "steps:"]
for index, (title, _prompt, _expected, _files) in enumerate(lesson["tasks"], 1):
flow.extend([
f" - id: task-{index:02d}", f" task: task{index:02d}", " action: test", " profile: native-amd64", " target: native",
f" command: stemctl test native-amd64 cpp {lesson['id']} {index}",
f" board:", f" show: [source, stdout, binary]", f" highlight: [cxx20, {lesson['slug'].replace('-', '_')}]",
f" question: {title} — jaka reguła języka wyjaśnia dokładny wynik?",
f" evidence: [compile_warnings_zero, expected_stdout, exit_zero]",
])
write(repo / "lesson-flow.yaml", "\n".join(flow) + "\n")
for index, (_title, _prompt, expected, files) in enumerate(lesson["tasks"], 1):
task = f"task{index:02d}"
for name, source in files.items():
write(repo / "src" / task / name, source)
write(repo / "tests" / "expected" / f"{task}.txt", expected + "\n")
document_uuid = str(uuid.uuid5(UUID_NAMESPACE, lesson["repo"]))
source = card_source(lesson, document_uuid)
write(repo / "json" / "card_source.json", json.dumps(source, ensure_ascii=False, indent=2) + "\n")
write(repo / "doc" / "main.tex", f"""
\\newcommand{{\\PublisherDomain}}{{mpabi}}
\\newcommand{{\\CardArea}}{{inf}}
\\newcommand{{\\CardSeries}}{{cpp-y2s1}}
\\newcommand{{\\CardNumber}}{{{lesson['number']}}}
\\newcommand{{\\CardSlug}}{{{lesson['slug']}}}
\\newcommand{{\\CardVersion}}{{v00.01}}
\\newcommand{{\\DocumentUUID}}{{{document_uuid}}}
""")
expected_rows = "\n".join(f"| Task{index:02d} | `{title}` | `{expected.replace(chr(10), ' / ')}` |" for index, (title, _prompt, expected, _files) in enumerate(lesson["tasks"], 1))
write(repo / "README.md", f"""
# {lesson['id']}{lesson['title']}
Karta {lesson['number']}/10 kursu **C++ · Rok 2 · Semestr 1**. Standard: C++20.
Oś merytoryczna: {lesson['chapter']}. Materiał książkowy jest parafrazowany;
repozytorium zawiera samodzielne eksperymenty i testy, a nie kopię książki.
## Taski
| Task | Temat | Dokładny dowód stdout |
| --- | --- | --- |
{expected_rows}
Każdy task jest osobnym programem w `src/task0N`. Test kompiluje wszystkie
jednostki translacji poleceniem zgodnym z `-std=c++20 -Wall -Wextra
-Wpedantic -Werror`, uruchamia program i porównuje dokładny wynik.
## Uruchomienie
```bash
stemctl build native-amd64 cpp {lesson['id']} 1
stemctl test native-amd64 cpp {lesson['id']} 1
stemctl test native-amd64 cpp {lesson['id']} 2
stemctl debug native-amd64 cpp {lesson['id']} 3
```
Lokalnie można użyć `make build`, `make test TASK=task02` i
`make trace TASK=task03`. Ślad debuggera trafia do `.stem/artifacts`.
## Granica semestru
Ta seria nie uczy API RTOS, zaawansowanych kontenerów STL ani
współbieżności. Są to osobne bloki następnych semestrów opisanych w
`/home/user/dev/keys/cxx.md`.
""")
answer_lines = [f"# Odpowiedzi — {lesson['id']}", "", lesson["mission"], ""]
for index, (title, prompt, expected, files) in enumerate(lesson["tasks"], 1):
answer_lines.extend([
f"## Task{index:02d}{title}", "", prompt, "",
f"Oczekiwany stdout: `{expected.replace(chr(10), ' / ')}`.", "",
f"Pliki dowodu: {', '.join(f'`src/task{index:02d}/{name}`' for name in sorted(files))}.", "",
"Wynik nie jest oceniany wzrokowo: test kompiluje kod bez ostrzeżeń, uruchamia go i wykonuje dokładne `diff` z plikiem `tests/expected`.", "",
])
write(repo / "answers" / "solutions.md", "\n".join(answer_lines))
write(repo / "tests" / "card_contract.test.mjs", f"""
import assert from 'node:assert/strict';
import {{ access, readFile }} from 'node:fs/promises';
import test from 'node:test';
const source = JSON.parse(await readFile(new URL('../json/card_source.json', import.meta.url), 'utf8'));
test('{lesson['id']} identity and task order are stable', () => {{
assert.equal(source.card.number, '{lesson['number']}');
assert.equal(source.card.uuid, '{document_uuid}');
assert.deepEqual(source.tasks_order, ['task01', 'task02', 'task03']);
assert.match(JSON.stringify(source), /C\\+\\+20/);
}});
test('all task programs and expected outputs exist', async () => {{
for (const task of ['task01', 'task02', 'task03']) {{
await access(new URL(`../src/${{task}}/main.cpp`, import.meta.url));
await access(new URL(`./expected/${{task}}.txt`, import.meta.url));
}}
}});
""")
shutil.copy2(TEMPLATE / "tests" / "card_state_db.test.mjs", repo / "tests" / "card_state_db.test.mjs")
def main() -> None:
OUTPUT.mkdir(parents=True, exist_ok=True)
for lesson in LESSONS:
generate_lesson(lesson)
print(f"generated {lesson['id']} {lesson['repo']}")
if __name__ == "__main__":
main()