载入中...
搜索中...
未找到
ScriptCompiler.cpp
浏览该文件的文档.
2
6
7#include <simplesquirrel/vm.hpp>
8
9#include <algorithm>
10#include <cctype>
11#include <cstdio>
12#include <fstream>
13#include <iomanip>
14#include <iterator>
15#include <mutex>
16#include <regex>
17#include <sstream>
18
19namespace eve::script {
20namespace {
21
22std::mutex compilerRegistryMutex;
23std::unordered_map<HSQUIRRELVM, ScriptCompiler*> compilerRegistry;
24
25std::string hashSource(std::string_view source) {
26 uint64_t hash = 1469598103934665603ull;
27 for (const unsigned char c : source) {
28 hash ^= c;
29 hash *= 1099511628211ull;
30 }
31 std::ostringstream out;
32 out << std::hex << hash;
33 return out.str();
34}
35
36ScriptSourcePosition positionAt(std::string_view source, size_t offset) {
37 ScriptSourcePosition position;
38 for (size_t i = 0; i < offset && i < source.size(); ++i) {
39 if (source[i] == '\n') {
40 ++position.line;
41 position.column = 1;
42 } else {
43 ++position.column;
44 }
45 }
46 return position;
47}
48
49template <typename Callback>
50void forMatches(std::string_view source, const std::regex& pattern, Callback callback) {
51 const std::string owned(source);
52 for (std::sregex_iterator it(owned.begin(), owned.end(), pattern), end; it != end; ++it)
53 callback(*it, static_cast<size_t>(it->position()));
54}
55
56void appendUnique(std::vector<std::string>& values, std::string value) {
57 if (std::find(values.begin(), values.end(), value) == values.end()) values.push_back(std::move(value));
58}
59
60std::string maskComments(std::string_view source) {
61 std::string result(source);
62 char quote = 0;
63 bool lineComment = false;
64 bool blockComment = false;
65 for (size_t i = 0; i < result.size(); ++i) {
66 const char c = result[i];
67 const char next = i + 1 < result.size() ? result[i + 1] : 0;
68 if (lineComment) {
69 if (c == '\n')
70 lineComment = false;
71 else
72 result[i] = ' ';
73 } else if (blockComment) {
74 if (c == '*' && next == '/') {
75 result[i] = result[i + 1] = ' ';
76 ++i;
77 blockComment = false;
78 } else if (c != '\n') {
79 result[i] = ' ';
80 }
81 } else if (quote != 0) {
82 if (c == '\\')
83 ++i;
84 else if (c == quote)
85 quote = 0;
86 } else if (c == '"' || c == '\'') {
87 quote = c;
88 } else if (c == '/' && next == '/') {
89 result[i] = result[i + 1] = ' ';
90 ++i;
91 lineComment = true;
92 } else if (c == '/' && next == '*') {
93 result[i] = result[i + 1] = ' ';
94 ++i;
95 blockComment = true;
96 }
97 }
98 return result;
99}
100
101std::string trim(std::string_view value) {
102 const size_t begin = value.find_first_not_of(" \t\r\n");
103 if (begin == std::string_view::npos) return {};
104 const size_t end = value.find_last_not_of(" \t\r\n");
105 return std::string(value.substr(begin, end - begin + 1));
106}
107
108std::string attributeValue(std::string_view value) {
109 std::string result = trim(value);
110 if (result.size() >= 2 &&
111 ((result.front() == '"' && result.back() == '"') || (result.front() == '\'' && result.back() == '\'')))
112 return result.substr(1, result.size() - 2);
113 return result;
114}
115
116std::vector<std::string> splitArguments(std::string_view arguments) {
117 std::vector<std::string> result;
118 size_t begin = 0;
119 char quote = 0;
120 for (size_t i = 0; i <= arguments.size(); ++i) {
121 const char c = i == arguments.size() ? ',' : arguments[i];
122 if ((c == '"' || c == '\'') && (i == 0 || arguments[i - 1] != '\\'))
123 quote = quote == 0 ? c : (quote == c ? 0 : quote);
124 if (c == ',' && quote == 0) {
125 result.push_back(trim(arguments.substr(begin, i - begin)));
126 begin = i + 1;
127 }
128 }
129 if (result.size() == 1 && result.front().empty()) result.clear();
130 return result;
131}
132
133std::vector<std::string> typeChoices(std::string_view type) {
134 std::vector<std::string> result;
135 static const std::regex choicePattern(R"([\"']([^\"']+)[\"'])");
136 forMatches(type, choicePattern, [&](const std::smatch& match, size_t) { appendUnique(result, match[1].str()); });
137 return result;
138}
139
140std::string diagnosticCode(std::string_view message) {
141 if (message.find("not assignable") != std::string_view::npos) return "EVE2002";
142 if (message.find("outside the allowed choices") != std::string_view::npos) return "EVE2101";
143 if (message.find("named argument") != std::string_view::npos) return "EVE2201";
144 if (message.find("Binding Contract") != std::string_view::npos) return "EVE2202";
145 if (message.find("incompatible unit") != std::string_view::npos) return "EVE2301";
146 if (message.find("non-exhaustive match") != std::string_view::npos) return "EVE2401";
147 if (message.find("persist is only allowed") != std::string_view::npos) return "EVE2501";
148 if (message.find("await is only allowed") != std::string_view::npos) return "EVE2601";
149 if (message.find("cannot resolve") != std::string_view::npos) return "EVE1101";
150 if (message.find("escapes") != std::string_view::npos) return "EVE1102";
151 if (message.find("cyclic import") != std::string_view::npos) return "EVE1103";
152 if (message.find("export") != std::string_view::npos) return "EVE1104";
153 if (message.find("identity") != std::string_view::npos) return "EVE1105";
154 return "EVE0001";
155}
156
157ScriptDiagnostic makeDiagnostic(std::string_view error, std::string_view uri) {
158 std::string source;
159 std::string message(error);
160 int line = 1;
161 int column = 1;
162 parseCompileError(std::string(error), &source, &line, &column, &message);
163 ScriptDiagnostic diagnostic;
164 diagnostic.code = diagnosticCode(message);
165 diagnostic.message = std::move(message);
166 diagnostic.canonicalUri = source.empty() ? std::string(uri) : std::move(source);
167 diagnostic.position.line = static_cast<uint32_t>(std::max(line, 1));
168 diagnostic.position.column = static_cast<uint32_t>(std::max(column, 1));
169 if (diagnostic.code == "EVE2201") diagnostic.fix = "check the parameter names and required arguments";
170 if (diagnostic.code == "EVE2401") diagnostic.fix = "add the missing cases or an else arm";
171 if (diagnostic.code == "EVE2601") diagnostic.fix = "mark the containing function async";
172 return diagnostic;
173}
174
175} // namespace
176
178 ScriptSourcePosition result = generated;
179 for (const ScriptSourceMapEntry& entry : entries) {
180 if (entry.generated.line > generated.line ||
181 (entry.generated.line == generated.line && entry.generated.column > generated.column))
182 break;
183 result.line = entry.original.line + (generated.line - entry.generated.line);
184 result.column = generated.line == entry.generated.line
185 ? entry.original.column + (generated.column - entry.generated.column)
186 : generated.column;
187 }
188 return result;
189}
190
192 ScriptSourcePosition result = original;
193 for (const ScriptSourceMapEntry& entry : entries) {
194 if (entry.original.line > original.line ||
195 (entry.original.line == original.line && entry.original.column > original.column))
196 break;
197 result.line = entry.generated.line + (original.line - entry.original.line);
198 result.column = original.line == entry.original.line
199 ? entry.generated.column + (original.column - entry.original.column)
200 : original.column;
201 }
202 return result;
203}
204
205namespace {
206
207bool sourceIdentityMatches(std::string_view registered, std::string_view requested) {
208 std::string left(registered);
209 std::string right(requested);
210 std::replace(left.begin(), left.end(), '\\', '/');
211 std::replace(right.begin(), right.end(), '\\', '/');
212 if (left == right) return true;
213 const size_t scheme = left.find(":/");
214 if (scheme != std::string::npos) left.erase(0, scheme + 2);
215 while (!left.empty() && left.front() == '/') left.erase(left.begin());
216 return right.size() >= left.size() && right.compare(right.size() - left.size(), left.size(), left) == 0 &&
217 (right.size() == left.size() || right[right.size() - left.size() - 1] == '/');
218}
219
220template <typename Mapper>
221ScriptSourcePosition mapRegisteredPosition(std::string_view source, ScriptSourcePosition position, Mapper mapper) {
222 std::lock_guard lock(compilerRegistryMutex);
223 for (const auto& [_, compiler] : compilerRegistry) {
224 for (const ScriptMetadata& metadata : compiler->metadataSnapshot()) {
225 if (sourceIdentityMatches(metadata.canonicalUri, source)) return mapper(metadata.sourceMap, position);
226 }
227 }
228 return position;
229}
230
231} // namespace
232
233std::string BindingContract::key() const {
234 std::string result = module;
235 result += '/';
236 if (!scriptClass.empty()) {
237 result += scriptClass;
238 result += '.';
239 }
240 result += method;
241 return result;
242}
243
244namespace {
245
246std::string jsonEscape(std::string_view text) {
247 std::string out;
248 out.reserve(text.size() + 8);
249 for (unsigned char c : text) {
250 switch (c) {
251 case '"': out += "\\\""; break;
252 case '\\': out += "\\\\"; break;
253 case '\n': out += "\\n"; break;
254 case '\r': out += "\\r"; break;
255 case '\t': out += "\\t"; break;
256 default:
257 if (c < 0x20) {
258 char buf[8];
259 std::snprintf(buf, sizeof(buf), "\\u%04x", c);
260 out += buf;
261 } else {
262 out.push_back(static_cast<char>(c));
263 }
264 break;
265 }
266 }
267 return out;
268}
269
270const char* scriptUnitName(ScriptUnit unit) {
271 switch (unit) {
272 case ScriptUnit::Seconds: return "Seconds";
273 case ScriptUnit::Milliseconds: return "Milliseconds";
274 case ScriptUnit::Radians: return "Radians";
275 case ScriptUnit::Degrees: return "Degrees";
276 case ScriptUnit::Pixels: return "Pixels";
277 case ScriptUnit::Meters: return "Meters";
278 case ScriptUnit::None: break;
279 }
280 return "None";
281}
282
283std::string asciiLower(std::string_view text) {
284 std::string out(text);
285 for (char& c : out) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
286 return out;
287}
288
289std::vector<std::string> splitTokens(const std::string& query) {
290 std::vector<std::string> tokens;
291 std::string current;
292 for (char c : query) {
293 if (std::isspace(static_cast<unsigned char>(c))) {
294 if (!current.empty()) {
295 tokens.push_back(current);
296 current.clear();
297 }
298 } else {
299 current.push_back(c);
300 }
301 }
302 if (!current.empty()) tokens.push_back(current);
303 return tokens;
304}
305
306int scoreContract(const BindingContract& contract, const std::string& queryLower,
307 const std::vector<std::string>& tokens) {
308 const std::string key = asciiLower(contract.key());
309 const std::string method = asciiLower(contract.method);
310 const std::string cls = asciiLower(contract.scriptClass);
311 const std::string module = asciiLower(contract.module);
312 const std::string docs = asciiLower(contract.documentationId);
313 const std::string hay = key + " " + module + " " + cls + " " + method + " " + docs;
314 if (queryLower.empty()) return 1;
315 for (const std::string& token : tokens) {
316 if (hay.find(token) == std::string::npos) return 0;
317 }
318 if (key == queryLower) return 200;
319 if (cls + "." + method == queryLower) return 180;
320 if (method == queryLower) return 150;
321 if (method.rfind(queryLower, 0) == 0) return 120;
322 if (method.find(queryLower) != std::string::npos) return 90;
323 if (cls.find(queryLower) != std::string::npos) return 75;
324 if (key.find(queryLower) != std::string::npos) return 70;
325 if (docs.find(queryLower) != std::string::npos) return 50;
326 return 40;
327}
328
329} // namespace
330
331std::string BindingContract::toJson() const {
332 std::ostringstream json;
333 json << "{\"key\":\"" << jsonEscape(key()) << "\",\"module\":\"" << jsonEscape(module) << "\",\"scriptClass\":\""
334 << jsonEscape(scriptClass) << "\",\"method\":\"" << jsonEscape(method) << "\",\"parameters\":[";
335 for (size_t i = 0; i < parameters.size(); ++i) {
336 const BindingParameterContract& parameter = parameters[i];
337 if (i != 0) json << ',';
338 json << "{\"name\":\"" << jsonEscape(parameter.name) << "\",\"type\":\"" << jsonEscape(parameter.type)
339 << "\",\"nullable\":" << (parameter.nullable ? "true" : "false");
340 if (parameter.scriptDefault) {
341 json << ",\"default\":\"" << jsonEscape(*parameter.scriptDefault) << '"';
342 }
343 if (parameter.unit != ScriptUnit::None) {
344 json << ",\"unit\":\"" << scriptUnitName(parameter.unit) << '"';
345 }
346 if (!parameter.choices.empty()) {
347 json << ",\"choices\":[";
348 for (size_t c = 0; c < parameter.choices.size(); ++c) {
349 if (c != 0) json << ',';
350 json << '"' << jsonEscape(parameter.choices[c]) << '"';
351 }
352 json << ']';
353 }
354 json << '}';
355 }
356 json << "],\"returnType\":\"" << jsonEscape(returnType)
357 << "\",\"returnNullable\":" << (returnNullable ? "true" : "false") << ",\"ownership\":\""
358 << jsonEscape(ownership) << "\",\"threadAffinity\":\"" << jsonEscape(threadAffinity) << "\",\"platforms\":[";
359 for (size_t i = 0; i < platforms.size(); ++i) {
360 if (i != 0) json << ',';
361 json << '"' << jsonEscape(platforms[i]) << '"';
362 }
363 json << "],\"documentationId\":\"" << jsonEscape(documentationId) << "\"}";
364 return json.str();
365}
366
368 ensureUnique();
369 storage_->contracts[contract.key()] = std::move(contract);
370}
371
373 ensureUnique();
374 return storage_->contracts.erase(std::string(key)) != 0;
375}
376
377const BindingContract* BindingContractRegistry::find(std::string_view key) const noexcept {
378 const auto found = storage_->contracts.find(std::string(key));
379 return found == storage_->contracts.end() ? nullptr : &found->second;
380}
381
382const BindingContract* BindingContractRegistry::findMethod(std::string_view method) const noexcept {
383 const BindingContract* result = nullptr;
384 for (const auto& [_, contract] : storage_->contracts) {
385 if (contract.method != method) continue;
386 if (result != nullptr) return nullptr;
387 result = &contract;
388 }
389 return result;
390}
391
392const BindingContract* BindingContractRegistry::findMethod(std::string_view scriptClass,
393 std::string_view method) const noexcept {
394 const BindingContract* result = nullptr;
395 for (const auto& [_, contract] : storage_->contracts) {
396 if (contract.scriptClass != scriptClass || contract.method != method) continue;
397 if (result != nullptr) return nullptr;
398 result = &contract;
399 }
400 return result;
401}
402
403bool BindingContractRegistry::hasScriptClass(std::string_view scriptClass) const noexcept {
404 for (const auto& [_, contract] : storage_->contracts) {
405 if (contract.scriptClass == scriptClass) return true;
406 }
407 return false;
408}
409
410std::vector<BindingContract> BindingContractRegistry::snapshot() const {
411 std::vector<BindingContract> result;
412 result.reserve(storage_->contracts.size());
413 for (const auto& [_, contract] : storage_->contracts) result.push_back(contract);
414 std::sort(result.begin(), result.end(),
415 [](const BindingContract& a, const BindingContract& b) { return a.key() < b.key(); });
416 return result;
417}
418
419std::vector<BindingSearchHit> BindingContractRegistry::search(std::string_view query, std::string_view moduleFilter,
420 std::string_view classFilter, size_t limit) const {
421 if (limit == 0) limit = 20;
422 if (limit > 64) limit = 64;
423 const std::string queryLower = asciiLower(query);
424 const std::string moduleLower = asciiLower(moduleFilter);
425 const std::string classLower = asciiLower(classFilter);
426 const auto tokens = splitTokens(queryLower);
427 std::vector<BindingSearchHit> hits;
428 for (const auto& [_, contract] : storage_->contracts) {
429 if (!moduleLower.empty() && asciiLower(contract.module) != moduleLower) continue;
430 if (!classLower.empty() && asciiLower(contract.scriptClass) != classLower) continue;
431 const int score = scoreContract(contract, queryLower, tokens);
432 if (score <= 0) continue;
433 hits.push_back({contract, score});
434 }
435 std::sort(hits.begin(), hits.end(), [](const BindingSearchHit& a, const BindingSearchHit& b) {
436 if (a.score != b.score) return a.score > b.score;
437 return a.contract.key() < b.contract.key();
438 });
439 if (hits.size() > limit) hits.resize(limit);
440 return hits;
441}
442
443void BindingContractRegistry::ensureUnique() {
444 if (storage_.use_count() != 1) storage_ = std::make_shared<Storage>(*storage_);
445}
446
448 static const BindingContractRegistry contracts = [] {
451 return result;
452 }();
453 return contracts;
454}
455
456ScriptCompiler::ScriptCompiler(ssq::VM& vm, ScriptModuleResolver& modules) : vm_(&vm), modules_(&modules) {
457 bindings_ = generatedBindingContracts();
458 const SQInteger top = sq_gettop(vm_->getHandle());
459 sq_pushroottable(vm_->getHandle());
460 sq_pushnull(vm_->getHandle());
461 while (SQ_SUCCEEDED(sq_next(vm_->getHandle(), -2))) {
462 if (sq_gettype(vm_->getHandle(), -2) == OT_STRING) {
463 const SQChar* name = nullptr;
464 if (SQ_SUCCEEDED(sq_getstring(vm_->getHandle(), -2, &name)) && name != nullptr) nativeGlobals_.insert(name);
465 }
466 sq_pop(vm_->getHandle(), 2);
467 }
468 sq_settop(vm_->getHandle(), top);
469 {
470 std::lock_guard lock(compilerRegistryMutex);
471 compilerRegistry[vm_->getHandle()] = this;
472 }
473 sq_setnamedargresolver(
474 vm_->getHandle(),
475 [](HSQUIRRELVM, const SQChar* callee, SQInteger index, const SQChar** type, SQBool* nullable,
476 const SQChar** unit, const SQChar** choices, SQUserPointer user) -> const SQChar* {
477 const auto& self = *static_cast<ScriptCompiler*>(user);
478 if (self.nativeGlobals_.find(callee) != self.nativeGlobals_.end()) return nullptr;
479 const BindingContract* contract = self.bindings_.findMethod(callee);
480 if (contract == nullptr || index < 0 || static_cast<size_t>(index) >= contract->parameters.size())
481 return nullptr;
482 const BindingParameterContract& parameter = contract->parameters[static_cast<size_t>(index)];
483 if (type != nullptr) *type = parameter.type.c_str();
484 if (nullable != nullptr) *nullable = parameter.nullable ? SQTrue : SQFalse;
485 static const char* units[] = {nullptr, "seconds", "milliseconds", "radians", "degrees", "pixels", "meters"};
486 if (unit != nullptr) *unit = units[static_cast<size_t>(parameter.unit)];
487 if (choices != nullptr) {
488 static thread_local std::string choiceBuffer;
489 choiceBuffer.clear();
490 for (size_t i = 0; i < parameter.choices.size(); ++i) {
491 if (i != 0) choiceBuffer += ',';
492 choiceBuffer += parameter.choices[i];
493 }
494 *choices = choiceBuffer.empty() ? nullptr : choiceBuffer.c_str();
495 }
496 return parameter.name.c_str();
497 },
498 this);
499 sq_setannotationresolver(
500 vm_->getHandle(),
501 [](HSQUIRRELVM, const SQChar* annotation, SQUserPointer user) -> SQBool {
502 const auto& names = static_cast<ScriptCompiler*>(user)->annotations_;
503 return names.find(annotation) == names.end() ? SQFalse : SQTrue;
504 },
505 this);
506}
507
508ScriptCompiler::~ScriptCompiler() {
509 {
510 std::lock_guard lock(compilerRegistryMutex);
511 compilerRegistry.erase(vm_->getHandle());
512 }
513 sq_setannotationresolver(vm_->getHandle(), nullptr, nullptr);
514 sq_setnamedargresolver(vm_->getHandle(), nullptr, nullptr);
515}
516
517ssq::Script ScriptCompiler::compileSource(std::string_view source, std::string_view sourceName) {
518 const std::string uri(sourceName);
519 modules_->beginCompilation(uri);
520 ScriptMetadata next = analyze(source, uri);
521 try {
522 ssq::Script compiled = vm_->compileSource(std::string(source).c_str(), uri.c_str());
523 modules_->prepareDependencies(uri);
524 next.dependencies = modules_->dependencies(uri);
525 metadata_[uri] = std::move(next);
526 refreshDependencyMetadata();
527 return compiled;
528 } catch (const std::exception& error) {
529 const auto failure = modules_->takeCompilationFailure();
530 next.diagnostics.push_back(makeDiagnostic(failure ? *failure : error.what(), uri));
531 if (failure) {
532 metadata_[uri] = std::move(next);
533 throw std::runtime_error(*failure);
534 }
535 metadata_[uri] = std::move(next);
536 throw;
537 }
538}
539
540ssq::Script ScriptCompiler::compileFile(std::string_view path) {
541 const std::string uri(path);
542 ScriptMetadata next;
543 std::ifstream input(uri, std::ios::binary);
544 if (input) {
545 const std::string source(std::istreambuf_iterator<char>(input), {});
546 next = analyze(source, uri);
547 } else {
548 next.canonicalUri = uri;
549 next.sourceMap.canonicalUri = uri;
550 }
551 modules_->beginCompilation(uri);
552 try {
553 ssq::Script compiled = vm_->compileFile(uri.c_str());
554 modules_->prepareDependencies(uri);
555 next.dependencies = modules_->dependencies(uri);
556 metadata_[uri] = std::move(next);
557 refreshDependencyMetadata();
558 return compiled;
559 } catch (const std::exception& error) {
560 const auto failure = modules_->takeCompilationFailure();
561 next.diagnostics.push_back(makeDiagnostic(failure ? *failure : error.what(), uri));
562 if (failure) {
563 metadata_[uri] = std::move(next);
564 throw std::runtime_error(*failure);
565 }
566 metadata_[uri] = std::move(next);
567 throw;
568 }
569}
570
571eve::OptionalRef<const ScriptMetadata> ScriptCompiler::metadata(std::string_view canonicalUri) const noexcept {
572 const auto found = metadata_.find(std::string(canonicalUri));
573 return found == metadata_.end() ? eve::OptionalRef<const ScriptMetadata>{}
574 : eve::OptionalRef<const ScriptMetadata>{std::cref(found->second)};
575}
576
577std::vector<ScriptMetadata> ScriptCompiler::metadataSnapshot() const {
578 std::vector<ScriptMetadata> result;
579 result.reserve(metadata_.size());
580 for (const auto& [_, metadata] : metadata_) result.push_back(metadata);
581 std::sort(result.begin(), result.end(),
582 [](const ScriptMetadata& a, const ScriptMetadata& b) { return a.canonicalUri < b.canonicalUri; });
583 return result;
584}
585
586void ScriptCompiler::refreshDependencyMetadata() {
587 for (auto& [_, metadata] : metadata_) metadata.reverseDependencies.clear();
588 for (const auto& [importer, metadata] : metadata_) {
589 for (const std::string& dependency : metadata.dependencies) {
590 const auto found = metadata_.find(dependency);
591 if (found != metadata_.end()) appendUnique(found->second.reverseDependencies, importer);
592 }
593 }
594 for (auto& [_, metadata] : metadata_)
595 std::sort(metadata.reverseDependencies.begin(), metadata.reverseDependencies.end());
596}
597
598BindingContractRegistry& ScriptCompiler::bindings() noexcept { return bindings_; }
599const BindingContractRegistry& ScriptCompiler::bindings() const noexcept { return bindings_; }
600
601void ScriptCompiler::registerAnnotation(std::string name) {
602 if (!name.empty()) annotations_.insert(std::move(name));
603}
604
605bool ScriptCompiler::unregisterAnnotation(std::string_view name) { return annotations_.erase(std::string(name)) != 0; }
606
607std::vector<ScriptCompletion> ScriptCompiler::completions(std::string_view canonicalUri,
608 std::string_view prefix) const {
609 std::vector<ScriptCompletion> result;
610 const auto matches = [prefix](std::string_view value) { return value.rfind(prefix, 0) == 0; };
611 if (const auto unit = metadata(canonicalUri)) {
612 for (const ScriptSymbolMetadata& symbol : unit->get().symbols) {
613 if (matches(symbol.name)) result.push_back({symbol.name, symbol.kind, symbol.erasedType, symbol.name});
614 }
615 for (const std::string& name : unit->get().exports) {
616 if (matches(name)) result.push_back({name, "export", "module export", name});
617 }
618 }
619 for (const BindingContract& contract : bindings_.snapshot()) {
620 if (!matches(contract.method)) continue;
621 std::string signature = contract.method + "(";
622 std::string insertion = contract.method + "(";
623 for (size_t i = 0; i < contract.parameters.size(); ++i) {
624 if (i != 0) {
625 signature += ", ";
626 insertion += ", ";
627 }
628 signature += contract.parameters[i].name + ": " + contract.parameters[i].type;
629 insertion += contract.parameters[i].name + ": ";
630 }
631 signature += ") -> " + contract.returnType;
632 insertion += ')';
633 result.push_back({contract.method, "function", std::move(signature), std::move(insertion)});
634 }
635 std::sort(result.begin(), result.end(), [](const ScriptCompletion& a, const ScriptCompletion& b) {
636 if (a.label != b.label) return a.label < b.label;
637 return a.kind < b.kind;
638 });
639 result.erase(std::unique(result.begin(), result.end(),
640 [](const ScriptCompletion& a, const ScriptCompletion& b) {
641 return a.label == b.label && a.kind == b.kind;
642 }),
643 result.end());
644 return result;
645}
646
647std::optional<ScriptHover> ScriptCompiler::hover(std::string_view canonicalUri, std::string_view symbol) const {
648 if (const auto unit = metadata(canonicalUri)) {
649 for (const ScriptSymbolMetadata& candidate : unit->get().symbols) {
650 if (candidate.name == symbol)
651 return ScriptHover{candidate.name,
652 "`" + candidate.kind + " " + candidate.name + ": " + candidate.erasedType + "`",
653 candidate.position};
654 }
655 }
656 if (const BindingContract* contract = bindings_.findMethod(symbol)) {
657 std::string markdown = "`" + contract->method + "(";
658 for (size_t i = 0; i < contract->parameters.size(); ++i) {
659 if (i != 0) markdown += ", ";
660 markdown += contract->parameters[i].name + ": " + contract->parameters[i].type;
661 }
662 markdown += ") -> " + contract->returnType + "`";
663 if (!contract->documentationId.empty()) markdown += "\n\n" + contract->documentationId;
664 return ScriptHover{contract->method, std::move(markdown), {1, 1}};
665 }
666 return std::nullopt;
667}
668
669std::vector<ScriptDiagnostic> ScriptCompiler::diagnostics(std::string_view canonicalUri) const {
670 const auto unit = metadata(canonicalUri);
671 return unit ? unit->get().diagnostics : std::vector<ScriptDiagnostic>{};
672}
673
674bool ScriptCompiler::setSourceMap(std::string_view canonicalUri, ScriptSourceMap sourceMap) {
675 const auto found = metadata_.find(std::string(canonicalUri));
676 if (found == metadata_.end()) return false;
677 sourceMap.canonicalUri = found->first;
678 found->second.sourceMap = std::move(sourceMap);
679 return true;
680}
681
682ScriptSourcePosition ScriptCompiler::toOriginalPosition(std::string_view source, ScriptSourcePosition generated) {
683 return mapRegisteredPosition(source, generated, [](const ScriptSourceMap& map, ScriptSourcePosition position) {
684 return map.originalPosition(position);
685 });
686}
687
688ScriptSourcePosition ScriptCompiler::toGeneratedPosition(std::string_view source, ScriptSourcePosition original) {
689 return mapRegisteredPosition(source, original, [](const ScriptSourceMap& map, ScriptSourcePosition position) {
690 return map.generatedPosition(position);
691 });
692}
693
694SQRESULT ScriptCompiler::compileBuffer(HSQUIRRELVM vm, const SQChar* source, SQInteger size, const SQChar* sourceName,
695 SQBool raiseError) {
696 ScriptCompiler* compiler = nullptr;
697 {
698 std::lock_guard lock(compilerRegistryMutex);
699 const auto found = compilerRegistry.find(vm);
700 if (found != compilerRegistry.end()) compiler = found->second;
701 }
702 if (compiler == nullptr) return sq_compilebuffer(vm, source, size, sourceName, raiseError);
703
704 const std::string_view text(source, static_cast<size_t>(size));
705 const std::string uri = sourceName == nullptr ? "buffer" : sourceName;
706 ScriptMetadata next = analyze(text, uri);
707 compiler->modules_->beginCompilation(uri);
708 const SQInteger top = sq_gettop(vm);
709 const SQRESULT result = sq_compilebuffer(vm, source, size, sourceName, raiseError);
710 if (SQ_FAILED(result)) {
712 ScriptDiagnostic diagnostic;
713 diagnostic.code = diagnosticCode(context.message);
714 diagnostic.message = context.message.empty() ? "script compilation failed" : context.message;
715 diagnostic.canonicalUri = context.source.empty() ? uri : context.source;
716 diagnostic.position.line = static_cast<uint32_t>(std::max(context.line, 1));
717 diagnostic.position.column = static_cast<uint32_t>(std::max(context.column, 1));
718 next.diagnostics.push_back(std::move(diagnostic));
719 compiler->metadata_[uri] = std::move(next);
720 return result;
721 }
722 try {
723 compiler->modules_->prepareDependencies(uri);
724 } catch (const std::exception& error) {
725 sq_settop(vm, top);
726 next.diagnostics.push_back(makeDiagnostic(error.what(), uri));
727 compiler->metadata_[uri] = std::move(next);
728 return sq_throwerror(vm, error.what());
729 }
730 next.dependencies = compiler->modules_->dependencies(uri);
731 compiler->metadata_[uri] = std::move(next);
732 compiler->refreshDependencyMetadata();
733 return SQ_OK;
734}
735
736std::vector<ScriptDiagnostic> ScriptCompiler::validateProjectConfig(std::string_view source,
737 std::string_view canonicalUri,
738 const std::vector<std::string>& knownModules,
739 const std::vector<std::string>& activeModules) {
740 std::vector<ScriptDiagnostic> result;
741 const std::string scanned = maskComments(source);
742 static const std::regex listPattern(R"(\b(modules|optionalModules)\s*=\s*\[([^\]]*)\])");
743 static const std::regex valuePattern(R"([\"']([^\"']+)[\"'])");
744 forMatches(scanned, listPattern, [&](const std::smatch& list, size_t listOffset) {
745 const bool required = list[1].str() == "modules";
746 const std::string values = list[2].str();
747 forMatches(values, valuePattern, [&](const std::smatch& value, size_t valueOffset) {
748 const std::string name = value[1].str();
749 const bool known = std::find(knownModules.begin(), knownModules.end(), name) != knownModules.end();
750 const bool active = std::find(activeModules.begin(), activeModules.end(), name) != activeModules.end();
751 if (known && (!required || active)) return;
752 ScriptDiagnostic diagnostic;
753 diagnostic.code = known ? "EVE1002" : "EVE1001";
754 diagnostic.message = known ? "required module is absent from the selected profile: " + name
755 : "config contains an unknown module root slot: " + name;
756 diagnostic.canonicalUri = std::string(canonicalUri);
757 diagnostic.position = positionAt(source, listOffset + static_cast<size_t>(list.position(2)) + valueOffset);
758 diagnostic.related = name;
759 diagnostic.fix = known ? "enable the module in the selected build profile"
760 : "use a root slot from the generated Module Contract";
761 result.push_back(std::move(diagnostic));
762 });
763 });
764 return result;
765}
766
767ScriptMetadata ScriptCompiler::analyze(std::string_view source, std::string_view canonicalUri) {
768 ScriptMetadata result;
769 result.sourceHash = hashSource(source);
770 result.canonicalUri = std::string(canonicalUri);
771 result.providerOrigin = std::string(canonicalUri);
772 result.sourceMap.canonicalUri = result.canonicalUri;
773 result.sourceMap.entries.push_back({{1, 1}, {1, 1}});
774 const std::string scanned = maskComments(source);
775
776 static const std::regex importPattern(
777 R"(\bimport\s+(?:\*\s+as\s+[A-Za-z_]\w*|\{[^}]*\})\s+from\s+[\"']([^\"']+)[\"'])");
778 static const std::regex exportPattern(R"(\bexport\s+(?:function|class|const)\s+([A-Za-z_]\w*))");
779 static const std::regex persistPattern(R"(\bpersist\s+([A-Za-z_]\w*)\s*(?::[^=]+)?=)");
780 static const std::regex symbolPattern(R"(\b(local|function|class)\s+([A-Za-z_]\w*)\s*(?::\s*([^=,\)\{\n]+))?)");
781 static const std::regex asyncPattern(R"(\basync\s+function\s+([A-Za-z_]\w*))");
782 static const std::regex awaitPattern(R"(\bawait\b)");
783 static const std::regex propertyPattern(
784 R"(((?:[ \t]*@[A-Za-z_]\w*\‍([^\r\n]*\)[ \t]*\r?\n)+)[ \t]*([A-Za-z_]\w*)[ \t]*:[ \t]*([^=\r\n]+)[ \t]*=)");
785 static const std::regex annotationPattern(R"(@([A-Za-z_]\w*)\‍(([^)]*)\))");
786
787 forMatches(scanned, importPattern, [&](const std::smatch& match, size_t) {
788 appendUnique(result.imports, match[1].str());
789 appendUnique(result.moduleReferences, match[1].str());
790 });
791 forMatches(scanned, exportPattern,
792 [&](const std::smatch& match, size_t) { appendUnique(result.exports, match[1].str()); });
793 forMatches(scanned, persistPattern, [&](const std::smatch& match, size_t offset) {
794 appendUnique(result.persistRoots, match[1].str());
795 result.symbols.push_back({match[1].str(), "persist", "dynamic", positionAt(source, offset)});
796 });
797 forMatches(scanned, symbolPattern, [&](const std::smatch& match, size_t offset) {
798 result.symbols.push_back({match[2].str(), match[1].str(), match[3].matched ? match[3].str() : "dynamic",
799 positionAt(source, offset)});
800 });
801 forMatches(scanned, asyncPattern,
802 [&](const std::smatch& match, size_t) { appendUnique(result.asyncFunctions, match[1].str()); });
803 forMatches(scanned, awaitPattern,
804 [&](const std::smatch&, size_t offset) { result.awaitLocations.push_back(positionAt(source, offset)); });
805 forMatches(scanned, propertyPattern, [&](const std::smatch& match, size_t offset) {
806 ScriptPropertyMetadata property;
807 property.name = match[2].str();
808 property.erasedType = trim(match[3].str());
809 property.position = positionAt(source, offset + static_cast<size_t>(match.position(2)));
810 const std::string annotations = match[1].str();
811 forMatches(annotations, annotationPattern, [&](const std::smatch& annotation, size_t) {
812 const std::string name = annotation[1].str();
813 const std::vector<std::string> args = splitArguments(annotation[2].str());
814 for (size_t i = 0; i < args.size(); ++i) {
815 const size_t colon = args[i].find(':');
816 if (i == 0 && colon == std::string::npos)
817 property.attributes[name] = attributeValue(args[i]);
818 else if (colon != std::string::npos)
819 property.attributes[trim(std::string_view(args[i]).substr(0, colon))] =
820 attributeValue(std::string_view(args[i]).substr(colon + 1));
821 }
822 });
823 property.choices = typeChoices(property.erasedType);
824 const auto editor = property.attributes.find("editor");
825 if (editor != property.attributes.end() && editor->second == "combo" && !property.choices.empty()) {
826 std::string options;
827 for (size_t i = 0; i < property.choices.size(); ++i) {
828 if (i != 0) options += ',';
829 options += property.choices[i];
830 }
831 property.attributes["options"] = std::move(options);
832 }
833 result.properties.push_back(std::move(property));
834 });
835 return result;
836}
837
838} // namespace eve::script
double value
SQInteger top
bool & active
double score
Definition Agent.cpp:50
ActiveSource owned
std::unordered_map< std::string, QuestRuntime > entries
struct SQVM * HSQUIRRELVM
int column
HSQUIRRELVM vm
Definition ECS.cpp:20
HSQOBJECT cls
Definition ECS.cpp:21
std::map< std::string, Var > values
std::uint16_t method
std::array< std::uint8_t, 32 > hash
Definition Evpack.cpp:172
EvpackChunkInput input
Definition Evpack.cpp:170
std::string pattern
std::string message
std::unordered_map< const Graphics *, std::shared_ptr< Lifetime > > tokens
std::uint32_t key
HexVec3 left
HexVec3 right
std::int32_t c
std::string text
size_t offset
char quote
std::array< float, 3 > position
bool required
std::set< std::string > choices
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
onnx_detail::CompilerQueue compiler
Definition OnnxGpgpu.cpp:45
std::string error
Definition Package.cpp:60
std::string path
Definition PlayHost.cpp:110
std::string uri
float begin
uint64_t token
bool found
double current
const SquirrelValueOptions & options
TacticalUnit * unit
std::vector< UnitCandidate > units
int limit
Definition TreeMesh.cpp:164
float size
Definition TreeMesh.cpp:156
uint32_t index
const UnitySourceAsset & source
const VegetationPresetContext & context
ViewPreparation callback
Registry consumed by compiler checks and tooling protocol adapters.
void registerContract(BindingContract contract)
Adds or atomically replaces a binding contract by key.
const BindingContract * find(std::string_view key) const noexcept
Finds a binding contract.
std::vector< BindingSearchHit > search(std::string_view query, std::string_view moduleFilter={}, std::string_view classFilter={}, size_t limit=20) const
Ranked substring/token search over keys, classes, methods and documentation ids.
bool hasScriptClass(std::string_view scriptClass) const noexcept
True when generated contracts include this script class.
std::vector< BindingContract > snapshot() const
Returns a stable snapshot sorted by contract key.
const BindingContract * findMethod(std::string_view method) const noexcept
Finds a uniquely named method.
bool unregisterContract(std::string_view key)
Removes a binding contract.
Unified Runtime compiler facade for metadata, diagnostics, and module preparation.
ScriptCompiler(ssq::VM &vm, ScriptModuleResolver &modules)
Script compiler.
Owns providers, the static dependency graph, compiled closures, and module instances.
I * query()
Queries .
Definition Capability.h:88
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
Definition HexMetrics.h:76
ScriptUnit
Unit attached to a binding parameter or return value.
ScriptErrorContext captureCompileError(HSQUIRRELVM vm)
Captures the last compilation error recorded by the VM.
const BindingContractRegistry & generatedBindingContracts()
Process-wide generated Binding Contracts for this linked engine build.
bool parseCompileError(const std::string &text, std::string *source, int *line, int *column, std::string *message)
Parses the ssq compile message "Compile error at source:line:column msg".
EVENGINE_API_FOUNDATION void registerEngineBindingContracts(BindingContractRegistry &registry)
Registers the build-generated contracts for every SimpleSquirrel binding.
std::optional< std::reference_wrapper< T > > OptionalRef
Optional borrowed reference; it does not extend T's lifetime.
Definition BorrowedRef.h:24
Complete script-facing contract for a native function or method.
std::string key() const
Stable lookup key in the form module/class.method.
std::vector< BindingParameterContract > parameters
std::vector< std::string > platforms
std::string toJson() const
Compact JSON object for one contract (eve.binding-api item).
std::string std::string scriptClass
One native binding parameter visible to EveScript tools and named arguments.
std::vector< std::string > choices
std::optional< std::string > scriptDefault
Ranked Binding Contract hit from text search.
Stable structured diagnostic shared by Runtime, LSP, MCP, and editor tools.
ScriptSourcePosition position
Structured snapshot of a script error: message, throw site and stack.
Definition ScriptError.h:25
Tool-neutral hover result for one script or binding symbol.
ScriptSourcePosition position
Metadata retained for one compiled EveScript source unit.
std::vector< std::string > imports
std::vector< std::string > moduleReferences
std::vector< ScriptSourcePosition > awaitLocations
std::vector< ScriptPropertyMetadata > properties
std::vector< std::string > asyncFunctions
std::vector< std::string > persistRoots
std::vector< ScriptSymbolMetadata > symbols
std::vector< std::string > exports
Inspector-facing metadata for one annotated script property.
std::vector< std::string > choices
std::unordered_map< std::string, std::string > attributes
One generated-to-original mapping segment. Native syntax uses identity mappings.
Source map for a compiled script.
ScriptSourcePosition generatedPosition(ScriptSourcePosition original) const noexcept
Maps an original location to the nearest generated location.
std::vector< ScriptSourceMapEntry > entries
ScriptSourcePosition originalPosition(ScriptSourcePosition generated) const noexcept
Maps a generated location back to the nearest original location.
Original EveScript source position. Lines and columns are one-based.
Erased source-level symbol retained for tooling.