Files
klammertext/mac/state.cpp
T
ack fe475e774a Klammertext runs natively on Windows; a Windows source-install guide; the bundled sh for @eval :shell
Assembled from klammertext-dev f344a37d0f6e. The commands, the library,
the Standard Klammer Set, the reflection klammerset and the Python
binding build and run natively on Windows (MSYS2 UCRT64 gcc, python.org
Python), and both test tiers pass there. doc/install/windows_source_install.md
is the fifth install guide: a source install parallel to the Linux and
macOS ones, verified by following it on a pristine Windows 11.

Behaviour on every system: paths Klammertext prints use forward slashes;
output is LF; the command line is UTF-8 on Windows; @eval :shell is a
POSIX shell command everywhere -- on Windows it runs the sh Klammertext
ships, BusyBox for Windows (win/busybox/, GPLv2, with its licence and the
source pointer). KLAMMERTEXT_FONTS and KLAMMERTEXT_KLAMMERSETS are lists in
the system's PATH convention (';' on Windows). On Linux and macOS nothing
observable changes.
2026-09-25 00:32:55 +02:00

403 lines
14 KiB
C++

#include <algorithm>
#include <cctype>
#include <regex>
#include <iostream>
#include <map>
#include "show.h"
#include "file.h"
#include "error.h"
#include "state.h"
#include "log.h"
#include "util.h"
int State::class_id = 0;
namespace {
// A sys.path entry reaches only the directory's own files, so the decision
// must not recurse; and the answer is fixed for the run (the search-dir set
// only grows, and a directory's answer does not change), so it is decided
// once per directory per process. The former recursive walk here ran on
// every @eval and took ~200 ms per eval on a document in a large home
// directory (27 s for one render).
bool directory_has_python_file(const std::string& dir)
{
static std::map<std::string, bool> cache {};
auto it = cache.find(dir);
if (it != cache.end()) {
return it->second;
}
bool found = false;
std::error_code ec {};
for (const auto& entry : std::filesystem::directory_iterator(dir, ec)) {
if (entry.is_regular_file(ec) && entry.path().extension() == ".py") {
found = true;
break;
}
}
cache[dir] = found;
return found;
}
} // namespace
bool Var::defined() const
{
return !m_name.empty();
}
std::vector<std::string> Frame::names() const
{
std::vector<std::string> result;
result.reserve(m_vars.size());
std::transform(m_vars.begin(), m_vars.end(), std::back_inserter(result),
[](const auto& pair) { return pair.first; });
return result;
}
void Frame::set(const std::string& name, const std::string& value,
const std::string& delim, const std::string& desc, const Locator& loc,
const Argtype& argtype)
{
Var v(name, value, delim, desc, loc, argtype);
m_vars[name] = v;
}
std::pair<Var, bool> Frame::get(const std::string& name) const
{
std::pair<Var, bool> result {Var(), false};
if (m_vars.contains(name)) {
result = {m_vars.at(name), true};
}
return result;
}
// State
void State::open_frame(const std::string& name)
{
Frame f(name);
// m_frames.push_back(f);
m_frames.emplace(m_frames.begin(), f);
}
void State::close_frame()
{
if (m_frames.empty()) {
throw Internal_error("No frame to close", Locator());
}
// Preserve altered machine state:
string_map machine_state {};
for (const auto& [name, var] : m_frames[0].m_vars) {
if (contains(name, "K_")) {
machine_state[name] = var.m_value;
}
}
m_frames.erase(m_frames.begin());
for (const auto& [name, value] : machine_state) {
set(name, value, true);
}
}
void State::set(const std::string& name, const std::string& value, bool update,
const std::string& delim, const std::string& desc, const Locator& loc,
const Argtype& argtype)
{
if (m_frames.empty()) {
std::stringstream ss {};
ss << "No open frame to set " << q_(name) << " to " << q_(value);
throw Internal_error(ss.str(), Locator());
}
auto [current, exists] = m_frames[0].get(name);
if (exists && current.m_value != klammerstate::no_value && !update) {
std::stringstream ss {};
ss << "Variable " << q_(name) << " is already defined at " << current.m_loc.desc()
<< ". Use ':replace <new-value>' to replace the current value of "
<< q_(current.m_value) << ".";
throw Argument_error(ss.str(), current.m_loc);
}
m_frames[0].set(name, value, delim, desc, loc, argtype);
}
void State::set(const std::map<std::string, std::string>& varmap)
{
for (const auto& [k, v] : varmap) {
set(k, v);
}
}
void State::set(const std::map<std::string, std::string>& varmap,
const Parameter_set& parameters)
{
for (const auto& [name, value] : varmap) {
const Parameter* parameter = parameters.find(name);
set(name, value, false, " ", "", Locator(),
parameter ? parameter->m_argtype : Argtype());
}
}
void State::replace(const std::string& name, const std::string& value, bool error_if_not_defined)
{
if (error_if_not_defined && !get(name).defined()) {
std::stringstream ss {};
ss << "Cannot replace value of nonexistent variable " << q_(name) << " with " << q_(value);
throw Argument_error(ss.str(), Locator());
}
set(name, value, true);
}
// Whether a name can be used as a state variable at all. POSIX allows only
// [A-Za-z_][A-Za-z0-9_]* in an environment variable name, and the katomizer's
// karg pattern (\*\w+\*) accepts no more, so a name outside this set could
// never be written as *NAME* in the first place.
//
// The one that matters in practice: bash EXPORTS SHELL FUNCTIONS, and they
// arrive in environ as "BASH_FUNC_name%%=() { ... }". Those were being taken
// as state variables and then emitted into the @eval Python globals as
// assignments, where "BASH_FUNC_mdpdf%%" is not a valid identifier -- so a
// single "export -f" anywhere in a user's shell profile made EVERY @eval fail
// with a Python SyntaxError, reported against whichever .k file happened to
// hold the first one (sks/date/date.k). Nothing in the message named the
// environment. Found 2026-09-04 by the dual-compiler confidence run.
static bool usable_variable_name(const std::string& name)
{
if (name.empty())
return false;
if (!std::isalpha(static_cast<unsigned char>(name[0])) && name[0] != '_')
return false;
return std::all_of(name.begin(), name.end(), [](unsigned char c) {
return std::isalnum(c) != 0 || c == '_'; });
}
void State::add_environment_frame()
{
open_frame(klammerstate::shell_environment_name);
for (const auto& [name, value] : environment_variables()) {
// Silently skipped rather than reported: the user can read it off
// their own shell profile, it changes no output, and an exported
// function would otherwise warn on every run.
if (usable_variable_name(name))
set(name, value);
}
}
Var State::get(const std::string& name, bool error_if_not_defined, const Locator& loc) const
{
for (const auto& f : m_frames) {
auto [result, found] = f.get(name);
if (found) {
return result;
}
}
if (error_if_not_defined) {
// Which frames were searched belongs IN the error, not printed beside
// it. This was a msg() dumping the whole state to stdout before the
// throw -- scaffolding, on the wrong stream, and separated from the
// message it was explaining. The frame NAMES are the useful part: a
// variable missing because the expected frame was never opened looks
// exactly like one that was never set.
strings_t frame_names {};
for (const auto& f : m_frames) {
frame_names.push_back(f.m_name);
}
throw Argument_error(
"Variable " + q_(name) + " not defined (searched: "
+ join(frame_names, ", ") + ")", loc);
} else {
return Var();
}
}
std::string State::value(const std::string& name, bool error_if_not_defined, const Locator& loc) const
{
return get(name, error_if_not_defined, loc).m_value;
}
std::string State::subst(const std::string& text, bool quote_values) const
{
(void)K::log(3);
std::string result = text;
std::regex varpat(R"(\*(\w+)\*)");
for (std::sregex_iterator iter(text.begin(), text.end(), varpat), end; iter != end; ++iter) {
std::string match = iter->str();
std::string var = (*iter)[1].str();
//std::cout << "Found: " << iter->str() << sp_arrow << (*iter)[1].str() << "\n";
// std::cout << "Found: " << match << sp_arrow << var << "\n";
//std::string value = get(var).m_value;
auto var_value = value(var, false);
auto printable = q_(var_value);
if (var_value != klammerstate::no_value) {
if (quote_values) {
var_value = q_(var_value);
}
result = string_replace(result, match, var_value);
} else {
// throw Argument_error("Variable " + printable + " is not defined");
}
}
return result;
}
void State::subst(katom_iter begin, katom_iter end)
{
(void)K::log(3);
std::regex varpat(R"((.*?)\*(\w+)\*(.*))");
for (auto ki = begin; ki < end ; ki++) {
// msg() << kall << ktype << *ki << "\n";
std::smatch match;
if (std::regex_match(ki->m_text, match, varpat)
&& ki->m_type == katom_t::karg) {
//auto [var, found] = get(match[1]);
auto var_value = value(match[2], true, begin->m_loc);
if (var_value != klammerstate::no_value) {
(void)K::log(3, "Found subst:", match[1].str(), var_value);
std::stringstream ss {};
ss << match[1] << var_value << match[3];
ki->m_text = ss.str(); // match[1] + var_value + match[3];
} else {
throw Argument_error("Variable " + q_(match[1]) + " is not defined", begin->m_loc);
}
}
}
}
void prohibit_change_of_description(
const std::string& name, bool defined, const std::string& old_desc,
const std::string& new_desc, const Locator& old_loc, const Locator& loc)
{
if (defined && !old_desc.empty() && !new_desc.empty()) {
std::stringstream ss {};
ss << "The " << q_(name) << " variable's description is already defined";
if (new_desc != old_desc) {
ss << "; the description cannot be changed to " << q_(new_desc)
<< " from " << q_(old_desc);
}
ss << " at " << old_loc.desc() << ".";
throw Argument_error(ss.str(), loc);
}
}
// Parameter_set m_parameters = Parameter_set("name :value :append :replace :delim :desc");
void State::parse_state_katoms(katom_iter begin, katom_iter end, katom_list& katoms)
{
(void)K::log(3, *begin, *(end-1));
// std::cout << "parse_katoms: " << std::pair(begin + 1, end - 1) << "\n";
auto [positional, optional, rest] = argument_split(begin + 1, end - 1);
auto args = m_parameters.value_map(positional, optional, rest, begin->m_loc);
// std::cout << std::setfill(' ') << "\nArgument values:\n" << args;
Var current = get(args["name"]);
bool defined = current.defined();
std::string delim = args["delim"];
delim = delim.empty() ? " " : delim;
prohibit_change_of_description(
args["name"], defined, current.m_desc, args["desc"], current.m_loc, begin->m_loc);
if (defined && !args["replace"].empty()) {
replace(args["name"], args["replace"]);
} else if (defined && !args["append"].empty()) {
replace(args["name"], current.m_value + delim + args["append"]);
} else if (!args["value"].empty()) {
set(args["name"], args["value"], false, delim, args["desc"], begin->m_loc);
}
modify_type(katom_t::replaced, begin, end);
auto next_iter = end;
ignore_whitespace(next_iter, katoms);
}
std::vector<std::string> State::all_names() const
{
std::vector<std::string> result {};
for (const Frame& f : m_frames) {
for (const auto& [name, var] : f.m_vars) {
// std::cout << "Name: " << name << "\n";
result.push_back(name);
}
}
return result;
}
void State::add_search_dir(const std::string& dir)
{
if (!dir.empty()
&& std::find(m_search_dirs.begin(), m_search_dirs.end(), dir)
== m_search_dirs.end()) {
m_search_dirs.push_back(dir);
}
}
std::string State::python_code()
{
(void)K::log(3);
std::vector<std::string> names = all_names();
std::string margin = " ";
std::stringstream ss {};
ss << "import sys\n";
strings_t python_dirs = sks_dirs();
// The directories of the files this Machine has read: a module next to
// the file whose @eval names it is found regardless of the cwd.
python_dirs.insert(python_dirs.end(),
m_search_dirs.begin(), m_search_dirs.end());
for (const auto& d : python_dirs) {
if (directory_has_python_file(d)) {
// A Python string literal, not the raw text: a Windows directory
// holds backslashes, and C:\Users reads as a \U unicode escape.
ss << "sys.path.append(" << pyformat_string({d}) << ")\n";
}
}
if (!m_frames.empty()) {
int name_length = max_length(names);
ss << "class K:\n"
<< margin << std::left << std::setw(name_length) << "K_eval_id" << " = "
<< State::class_id++ << "\n";
for (const auto& name : names) {
Var var = get(name);
ss << var.m_argtype.python_value(name, {var.m_value}, name_length) << "\n";
}
}
// msg() << ss.str() << "\n";
return ss.str();
}
std::string State::describe(bool show_environment, int margin_size) const
{
std::string margin(margin_size, ' ');
int i = m_frames.size() - 1;
std::stringstream ss {};
for (const auto& f : m_frames) {
int width = max_key_length(f.m_vars);
ss << margin << "Frame " << i-- << ": " << f.m_name << "\n";
if ((f.m_name != klammerstate::shell_environment_name) ||
(show_environment && f.m_name == klammerstate::shell_environment_name)) {
for (const auto& [key, value] : f.m_vars) {
std::string print_value = value.m_value;
if (print_value == klammerstate::no_value) {
print_value = "<no-value>";
}
ss << margin << " " << std::setw(width) << std::left << key << " "
<< abbrev(print_value) << "\n";
}
}
}
ss << "\n";
return ss.str();
}