Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca7e980d49 | ||
|
|
4a9883b42c | ||
|
|
b576b347b1 | ||
|
|
60c34b6907 | ||
|
|
e3a1e5633f | ||
|
|
681b82c230 | ||
|
|
948a4bf25f | ||
|
|
8b0e330c25 | ||
|
|
0bc422d7be | ||
|
|
2895a18ca9 | ||
|
|
7a6defab18 |
@@ -290,14 +290,19 @@ jobs:
|
||||
- if: needs.changes.outputs.code == 'true' && success()
|
||||
run: npm ci
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
if: needs.changes.outputs.code == 'true' && success()
|
||||
if: >-
|
||||
!cancelled() && needs.changes.outputs.code == 'true' &&
|
||||
(needs.test-shard.result == 'success' || needs.test-shard.result == 'failure')
|
||||
with:
|
||||
path: .vitest/blob
|
||||
pattern: vitest-report-*
|
||||
merge-multiple: true
|
||||
- name: Merge coverage, JUnit, and timing reports
|
||||
id: merge
|
||||
if: needs.changes.outputs.code == 'true' && needs.changes.outputs.tier == 'full' && success()
|
||||
if: >-
|
||||
!cancelled() && needs.changes.outputs.code == 'true' &&
|
||||
needs.changes.outputs.tier == 'full' &&
|
||||
(needs.test-shard.result == 'success' || needs.test-shard.result == 'failure')
|
||||
run: >-
|
||||
npm run test:ci:merge --
|
||||
--coverage.reportsDirectory=coverage
|
||||
@@ -312,7 +317,10 @@ jobs:
|
||||
--outputFile.json=test-results.json
|
||||
- name: Merge fast-suite JUnit and timing reports
|
||||
id: merge-fast
|
||||
if: needs.changes.outputs.code == 'true' && needs.changes.outputs.tier == 'fast' && success()
|
||||
if: >-
|
||||
!cancelled() && needs.changes.outputs.code == 'true' &&
|
||||
needs.changes.outputs.tier == 'fast' &&
|
||||
(needs.test-shard.result == 'success' || needs.test-shard.result == 'failure')
|
||||
run: >-
|
||||
npm exec -- vitest --merge-reports
|
||||
--reporter=default
|
||||
|
||||
+1
-2
@@ -182,8 +182,7 @@ The separate GitHub Pages host uses only the manual
|
||||
## Development feedback and PR verification
|
||||
|
||||
For routine merges, wait for the required checks to pass for the current PR
|
||||
revision against the current `main`. Branch protection requires up-to-date
|
||||
branches; administrators retain an override for deliberate maintainer recovery.
|
||||
revision. Administrators retain an override for deliberate maintainer recovery.
|
||||
If `main` is broken, prioritize a focused repair before updating affected PRs.
|
||||
|
||||
Select checks for the behavior changed. For a source edit:
|
||||
|
||||
@@ -244,6 +244,20 @@ def _hex(value):
|
||||
return "0x%x" % value
|
||||
|
||||
|
||||
def _source_location(address):
|
||||
"""Preserve available line-entry facets without acquiring source files."""
|
||||
entry = address.GetLineEntry()
|
||||
if not entry.IsValid():
|
||||
return None
|
||||
file_spec = entry.GetFileSpec()
|
||||
file_path = str(file_spec) if file_spec.IsValid() else None
|
||||
line = entry.GetLine() or None
|
||||
column = entry.GetColumn() or None
|
||||
if not file_path and line is None and column is None:
|
||||
return None
|
||||
return {"file_path": file_path or None, "line": line, "column": column}
|
||||
|
||||
|
||||
def _location(address, target):
|
||||
"""Load and file address, module and symbol of one code address."""
|
||||
module = address.GetModule()
|
||||
@@ -257,6 +271,7 @@ def _location(address, target):
|
||||
"module": module.GetFileSpec().GetFilename() if module.IsValid() else None,
|
||||
"module_path": str(module.GetFileSpec()) if module.IsValid() else None,
|
||||
"symbol": symbol.GetName() if symbol.IsValid() else None,
|
||||
"source_location": _source_location(address),
|
||||
}
|
||||
return location
|
||||
|
||||
|
||||
@@ -22,6 +22,45 @@ spec.loader.exec_module(tracer)
|
||||
|
||||
|
||||
class NativeTraceBudgetTests(unittest.TestCase):
|
||||
def test_source_coordinates_preserve_partial_metadata_without_reading_source(self):
|
||||
def address(valid=True, file_valid=True, line=23, column=7):
|
||||
class SourceFileSpec:
|
||||
def IsValid(self):
|
||||
return file_valid
|
||||
|
||||
def __str__(self):
|
||||
return "/missing/é space/fixture.pas"
|
||||
|
||||
file_spec = SourceFileSpec()
|
||||
entry = types.SimpleNamespace(
|
||||
IsValid=lambda: valid,
|
||||
GetFileSpec=lambda: file_spec,
|
||||
GetLine=lambda: line,
|
||||
GetColumn=lambda: column,
|
||||
)
|
||||
return types.SimpleNamespace(GetLineEntry=lambda: entry)
|
||||
|
||||
self.assertEqual(
|
||||
tracer._source_location(address()),
|
||||
{"file_path": "/missing/é space/fixture.pas", "line": 23, "column": 7},
|
||||
)
|
||||
self.assertEqual(
|
||||
tracer._source_location(address(column=0)),
|
||||
{"file_path": "/missing/é space/fixture.pas", "line": 23, "column": None},
|
||||
)
|
||||
self.assertEqual(
|
||||
tracer._source_location(address(file_valid=False)),
|
||||
{"file_path": None, "line": 23, "column": 7},
|
||||
)
|
||||
self.assertEqual(
|
||||
tracer._source_location(address(line=0)),
|
||||
{"file_path": "/missing/é space/fixture.pas", "line": None, "column": 7},
|
||||
)
|
||||
self.assertIsNone(tracer._source_location(address(valid=False)))
|
||||
self.assertIsNone(
|
||||
tracer._source_location(address(file_valid=False, line=0, column=0))
|
||||
)
|
||||
|
||||
def test_journal_checks_exact_byte_budget_before_writing(self):
|
||||
record = {"kind": "other-stop", "reason": "signal"}
|
||||
encoded_size = len((tracer.json.dumps(record) + "\n").encode("utf-8"))
|
||||
@@ -154,6 +193,9 @@ class NativeTraceBudgetTests(unittest.TestCase):
|
||||
return FileSpec()
|
||||
|
||||
class Address:
|
||||
def GetLineEntry(self):
|
||||
return types.SimpleNamespace(IsValid=lambda: False)
|
||||
|
||||
def GetModule(self):
|
||||
return Module()
|
||||
|
||||
@@ -227,7 +269,24 @@ class NativeTraceBudgetTests(unittest.TestCase):
|
||||
def GetFileSpec(self):
|
||||
return FileSpec()
|
||||
|
||||
class SourceFileSpec:
|
||||
def IsValid(self):
|
||||
return True
|
||||
|
||||
def __str__(self):
|
||||
return source_path
|
||||
|
||||
class Address:
|
||||
def GetLineEntry(self):
|
||||
if source_path is None:
|
||||
return types.SimpleNamespace(IsValid=lambda: False)
|
||||
return types.SimpleNamespace(
|
||||
IsValid=lambda: True,
|
||||
GetFileSpec=SourceFileSpec,
|
||||
GetLine=lambda: 15,
|
||||
GetColumn=lambda: 3,
|
||||
)
|
||||
|
||||
def GetModule(self):
|
||||
return Module()
|
||||
|
||||
@@ -254,19 +313,35 @@ class NativeTraceBudgetTests(unittest.TestCase):
|
||||
breakpoint_count = 1000
|
||||
created = [[Breakpoint()] for _ in range(breakpoint_count)]
|
||||
config = {"breakpoints": [{"kind": "objc-method"}] * breakpoint_count}
|
||||
report, locations_truncated = tracer._breakpoint_report(
|
||||
object(), created, config
|
||||
)
|
||||
retained_bytes = sum(
|
||||
len(json.dumps(location).encode("utf-8"))
|
||||
+ (1 if index > 0 else 0)
|
||||
for row in report
|
||||
for index, location in enumerate(row["locations"])
|
||||
)
|
||||
self.assertTrue(locations_truncated)
|
||||
self.assertEqual(sum(row["location_count"] for row in report), 64_000)
|
||||
self.assertLess(sum(len(row["locations"]) for row in report), 64_000)
|
||||
self.assertLessEqual(retained_bytes, tracer.MAX_BREAKPOINT_LOCATION_BYTES)
|
||||
for source_path in (None, "/tmp/" + "源🚀" * 3000 + "/calls.pas"):
|
||||
with self.subTest(source_path_present=source_path is not None):
|
||||
report, locations_truncated = tracer._breakpoint_report(
|
||||
object(), created, config
|
||||
)
|
||||
retained_bytes = sum(
|
||||
len(json.dumps(location).encode("utf-8"))
|
||||
+ (1 if index > 0 else 0)
|
||||
for row in report
|
||||
for index, location in enumerate(row["locations"])
|
||||
)
|
||||
self.assertTrue(locations_truncated)
|
||||
self.assertEqual(
|
||||
sum(row["location_count"] for row in report), 64_000
|
||||
)
|
||||
self.assertLess(
|
||||
sum(len(row["locations"]) for row in report), 64_000
|
||||
)
|
||||
self.assertLessEqual(
|
||||
retained_bytes, tracer.MAX_BREAKPOINT_LOCATION_BYTES
|
||||
)
|
||||
if source_path is not None:
|
||||
self.assertTrue(
|
||||
any(
|
||||
location["source_location"]["file_path"] == source_path
|
||||
for row in report
|
||||
for location in row["locations"]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -426,6 +426,25 @@ arguments. The receiver class comes from LLDB's Objective-C runtime reader
|
||||
without running target code; no expression is evaluated, and REA never
|
||||
attaches to a process it did not launch.
|
||||
|
||||
Resolved breakpoint locations, entries and caller frames also retain
|
||||
`source_location` from LLDB's line tables when available. Its `file_path`,
|
||||
one-based `line` and one-based `column` are independently nullable. An invalid
|
||||
line entry is null; historical captures may omit the field. This is debug metadata
|
||||
attribution: REA does not read that source path or verify the source file bytes.
|
||||
Absent coordinates do not imply that the executable has no source code.
|
||||
|
||||
Free Pascal programs with retained native symbols and usable DWARF can use this
|
||||
same bounded call observation on macOS. Select an exact emitted symbol from the
|
||||
binary rather than assuming a source procedure name. The owned Pascal fixture
|
||||
uses an explicit C calling convention and a public symbol; its verification
|
||||
covers call entries, raw integer registers and source coordinates, without
|
||||
establishing typed Pascal locals or interactive stepping. After building REA,
|
||||
run the real lane with an existing caller-selected compiler:
|
||||
|
||||
```sh
|
||||
REA_VERIFY_FPC_COMMAND=/absolute/path/to/fpc npm run verify:native:pascal-calls
|
||||
```
|
||||
|
||||
The run ends when the process exits, `max_events` entries are recorded,
|
||||
`duration_ms` elapses, or the aggregate trace resource budget is exhausted.
|
||||
Bounded termination reports partial coverage. REA checks process termination
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Implementation plan
|
||||
|
||||
## Boundary and contract
|
||||
|
||||
Extend nativeCodeLocationSchema in src/domain/native/nativeCallObservation.ts.
|
||||
Reuse that shape for events, breakpoint locations, backtraces and retained journal
|
||||
events. Keep the field optional for older evidence and nullable for current LLDB
|
||||
observations with unavailable metadata. Each inner facet can be independently
|
||||
unavailable; avoid dropping a valid line number because a file path is absent.
|
||||
|
||||
Read SBAddress.GetLineEntry once in the existing Python _location producer. A
|
||||
valid SBFileSpec supplies its displayed path without filesystem access. Convert
|
||||
zero line/column to null. An invalid entry returns null. Existing estimation and
|
||||
serialization budget checks already traverse the location object recursively.
|
||||
|
||||
## Incremental work and validation
|
||||
|
||||
1. Define contracts and criteria before code; add failing producer tests.
|
||||
2. Extend the canonical schema and _location producer. Update existing fake
|
||||
address objects to implement the actual LLDB line-entry interface.
|
||||
3. Extend provider/MCP boundary coverage and retained-journal coverage for the
|
||||
additive field; preserve omission compatibility.
|
||||
4. Extend the existing real macOS verifier with source-coordinate assertions
|
||||
against its own Objective-C/C fixture built with debug information.
|
||||
5. Update the native investigation guide; run Python tests, focused TypeScript
|
||||
tests, repository checks and the real verifier if host authority permits.
|
||||
|
||||
## Effects and compatibility
|
||||
|
||||
The existing LLDB operation already owns its breakpoint callbacks and process
|
||||
lifecycle. This adds a metadata read within that observation. It does not introduce
|
||||
target execution or a new provider beyond existing behavior. No source file is
|
||||
acquired from a path in debug information. Consumers of historical evidence retain
|
||||
omission; consumers of new captures see either a partial/full object or null.
|
||||
|
||||
## References
|
||||
|
||||
- https://lldb.llvm.org/python_api/lldb.SBAddress.html
|
||||
- https://lldb.llvm.org/python_api/lldb.SBLineEntry.html
|
||||
@@ -0,0 +1,36 @@
|
||||
# Native call source coordinates
|
||||
|
||||
Issue: https://github.com/morluto/rea/issues/2382
|
||||
|
||||
## Problem and outcome
|
||||
|
||||
The existing bounded macOS LLDB call observation retains module, symbol and
|
||||
addresses, but loses compiler-provided source coordinates. Retain line-entry
|
||||
metadata at the same producer boundary for breakpoint locations, call entries
|
||||
and caller frames.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Add optional nullable source_location to the canonical native code location.
|
||||
A present object contains nullable file_path, line and column. Positive line
|
||||
and column values are one-based. Provider zero values mean unavailable.
|
||||
- Preserve partial facts from a valid line entry. Invalid entries or entries
|
||||
without any usable facet produce null. Historical captures may omit the field.
|
||||
- Read only SBAddress line metadata; do not resolve paths, read source contents,
|
||||
evaluate expressions or change the existing launch, stop or cleanup policy.
|
||||
- The field establishes LLDB line-table attribution, not verified source bytes.
|
||||
- Existing trace and breakpoint byte budgets include the new representation.
|
||||
|
||||
## Acceptance
|
||||
|
||||
Producer tests cover valid, invalid and partial entries, Unicode paths and absent
|
||||
coordinates. Existing evidence without the field remains accepted. CLI/MCP and
|
||||
partial retained-journal paths preserve the canonical field. The real native-call
|
||||
verifier checks source coordinates for its owned debug build, including caller
|
||||
frames and breakpoint locations, and reports actual host/provider coverage.
|
||||
|
||||
## Out of scope
|
||||
|
||||
New debugger adapters, interactive stepping, source breakpoints, source-file
|
||||
association verification, source reads, Pascal/Delphi compatibility claims and
|
||||
Java/Android/CLR runtime support.
|
||||
@@ -0,0 +1,14 @@
|
||||
# Tasks
|
||||
|
||||
The checked items below record work completed on an earlier revision. The
|
||||
current revision's final caller-frame assertions, marker-derived line
|
||||
expectations, timeout handling, and Unicode budget case remain source-reviewed
|
||||
only; the full native-call verifier is incomplete.
|
||||
|
||||
- [x] Define the single-feature scope, contract and implementation plan.
|
||||
- [x] Reproduce lost source coordinates with producer regression tests.
|
||||
- [x] Retain line-entry metadata in the producer and canonical schema.
|
||||
- [x] Verify boundary/MCP propagation and historical omission.
|
||||
- [x] Historical: verify real CLI/MCP call coordinates on an owned Free Pascal 3.2.2 program.
|
||||
- [x] Historical: update documentation; pass Python, focused TypeScript, static and discovery checks.
|
||||
- [ ] Complete the full native-call verifier after investigating #2397.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Historical verification
|
||||
|
||||
The records below describe earlier revisions, before the final caller-frame
|
||||
assertions, marker-derived line expectations, forced compiler timeout termination,
|
||||
and large Unicode source-location budget coverage. They do not validate the
|
||||
current revision. Those final changes were reviewed from source only; tests and
|
||||
automated checks were not run at the user’s explicit request.
|
||||
|
||||
Host: macOS ARM64. Compiler: caller-installed Free Pascal 3.2.2, Darwin AArch64.
|
||||
Debugger: LLDB 2103.0.34.103 from Command Line Tools.
|
||||
|
||||
- Python producer/budget/output tests: 10 passed.
|
||||
- Focused native provider, retained-journal and MCP tests: 18 passed.
|
||||
- Real Pascal CLI and stdio MCP: three expected calls, integer register values
|
||||
0/1/2, source path and positive line attribution, successful target exit.
|
||||
- Repository static checks: six tasks and architecture guards passed.
|
||||
- Test discovery: all 982 test files discovered exactly once.
|
||||
|
||||
The complete native-call verifier did not pass. One run reported exited instead
|
||||
of event-limit for capped overlapping breakpoints. Another reported unavailable
|
||||
launch start identity in a later capped scenario. Investigation:
|
||||
https://github.com/morluto/rea/issues/2397. These results have not yet been isolated
|
||||
on unchanged upstream. The source-coordinate PR was draft at that historical checkpoint.
|
||||
|
||||
The first real native scenario and CLI/MCP source-coordinate comparisons completed
|
||||
before those broader failures. The Pascal lane verifies bounded native call
|
||||
observation; it does not establish typed Pascal local values, source breakpoints,
|
||||
interactive stepping, Delphi compatibility or a GDB provider.
|
||||
|
||||
## Unmeasured costs and outstanding execution
|
||||
|
||||
Repeated source paths expand event, caller-frame and breakpoint-location payloads.
|
||||
Equivalent before/after completed workflows have not been measured for agent tokens,
|
||||
elapsed time, CPU time or peak process-family memory, for either cold or warm runs.
|
||||
No byte count is presented as a token or performance measurement. Representative
|
||||
measurements remain outstanding before integration; they were not executed under
|
||||
the current instruction to skip tests and provider workloads.
|
||||
|
||||
The current caller-frame equality checks, marker-derived source lines, forced
|
||||
compiler timeout termination and Unicode source-path budget case remain unverified
|
||||
by execution. Earlier Pascal CLI/MCP observations establish only the historical
|
||||
coverage described above.
|
||||
@@ -246,6 +246,7 @@
|
||||
"prepare": "node scripts/prepare.mjs",
|
||||
"verify:asset-catalog": "npm run build:cached && node scripts/verify/apple/asset-catalog.mjs",
|
||||
"verify:native-calls": "npm run build:cached && node scripts/verify-native-calls.mjs",
|
||||
"verify:native:pascal-calls": "npm run build:cached && node scripts/verify-native-pascal-calls.mjs",
|
||||
"verify:native-ui:permissions": "npm run build:cached && node scripts/verify-native-ui.mjs --allow-permission-denial",
|
||||
"test:focused": "node scripts/run-development-tests.mjs focused",
|
||||
"test:sqlite": "npm run test:prepare && npm run test:sqlite:run",
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { mkdtemp, realpath, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
/** Allocate under a caller-selected parent and return the host's canonical spelling. */
|
||||
export const createCanonicalTemporaryDirectory = async (
|
||||
prefix,
|
||||
parent = tmpdir(),
|
||||
) => {
|
||||
const allocatedPath = await mkdtemp(join(parent, prefix));
|
||||
try {
|
||||
return { path: await realpath(allocatedPath), allocatedPath };
|
||||
} catch (cause) {
|
||||
await rm(allocatedPath, { recursive: true, force: true });
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
@@ -60,6 +60,10 @@ const stable = (result) =>
|
||||
file_address: event.file_address,
|
||||
selector: event.selector,
|
||||
receiver_class: event.receiver_class,
|
||||
source_location: event.source_location,
|
||||
caller_source_locations: event.backtrace.map(
|
||||
(frame) => frame.source_location,
|
||||
),
|
||||
counter:
|
||||
event.symbol === GREET
|
||||
? event.registers[3].value
|
||||
@@ -95,6 +99,18 @@ const checkScenario = async (binary) => {
|
||||
});
|
||||
const greet = result.events.filter(({ symbol }) => symbol === GREET);
|
||||
const add = result.events.filter(({ symbol }) => symbol === "rea_call_add");
|
||||
for (const location of [
|
||||
...result.events,
|
||||
...result.events.flatMap((event) => event.backtrace),
|
||||
...result.breakpoints.flatMap((breakpoint) => breakpoint.locations),
|
||||
]) {
|
||||
assert.equal(location.source_location?.file_path, SOURCE);
|
||||
assert.ok(location.source_location.line > 0);
|
||||
assert.ok(
|
||||
location.source_location.column === null ||
|
||||
location.source_location.column > 0,
|
||||
);
|
||||
}
|
||||
assert.equal(greet.length, 3, JSON.stringify(result));
|
||||
assert.equal(add.length, 3, JSON.stringify(result));
|
||||
for (const [index, event] of greet.entries()) {
|
||||
@@ -267,6 +283,8 @@ try {
|
||||
const binary = join(directory, "ReaCalls");
|
||||
await exec("/usr/bin/xcrun", [
|
||||
"clang",
|
||||
"-g",
|
||||
"-O0",
|
||||
"-fobjc-arc",
|
||||
"-framework",
|
||||
"Foundation",
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { readFile, rm } from "node:fs/promises";
|
||||
import { isAbsolute, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { runOwnedCommand } from "../dist/process/OwnedCommand.js";
|
||||
|
||||
import {
|
||||
artifactCli,
|
||||
artifactMcpResult,
|
||||
withArtifactMcp,
|
||||
} from "./lib/artifact-e2e.mjs";
|
||||
import { createCanonicalTemporaryDirectory } from "./lib/canonical-temporary-directory.mjs";
|
||||
|
||||
const cancellation = new AbortController();
|
||||
let shutdownSignal;
|
||||
const signalExitCodes = { SIGINT: 130, SIGTERM: 143 };
|
||||
const forwardCancellation = (signal) => {
|
||||
if (shutdownSignal !== undefined) return;
|
||||
shutdownSignal = signal;
|
||||
cancellation.abort(new Error(`Verification cancelled by ${signal}`));
|
||||
};
|
||||
const signalHandlers = Object.fromEntries(
|
||||
Object.keys(signalExitCodes).map((signal) => [
|
||||
signal,
|
||||
() => forwardCancellation(signal),
|
||||
]),
|
||||
);
|
||||
for (const [signal, handler] of Object.entries(signalHandlers))
|
||||
process.on(signal, handler);
|
||||
|
||||
const execute = async (command, arguments_, { timeout }) => {
|
||||
const result = await runOwnedCommand(
|
||||
{
|
||||
command,
|
||||
arguments: arguments_,
|
||||
runId: `rea-verify-pascal-${randomUUID()}`,
|
||||
expectedCommand: null,
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
},
|
||||
{ timeoutMs: timeout, diagnosticBytes: 1024 * 1024 },
|
||||
{ signal: cancellation.signal },
|
||||
);
|
||||
return { stdout: result.stdout.text, stderr: result.stderr.text };
|
||||
};
|
||||
const compiler = process.env.REA_VERIFY_FPC_COMMAND;
|
||||
if (process.platform !== "darwin" || !isAbsolute(compiler ?? ""))
|
||||
throw new Error(
|
||||
"Requires macOS and an absolute caller-selected REA_VERIFY_FPC_COMMAND; build the runtime first.",
|
||||
);
|
||||
const source = fileURLToPath(
|
||||
new URL("../tests/conformance/native/calls.pas", import.meta.url),
|
||||
);
|
||||
const fixtureLines = (await readFile(source, "utf8")).split(/\r?\n/u);
|
||||
const callerLines = new Set(
|
||||
["REA_VERIFY_CALLER_LOOP", "REA_VERIFY_CALLER_CALL"].map((marker) => {
|
||||
const matches = fixtureLines.flatMap((line, index) =>
|
||||
line.includes(marker) ? [index + 1] : [],
|
||||
);
|
||||
assert.equal(matches.length, 1, `fixture has one ${marker} marker`);
|
||||
return matches[0];
|
||||
}),
|
||||
);
|
||||
const allocatedDirectory = await createCanonicalTemporaryDirectory(
|
||||
"rea-pascal-calls-",
|
||||
);
|
||||
let directory = allocatedDirectory.path;
|
||||
const input = {
|
||||
breakpoints: [{ kind: "function", name: "rea_pascal_call" }],
|
||||
argument_registers: 1,
|
||||
backtrace_frames: 1,
|
||||
duration_ms: 30_000,
|
||||
};
|
||||
const inspect = (result) => {
|
||||
assert.equal(result.process.outcome, "exited");
|
||||
assert.equal(result.process.exit_status, 0);
|
||||
assert.equal(result.process.terminated, true);
|
||||
assert.equal(result.process.stdout.text, "pascal 0\npascal 1\npascal 2\n");
|
||||
assert.equal(result.events.length, 3);
|
||||
assert.deepEqual(
|
||||
result.events.map((event) => event.registers[0].value),
|
||||
["0x0", "0x1", "0x2"],
|
||||
);
|
||||
assert.equal(result.breakpoints[0].location_count, 1);
|
||||
for (const event of result.events) {
|
||||
assert.equal(event.backtrace.length, 1);
|
||||
assert.equal(event.backtrace[0].source_location?.file_path, source);
|
||||
assert.equal(event.backtrace[0].module_path, join(directory, "calls"));
|
||||
assert.ok(
|
||||
callerLines.has(event.backtrace[0].source_location.line),
|
||||
"caller is the Pascal loop/call site",
|
||||
);
|
||||
}
|
||||
for (const location of [
|
||||
...result.events,
|
||||
...result.breakpoints[0].locations,
|
||||
]) {
|
||||
assert.equal(location.source_location?.file_path, source);
|
||||
assert.ok(location.source_location.line > 0);
|
||||
}
|
||||
};
|
||||
const stable = (result) =>
|
||||
result.events.map((event) => ({
|
||||
file_address: event.file_address,
|
||||
source_location: event.source_location,
|
||||
registers: event.registers,
|
||||
callers: event.backtrace.map((frame) => ({
|
||||
file_address: frame.file_address,
|
||||
symbol: frame.symbol,
|
||||
source_location: frame.source_location,
|
||||
})),
|
||||
}));
|
||||
try {
|
||||
const version = (
|
||||
await execute(compiler, ["-iV"], { timeout: 30_000 })
|
||||
).stdout.trim();
|
||||
const architecture = (
|
||||
await execute(compiler, ["-iTP"], {
|
||||
timeout: 30_000,
|
||||
})
|
||||
).stdout.trim();
|
||||
await execute(
|
||||
compiler,
|
||||
[
|
||||
"-dDEBUG",
|
||||
"-gw3",
|
||||
"-O-",
|
||||
"-Xs-",
|
||||
`-FE${directory}`,
|
||||
`-FU${directory}`,
|
||||
source,
|
||||
],
|
||||
{ timeout: 120_000 },
|
||||
);
|
||||
const binary = join(directory, "calls");
|
||||
const cli = await artifactCli(
|
||||
"observe-native-calls",
|
||||
binary,
|
||||
[JSON.stringify(input)],
|
||||
{ timeoutMs: 180_000 },
|
||||
);
|
||||
inspect(cli);
|
||||
await withArtifactMcp(binary, async (client) => {
|
||||
const mcp = await artifactMcpResult(client, "observe_native_calls", input);
|
||||
inspect(mcp);
|
||||
assert.deepEqual(stable(mcp), stable(cli));
|
||||
});
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
mocked: false,
|
||||
compiler,
|
||||
version,
|
||||
architecture,
|
||||
debugger: cli.debugger,
|
||||
cli: true,
|
||||
stdio_mcp: true,
|
||||
scope:
|
||||
"bounded native call entries and compiler-provided source coordinates; no typed Pascal locals or interactive stepping",
|
||||
}),
|
||||
);
|
||||
} catch (cause) {
|
||||
if (shutdownSignal === undefined) throw cause;
|
||||
const cleanupFailure = cause?.cleanupFailure;
|
||||
if (cleanupFailure !== undefined && cleanupFailure !== null) {
|
||||
console.error(
|
||||
`Verification cancelled by ${shutdownSignal}; owned compiler cleanup is incomplete: ${cleanupFailure}`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.error(
|
||||
`Verification cancelled by ${shutdownSignal}; owned compiler process group was stopped.`,
|
||||
);
|
||||
process.exitCode = signalExitCodes[shutdownSignal];
|
||||
}
|
||||
} finally {
|
||||
for (const signal of Object.keys(signalExitCodes))
|
||||
process.off(signal, signalHandlers[signal]);
|
||||
await rm(allocatedDirectory.allocatedPath, { recursive: true, force: true });
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisInputError,
|
||||
AnalysisOutputError,
|
||||
type AnalysisInputIssue,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import {
|
||||
@@ -41,22 +42,29 @@ export class WasmArtifactService {
|
||||
cause: input.error,
|
||||
}),
|
||||
);
|
||||
if (
|
||||
![
|
||||
input.data.path,
|
||||
...input.data.glue_paths,
|
||||
...input.data.candidate_paths,
|
||||
].every(isAbsolute)
|
||||
)
|
||||
return err(
|
||||
new AnalysisInputError(OPERATION, undefined, [
|
||||
{
|
||||
path: ["path"],
|
||||
reason: "invalid_format",
|
||||
message: "Expected an absolute filesystem path on this host.",
|
||||
},
|
||||
]),
|
||||
);
|
||||
const pathIssues = [
|
||||
{ value: input.data.path, path: ["path"] },
|
||||
...input.data.glue_paths.map((value, index) => ({
|
||||
value,
|
||||
path: ["glue_paths", index],
|
||||
})),
|
||||
...input.data.candidate_paths.map((value, index) => ({
|
||||
value,
|
||||
path: ["candidate_paths", index],
|
||||
})),
|
||||
].flatMap<AnalysisInputIssue>(({ value, path }) =>
|
||||
isAbsolute(value)
|
||||
? []
|
||||
: [
|
||||
{
|
||||
path,
|
||||
reason: "invalid_format",
|
||||
message: "Expected an absolute filesystem path on this host.",
|
||||
},
|
||||
],
|
||||
);
|
||||
if (pathIssues.length > 0)
|
||||
return err(new AnalysisInputError(OPERATION, undefined, pathIssues));
|
||||
const inspected = await this.provider.inspect(input.data, options);
|
||||
if (!inspected.ok) return inspected;
|
||||
if (options?.signal?.aborted)
|
||||
|
||||
@@ -114,6 +114,18 @@ export const nativeCodeLocationSchema = z.strictObject({
|
||||
module: z.string().nullable(),
|
||||
module_path: z.string().nullable(),
|
||||
symbol: z.string().nullable(),
|
||||
/** LLDB line-table attribution; does not verify source file contents. */
|
||||
source_location: z
|
||||
.strictObject({
|
||||
file_path: z.string().min(1).nullable(),
|
||||
line: z.number().int().positive().nullable(),
|
||||
column: z.number().int().positive().nullable(),
|
||||
})
|
||||
.nullable()
|
||||
.optional()
|
||||
.describe(
|
||||
"Compiler or debugger source attribution only; REA does not read or verify the named source-file bytes.",
|
||||
),
|
||||
});
|
||||
|
||||
/** One observed entry into a requested function or method. */
|
||||
|
||||
@@ -44,6 +44,16 @@ const dossier = jsonValueSchema.parse(
|
||||
referenced_names: [],
|
||||
native_api: null,
|
||||
native_value_flow: null,
|
||||
facet_availability: {
|
||||
pseudocode: { available: true },
|
||||
assembly: { available: true },
|
||||
comments: { available: true },
|
||||
callers: { available: true },
|
||||
callees: { available: true },
|
||||
references: { available: true },
|
||||
strings_names: { available: true },
|
||||
cfg: { available: true },
|
||||
},
|
||||
basic_blocks: [{ start: "0x1000", end: "0x1001", successors: [] }],
|
||||
limitations: [],
|
||||
}),
|
||||
|
||||
@@ -46,6 +46,7 @@ export const projectLldbPartialObservation = (input: {
|
||||
limitations: [
|
||||
"The LLDB run did not produce a complete native call observation result.",
|
||||
"The target SHA-256 identifies the selected artifact; LLDB did not verify the bytes loaded into the process.",
|
||||
"Source paths and lines are LLDB line-table attribution; REA did not read or verify the referenced source bytes.",
|
||||
"Captured output byte counts are observed lower bounds because LLDB did not report final stream counters.",
|
||||
...input.journal.limitations,
|
||||
...(input.journalLimitations ?? []),
|
||||
|
||||
@@ -31,6 +31,7 @@ export const NATIVE_CALL_OBSERVATION_LIMITATIONS = [
|
||||
"Registers are the raw integer argument registers at entry. For Objective-C methods the first two hold the receiver and the selector.",
|
||||
"Receiver classes come from LLDB's Objective-C runtime reader without running target code; they are null for class methods and for receivers LLDB cannot classify.",
|
||||
"Calls that never reach a symbol's entry, such as inlined code or objc_direct methods called directly, are not observed.",
|
||||
"Source locations come from LLDB line-table metadata; REA does not read or verify the named source-file contents.",
|
||||
"The target is launched directly under LLDB, not through LaunchServices; LLDB disables ASLR for it, so load addresses can equal file addresses.",
|
||||
"The observation window is checked about once a second, and recording pauses the process at every entry, which slows it.",
|
||||
] as const;
|
||||
|
||||
@@ -45,23 +45,34 @@ const workflow = async () =>
|
||||
),
|
||||
).jobs;
|
||||
|
||||
it("collects failed-shard diagnostics before enforcing the shard result", async () => {
|
||||
it("merges available failed-shard diagnostics and skips unavailable shard reports", async () => {
|
||||
const steps = (await workflow()).test!.steps;
|
||||
const download = steps.find(
|
||||
(step) =>
|
||||
step.uses?.includes("actions/download-artifact") &&
|
||||
step.with?.pattern === "vitest-report-*",
|
||||
);
|
||||
const merge = steps.findIndex((step) => step.run?.includes("test:ci:merge"));
|
||||
const upload = steps.findIndex((step) => step.with?.name === "test-results");
|
||||
const gate = steps.findIndex(
|
||||
(step) => step.env?.SHARD_RESULT && step.run?.includes("test "),
|
||||
);
|
||||
expect(merge).toBeGreaterThan(0);
|
||||
expect(download?.if).toContain("!cancelled()");
|
||||
expect(download?.if).toContain("needs.test-shard.result == 'success'");
|
||||
expect(download?.if).toContain("needs.test-shard.result == 'failure'");
|
||||
expect(upload).toBeGreaterThan(merge);
|
||||
expect(gate).toBeGreaterThan(upload);
|
||||
expect(steps[merge]?.if).not.toContain("needs.test-shard.result");
|
||||
const merges = steps.filter((step) => step.run?.includes("--reporter=json"));
|
||||
expect(merges).toHaveLength(2);
|
||||
for (const step of merges)
|
||||
for (const step of merges) {
|
||||
expect(step.run).toContain(
|
||||
"--reporter=./scripts/ci/test-timing-reporter.mjs",
|
||||
);
|
||||
expect(step.if).toContain("!cancelled()");
|
||||
expect(step.if).toContain("needs.test-shard.result == 'success'");
|
||||
expect(step.if).toContain("needs.test-shard.result == 'failure'");
|
||||
}
|
||||
expect(steps[upload]?.with?.path).toContain("test-timings.json");
|
||||
expect(steps[upload]?.with).toMatchObject({ "include-hidden-files": true });
|
||||
expect(steps[upload]?.with?.path).toContain(
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import {
|
||||
chmod,
|
||||
readFile,
|
||||
readdir,
|
||||
realpath,
|
||||
rm,
|
||||
symlink,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { basename, join } from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { createCanonicalTemporaryDirectory } from "../../../scripts/lib/canonical-temporary-directory.mjs";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
it("uses the canonical spelling for a verifier directory allocated through a symlink", async () => {
|
||||
const root = await createTestTempDirectory("rea-pascal-verifier-path-");
|
||||
const alias = join(root, "alias");
|
||||
await symlink(
|
||||
root,
|
||||
alias,
|
||||
process.platform === "win32" ? "junction" : "dir",
|
||||
);
|
||||
const allocated = await createCanonicalTemporaryDirectory(
|
||||
"rea-pascal-calls-",
|
||||
alias,
|
||||
);
|
||||
try {
|
||||
expect(allocated.path).toBe(await realpath(allocated.allocatedPath));
|
||||
expect(allocated.path).not.toBe(
|
||||
join(alias, basename(allocated.allocatedPath)),
|
||||
);
|
||||
} finally {
|
||||
await rm(allocated.allocatedPath, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it.skipIf(process.platform !== "darwin")(
|
||||
"forwards verifier SIGTERM to the owned compiler group and removes its temporary directory",
|
||||
async () => {
|
||||
const root = await createTestTempDirectory("rea-pascal-verifier-cancel-");
|
||||
const fakeCompiler = join(root, "fake-fpc");
|
||||
const pidFile = join(root, "compiler-pids.txt");
|
||||
await writeFile(
|
||||
fakeCompiler,
|
||||
'#!/usr/bin/env node\nimport { spawn } from "node:child_process";\nimport { writeFileSync } from "node:fs";\nconst child = spawn(process.execPath, ["-e", "setInterval(() => undefined, 1000)"], { stdio: "ignore" });\nwriteFileSync(process.env.REA_VERIFY_TEST_PID_FILE, `${process.pid} ${child.pid}\\n`);\nawait new Promise(() => undefined);\n',
|
||||
);
|
||||
await chmod(fakeCompiler, 0o700);
|
||||
const verifier = spawn(
|
||||
process.execPath,
|
||||
["scripts/verify-native-pascal-calls.mjs"],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
TMPDIR: root,
|
||||
REA_VERIFY_FPC_COMMAND: fakeCompiler,
|
||||
REA_VERIFY_TEST_PID_FILE: pidFile,
|
||||
},
|
||||
stdio: "ignore",
|
||||
},
|
||||
);
|
||||
try {
|
||||
const deadline = Date.now() + 10_000;
|
||||
let pids: number[] | undefined;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
pids = (await readFile(pidFile, "utf8"))
|
||||
.trim()
|
||||
.split(/\s+/u)
|
||||
.map(Number);
|
||||
break;
|
||||
} catch (cause) {
|
||||
if (cause.code !== "ENOENT") throw cause;
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
}
|
||||
}
|
||||
expect(pids, "the fake compiler started").toHaveLength(2);
|
||||
verifier.kill("SIGTERM");
|
||||
const result = await new Promise<{
|
||||
code: number | null;
|
||||
signal: NodeJS.Signals | null;
|
||||
}>((resolve, reject) => {
|
||||
verifier.once("error", reject);
|
||||
verifier.once("close", (code, signal) => resolve({ code, signal }));
|
||||
});
|
||||
expect(result).toEqual({ code: 143, signal: null });
|
||||
for (const pid of pids ?? [])
|
||||
expect(() => process.kill(pid, 0)).toThrow();
|
||||
expect(
|
||||
(await readdir(root)).filter((name) =>
|
||||
name.startsWith("rea-pascal-calls-"),
|
||||
),
|
||||
).toEqual([]);
|
||||
} finally {
|
||||
verifier.kill("SIGKILL");
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -61,7 +61,16 @@ const tracer: NativeCallTracer & { calls: number } = {
|
||||
breakpoints: request.input.breakpoints.map((_, index) => ({
|
||||
index,
|
||||
location_count: 1,
|
||||
locations: [],
|
||||
locations: [
|
||||
{
|
||||
load_address: "0x1000",
|
||||
file_address: "0x1000",
|
||||
module: "Tool",
|
||||
module_path: request.executable,
|
||||
symbol: "open",
|
||||
source_location: { file_path: null, line: 23, column: null },
|
||||
},
|
||||
],
|
||||
})),
|
||||
events: [],
|
||||
resource_limit_reached: false,
|
||||
@@ -124,6 +133,11 @@ it("routes observe_native_calls through MCP with schema-checked input and output
|
||||
.parse(called.structuredContent).normalized_result,
|
||||
);
|
||||
expect(result.process).toMatchObject({ pid: 99, outcome: "exited" });
|
||||
expect(result.breakpoints[0]?.locations[0]?.source_location).toEqual({
|
||||
file_path: null,
|
||||
line: 23,
|
||||
column: null,
|
||||
});
|
||||
expect(result.breakpoints[0]?.request).toEqual({
|
||||
kind: "function",
|
||||
name: "open",
|
||||
|
||||
@@ -215,6 +215,97 @@ it("advertises exact valid schemas and records inline WASM interface evidence wi
|
||||
expect(close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("attributes rejected WASM paths to their selected fields before provider work", async () => {
|
||||
const value = wasmArtifactFixture();
|
||||
value.glue = [
|
||||
{
|
||||
artifact: { path: "/glue.js", sha256: "b".repeat(64), bytes: 0 },
|
||||
parse_status: "complete",
|
||||
references: [],
|
||||
},
|
||||
];
|
||||
value.candidates.push({
|
||||
path: "/other.wasm",
|
||||
sha256: "c".repeat(64),
|
||||
bytes: 8,
|
||||
});
|
||||
const validInput = {
|
||||
path: value.artifact.path,
|
||||
glue_paths: ["/glue.js"],
|
||||
candidate_paths: ["/other.wasm"],
|
||||
};
|
||||
const inspect = vi.fn(() =>
|
||||
Promise.resolve(ok(wasmArtifactExecution(value))),
|
||||
);
|
||||
const harness = await createApplicationMcpHarness({
|
||||
serverOptions: {
|
||||
wasmArtifact: new WasmArtifactService({
|
||||
close: () => Promise.resolve(),
|
||||
inspect,
|
||||
}),
|
||||
},
|
||||
});
|
||||
onTestFinished(harness.close);
|
||||
for (const [input, paths] of [
|
||||
[{ path: "relative.wasm" }, [["path"]]],
|
||||
[
|
||||
{ path: value.artifact.path, glue_paths: ["relative.js"] },
|
||||
[["glue_paths", 0]],
|
||||
],
|
||||
[
|
||||
{ path: value.artifact.path, candidate_paths: ["relative.wasm"] },
|
||||
[["candidate_paths", 0]],
|
||||
],
|
||||
[
|
||||
{
|
||||
path: value.artifact.path,
|
||||
glue_paths: ["/glue.js", "relative.js"],
|
||||
candidate_paths: ["/other.wasm", "relative.wasm"],
|
||||
},
|
||||
[
|
||||
["glue_paths", 1],
|
||||
["candidate_paths", 1],
|
||||
],
|
||||
],
|
||||
[
|
||||
{
|
||||
path: "relative.wasm",
|
||||
glue_paths: ["relative.js"],
|
||||
candidate_paths: ["relative-candidate.wasm"],
|
||||
},
|
||||
[["path"], ["glue_paths", 0], ["candidate_paths", 0]],
|
||||
],
|
||||
] as const) {
|
||||
const response = await harness.client.callTool({
|
||||
name: "inspect_wasm_artifact",
|
||||
arguments: input,
|
||||
});
|
||||
const failure = parseMcpToolError(response).error;
|
||||
expect(failure.code).toBe("invalid_request");
|
||||
const { issues } = z
|
||||
.object({ issues: z.array(z.unknown()) })
|
||||
.parse(failure.details);
|
||||
expect(issues).toEqual(
|
||||
paths.map((path) => ({
|
||||
path,
|
||||
reason: "invalid_format",
|
||||
message: "Expected an absolute filesystem path on this host.",
|
||||
})),
|
||||
);
|
||||
}
|
||||
expect(inspect).not.toHaveBeenCalled();
|
||||
const valid = await harness.client.callTool({
|
||||
name: "inspect_wasm_artifact",
|
||||
arguments: validInput,
|
||||
});
|
||||
expect(valid.isError).not.toBe(true);
|
||||
expect(inspect).toHaveBeenCalledExactlyOnceWith(
|
||||
validInput,
|
||||
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
||||
);
|
||||
await harness.client.ping();
|
||||
});
|
||||
|
||||
it("rejects a WASM provider response with a non-file subject before recording Evidence", async () => {
|
||||
const value = wasmArtifactFixture();
|
||||
const execution = wasmArtifactExecution(value);
|
||||
|
||||
@@ -50,6 +50,25 @@ const observation = (
|
||||
};
|
||||
|
||||
describe("LLDB retained observations", () => {
|
||||
it("preserves partial source coordinates while accepting historical omission", () => {
|
||||
const attributed = {
|
||||
...event,
|
||||
source_location: {
|
||||
file_path: "/missing/fixture.pas",
|
||||
line: 23,
|
||||
column: null,
|
||||
},
|
||||
};
|
||||
const journal =
|
||||
[event, attributed]
|
||||
.map((entry) => JSON.stringify({ kind: "event", event: entry }))
|
||||
.join("\n") + "\n";
|
||||
expect(observation("cancelled", journal).events).toEqual([
|
||||
event,
|
||||
attributed,
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["cancellation", "cancelled"],
|
||||
["bridge failure", "tracer-failure"],
|
||||
|
||||
@@ -30,6 +30,11 @@ const LOCATION = {
|
||||
module: "Fixture",
|
||||
module_path: "/tmp/Fixture",
|
||||
symbol: "-[Greeter greet:times:]",
|
||||
source_location: {
|
||||
file_path: "/not-acquired/é space/fixture.pas",
|
||||
line: 23,
|
||||
column: null,
|
||||
},
|
||||
};
|
||||
|
||||
const run = (
|
||||
@@ -79,7 +84,7 @@ const run = (
|
||||
{ name: "x0", value: "0x6000" },
|
||||
{ name: "x1", value: "0x7000" },
|
||||
],
|
||||
backtrace: [],
|
||||
backtrace: [{ ...LOCATION, source_location: null }],
|
||||
},
|
||||
],
|
||||
other_stops: [],
|
||||
@@ -202,7 +207,12 @@ describe("observe_native_calls projection: projects completed observations", ()
|
||||
expect(result.events[0]).toMatchObject({
|
||||
receiver_class: "Greeter",
|
||||
selector: "greet:times:",
|
||||
source_location: LOCATION.source_location,
|
||||
backtrace: [{ ...LOCATION, source_location: null }],
|
||||
});
|
||||
expect(result.breakpoints[0]?.locations[0]?.source_location).toEqual(
|
||||
LOCATION.source_location,
|
||||
);
|
||||
expect(result.coverage).toEqual({
|
||||
status: "complete",
|
||||
event_limit_reached: false,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
program ReaPascalCalls;
|
||||
|
||||
{$mode objfpc}
|
||||
{$inline off}
|
||||
|
||||
procedure ReaPascalCall(Value: LongInt); cdecl; public name 'rea_pascal_call';
|
||||
begin
|
||||
WriteLn('pascal ', Value);
|
||||
end;
|
||||
|
||||
var
|
||||
Index: LongInt;
|
||||
begin
|
||||
for Index := 0 to 2 do { REA_VERIFY_CALLER_LOOP }
|
||||
ReaPascalCall(Index); { REA_VERIFY_CALLER_CALL }
|
||||
end.
|
||||
Reference in New Issue
Block a user