Commit graph

1042 commits

Author SHA1 Message Date
administrator 7651b2a5ac Обновить README.adoc 2026-07-21 10:40:22 +03:00
artradeskz 1c0646f768
Update README.adoc 2026-07-04 21:54:44 +03:00
mzhoot f2c69b9d00 обновить ридми 2026-07-04 21:53:04 +03:00
mzhoot 8aec317ec9 Добавить todo 2026-07-04 21:50:02 +03:00
mzhoot 94ae8c354c Рабочие синонимы 2026-07-04 21:44:54 +03:00
mzhoot 8b50bc0c9f Merge version-0.12.0 into master с разрешением конфликтов 2026-07-04 21:34:25 +03:00
mzhoot ec10da32b9 Мои изменения для версии 0.12.0 2026-07-04 21:31:55 +03:00
Graeme Geldenhuys 8d1130852f fix(linker): resolve absolute 32-bit relocations in static mode
A static FreeBSD link with --debug-opdf failed with 'absolute 32-bit
relocation unsupported in static mode': the OPDF emitter writes a
32-bit section-offset reference (.long label, R_X86_64_32) into the
.opdf section, and the internal linker's static mode rejected all
abs32 forms.  The guard was over-conservative - in a non-PIE ET_EXEC
linked at base 0x400000 every mapped address has a final link-time
value that fits in 32 bits, so the relocation is directly resolvable
(this is what GNU ld does, erroring only on overflow).

Static mode now patches S+A with GNU-ld-style range checks:
R_X86_64_32 must fit zero-extended, R_X86_64_32S sign-extended.
Dynamic/PIE behaviour is unchanged (the patched value remains the
unslid link-time address; the OPDF reader applies the run-time slide).

Linux never hit this because Linux binaries link in PIE/dynamic mode;
the bug only surfaced on the first FreeBSD-hosted pdr smoke test.

Guard: TestReloc_Abs32_ResolvesInStaticMode links a .long target data
reference statically and asserts the slot is patched with the symbol's
absolute address.

Verified: all four fixpoints green (FIXPOINT_OK, NATIVE_FIXPOINT_OK,
NATIVE_INTERNAL_OK, WARMCACHE_FIXPOINT_OK); full suite OK (4035) under
both the QBE-fixpoint-built and native-built test runners; cross-
compiled freebsd-x86_64 --debug-opdf hello links and carries a
correctly patched .opdf section.
2026-07-02 17:44:49 +01:00
Graeme Geldenhuys 5e9ec4eeec fix(config): expand ~ in blaise.cfg unit-path / rtl-src values
A blaise.cfg value like `rtl-src=~/devel/blaise/compiler/src/main/pascal` was
treated as RELATIVE (only a leading '/' counted as absolute) and prefixed with
the config directory, and the `~` was never expanded — producing a bogus path
like `/home/user/cwd/~/devel/...` and an "RTL source directory not found" error.

Resolve config paths through a shared helper: a leading '~' or '~/' expands to
$HOME (and is treated as absolute); a leading '/' is used as is; anything else
is relative to the config file's directory.  Applies to both unit-path= and
rtl-src=.  If $HOME is unset, '~' is left untouched rather than mis-resolved.

Tests: TConfigTests.TestParseTildePath_ExpandsToHome and
TestParseRtlSrcTilde_ExpandsToHome.  Verified end-to-end: a blaise.cfg using
`~/...` for unit-path + rtl-src compiles with no path flags from any CWD.
2026-07-02 12:46:41 +01:00
Graeme Geldenhuys 0824ed8a6b feat(freebsd): self-host on FreeBSD — runtime fixes + target-driven defines
Blaise now runs natively on FreeBSD and compiles+links working FreeBSD binaries
(fork/exec RTL sub-compiles, threaded incremental workers, internal assembler +
linker, freestanding runtime), and cross-compiles both directions (Linux<->
FreeBSD). Each of these was a real FreeBSD-only runtime bug exposed by running on
a real FreeBSD 14.3 kernel — invisible to host-side static checks:

  * fork(2) child return: FreeBSD returns the child in %rax=parent-pid with
    %edx=1 (child indicator); the raw leaf must zero %eax when %edx!=0, else the
    forked child runs the parent branch and never execve's its target (exit 127).
  * thread join teardown: the worker set its join word + umtx-WAKE while still on
    its own stack, so pthread_join munmap'd the shared stack/CB region out from
    under the still-live thread -> SIGSEGV. Fixed by passing the join word as
    thr_exit(state): the kernel does the suword+umtx-wake AFTER the thread leaves
    its stack, so the joiner only wakes once the stack is safe to free.
  * argc stack pad word: FreeBSD 16-aligns the entry %rsp and inserts a zero pad
    word below argc when the argv+envc pointer-count parity requires it, so argc
    is at [rsp] or [rsp+8]. _BlaiseStartC now detects and skips the pad word;
    previously an execve'd child with a different environment read the pad (0) as
    argc and reported "--source is required".

Target-driven OS conditional compilation (self-hosting requires HostTarget to be
correct on the produced binary):

  * The OS defines (LINUX/FREEBSD/WINDOWS/UNIX) now follow the resolved --target,
    not the compiler's build host. SeedPredefines seeds the host OS as a default
    (keeps direct-lexer callers/tests working) via compile-time IFDEF; the driver
    injects the target's OS symbol and AddDefinesTo / the unit loader replace the
    host-seeded OS symbols with it on every lexer (program + dep units).
  * HostTarget() reads {$IFDEF FREEBSD} etc., so a blaise built with
    --target freebsd bakes HostTarget=osFreeBSD — a FreeBSD-hosted blaise then
    defaults (no --target) to producing FreeBSD binaries. Verified both
    directions on the VM.

Usability:

  * blaise.cfg now supports rtl-src=<dir> (relative resolved against the config
    dir); a CLI --rtl-src overrides it. So a relocated binary can drop a
    blaise.cfg with unit-path= + rtl-src= and need no path flags. Tests:
    TConfigTests.TestParseRtlSrc_*.
  * --help documents the previously-undocumented --rtl-src flag and the
    blaise.cfg keys, and the --target line now shows the ACTUAL host default
    (TargetName(HostTarget())) instead of a hardcoded linux-x86_64 — correct on
    every host.

Verified: all four fixpoints green; full suite 4032 tests under QBE-built and
native-built runner; Linux self-host and Linux<->FreeBSD cross-compile confirmed;
FreeBSD self-host (blaise compiling+running a program) confirmed on FreeBSD 14.3.
2026-07-02 12:20:18 +01:00
Graeme Geldenhuys db85aba08d fix(freebsd): translate MAP_ANONYMOUS in the mmap leaf; read phdrs from ELF header
The FreeBSD cross-compile emulation lane (Step 8) booted real FreeBSD 14.x and
every cross-compiled binary — even a bare WriteLn — SIGSEGV'd. truss pinned it:

    mmap(0, 65536, RW, MAP_PRIVATE|0x20, -1, 0) ERR#9 'Bad file descriptor'

The shared allocator (runtime.mem) hardcodes the LINUX MAP_ANONYMOUS bit (0x20);
FreeBSD's MAP_ANON is 0x1000.  Without a recognised anon flag FreeBSD treated the
first heap arena as a file mapping on fd -1, returned EBADF, and the allocator
dereferenced the -errno result -> SIGSEGV.  This is exactly the runtime-only,
struct/syscall-flag class of bug host-side static ELF checks cannot see, and the
emulation lane caught it on its first run.

Fix: the FreeBSD mmap leaf translates the flag — if the Linux MAP_ANONYMOUS bit
(0x20) is set, clear it and set FreeBSD MAP_ANON (0x1000).  This keeps runtime.mem
OS-agnostic (one canonical, Linux-shaped flag set) and confines the divergence to
the per-OS syscall leaf, matching the Strategy-B link-time-swap design.  A flag
already carrying 0x1000 (e.g. runtime.start.static.freebsd's TLS mmap) passes
through untouched.

Also harden SetupTLS: read the program-header table directly from this binary's
own ELF header at the fixed non-PIE load base ($400000) instead of walking the
kernel auxv for AT_PHDR.  A non-PIE ET_EXEC always maps its header there, so this
is exact and self-contained (FreeBSD does supply AT_PHDR, but relying on it
coupled startup to the auxv layout for no benefit).

Verified on real FreeBSD 14.3: hello prints 'Hello' (exit 0); fileio round-trips
a file — write/read/stat/unlink — printing file-io-ok/exists-ok/done (exit 0),
validating the struct-stat offsets, the file syscall leaf AND the allocator on a
real kernel.  Linux unaffected (FreeBSD units are standalone): all four fixpoints
green, full suite 4029 tests under QBE-built and native-built runner.  The
freebsd-crosscheck emulation job (fileio fixture) is the standing regression
guard for the mmap path.
2026-07-01 23:01:46 +01:00
Graeme Geldenhuys 0ee32e51f5 ci(freebsd): run cross-compiled FreeBSD binaries under emulation (Step 8)
Host-side static ELF checks (EI_OSABI/ET_EXEC/no-PT_INTERP, already asserted by
TInternalLinkerE2ETests.TestCompile_FreeBSDTarget_EmitsStaticFreeBSDExe) cannot
catch a wrong FreeBSD struct-stat offset or syscall number — only running the
binary on a real FreeBSD kernel does.

Add a freebsd-crosscheck CI job (needs: bootstrap) that:
  * downloads the -pre binary the bootstrap job built,
  * cross-compiles the fixtures in compiler/src/it/freebsd (hello, fileio) with
    --target freebsd-x86_64 on the Linux runner via
    scripts/freebsd-crosscheck-build.sh (which also sanity-checks each emitted
    ELF is a FreeBSD ET_EXEC), and
  * boots a FreeBSD VM (vmactions/freebsd-vm, release 14.2 to match the
    struct-stat offsets in rtl.platform.layout.freebsd) and runs the binaries
    there, asserting hello prints 'Hello' and fileio round-trips a file
    (write/read/stat/delete) with 'file-io-ok'/'exists-ok'/'done'.

The FreeBSD output is a static, freestanding raw-syscall ET_EXEC, so the VM
needs no extra packages to run it.  fileio is the meaningful check: it exercises
open/write/read/stat/unlink through the FreeBSD leaf and layout, which the
static checks are blind to.
2026-07-01 17:28:07 +01:00
Graeme Geldenhuys d48cbc64a0 docs(freebsd): mark Steps 6 and 7 (target-driven link + CLI) done
Record that --target drives the static/freestanding link and per-target
TPlatformLayout selection (dependency injection via PlatformLayoutInitSym +
weak _BlaisePlatformInit fallback), and that the --target CLI surface (Step 7)
was already satisfied by ParseTargetName in Blaise.pas.
2026-07-01 13:59:54 +01:00
Graeme Geldenhuys 0014c157a0 feat(freebsd): drive static/freestanding link + per-target layout by --target (Step 6)
Complete the end-to-end FreeBSD cross-compile: `blaise --target freebsd-x86_64`
now emits a static, freestanding FreeBSD ET_EXEC on a Linux host with no
external tools.

Target selection through the linker:
  * TargetIsFreestanding(target) — FreeBSD (Strategy B: direct syscalls, no
    libc) is always a static ET_EXEC.  LinkViaInternalLinker sets dynamic mode
    to `not (AOpts.Static or freestanding)`, and EnsureRTLObjects selects the
    freestanding kernel-leaf unit list the same way — so a FreeBSD target links
    the syscall/libc-shim/thread leaves and no libc, regardless of --static.

Per-target platform layout (dependency injection off --target):
  * The concrete TPlatformLayout adapter (rtl.platform.layout.<os>) is selected
    and linked by the driver (BuildRTLUnitList), and the compiler injects a
    direct call to that unit's initialiser — PlatformLayoutInitSym(target) =
    rtl.platform.layout.<os>_init — into main, first, for the compile-time
    --target.  rtl.platform.posix no longer `uses` a concrete layout, so the
    shared POSIX code is OS-agnostic and a FreeBSD build carries no Linux layout.
  * Bootstrap fallback: each layout unit also defines a WEAK _BlaisePlatformInit
    trampoline that _SetArgs calls, so a binary built by an older codegen (which
    does not emit main's by-name call) still initialises GPlatformLayout.  Weak
    binding lets both layout units coexist in one link (e.g. the TestRunner,
    which imports the FreeBSD layout via cp.test.platformlayout.freebsd) without
    a duplicate-symbol error — the linker keeps the first-seen (host) copy.

FreeBSD libc-shaped leaf:
  * New runtime.libc.freebsd (sibling of runtime.libc.linux): getcwd/getenv/
    time/waitpid/execvp/sysconf on the FreeBSD syscall leaf.  time() reads
    CLOCK_REALTIME (no SYS_time on FreeBSD); sysconf(CPU count) uses
    sysctl(hw.ncpu) — new SYS___sysctl=202 leaf, MIB {CTL_HW=6, HW_NCPU=3}.

Tests:
  * TTargetToolkitTests.TestTargetIsFreestanding_{FreeBSD_True,Linux_False}.
  * TInternalLinkerE2ETests.TestCompile_FreeBSDTarget_EmitsStaticFreeBSDExe —
    a full compiler-CLI cross-compile asserting EI_OSABI=9, ET_EXEC, no
    PT_INTERP (the binary cannot run on the Linux host, so shape only).

Verified: all four fixpoints green; full suite passes under QBE-built and
native-built test runner (4029 tests); the previously latent TestRunner
GPlatformLayout crash is resolved.
2026-07-01 13:53:54 +01:00
Graeme Geldenhuys 9bbcfd8b77 fix(opdf): escape string-constant values in the recConstant .ascii line
The OPDF emitter wrote a string constant's value verbatim into the debug
section's `.ascii "<val>"  # Value` line. Any double-quote, newline or
backslash in the value broke the line: an unescaped `"` closed the literal
early, leaving `..."  # Value` as a garbage mnemonic, and a newline split the
directive across two lines. Under --debug-opdf the OPDF section is appended to
the same assembly text the assembler consumes, so this aborted the compile —
the internal assembler with "unhandled mnemonic: [\"  # Value]" and the
external assembler with "junk at end of line". It was content-specific, so it
only surfaced on larger programs (e.g. building a debuggable TestRunner) whose
constants happened to contain such characters.

Add EscapeAsciiStr (C-style escapes: \" \\ \n \r \t + octal for other
non-printables) and apply it to the recConstant value. The OPDF ValueLen field
stays the RAW byte count, which `.ascii` with these escapes reproduces exactly,
so the emitted byte length is unchanged.

Tests: TOPDFTests.TestOPDF_StringConst_SpecialChars_Escaped (IR: the .ascii
line is escaped) and TInternalAsmE2ETests.TestDebugOPDF_StringConstSpecialChars_
AssemblesAndRuns (compile+run under --debug-opdf --assembler internal). Both the
internal- and external-assembler failure modes are resolved by the one escape.

Verified: all four fixpoints green; full suite passes under QBE-built and
native-built test runner (4026 tests).
2026-07-01 12:04:34 +01:00
Graeme Geldenhuys 135588502d docs(freebsd): mark Step 5 (per-target RTL) done
Record that BuildRTLUnitList drives the per-target RTL unit selection off
AOpts.Static + AOpts.Target.OS, with the FreeBSD adapter set selected and no
Linux RTL unit present; Linux link path unchanged.
2026-07-01 10:30:51 +01:00
Graeme Geldenhuys d0032269d6 feat(freebsd): select the RTL unit list per target (Step 5)
EnsureRTLObjects hard-coded the Linux RTL adapter set: rtl.platform.layout.linux
in the base list, and for a --static link the Linux kernel leaf
(runtime.start.static.linux, runtime.syscall.linux, runtime.libc.linux,
runtime.libc2.linux, runtime.thread.static.linux). A --target freebsd-x86_64
program therefore always linked Linux/libc RTL code regardless of the target.

Extract the selection into a pure, unit-tested helper BuildRTLUnitList(AStatic,
AOS) that follows the target: the platform-layout adapter becomes
rtl.platform.layout.<os>, and the --static kernel leaf swaps to the freestanding
per-OS units (runtime.start.static.<os>, runtime.syscall.<os>,
runtime.libc.<os>, runtime.libc2.<os>, runtime.thread.static.<os>) plus the
OS-invariant runtime.cstub. EnsureRTLObjects now drives it off AOpts.Static and
AOpts.Target.OS. Linux is the default suffix, so the existing Linux link path is
byte-for-byte unchanged; FreeBSD selects its full adapter set with no Linux RTL
unit present.

Tests: TBackendDriverContractTests.TestRTLUnits_* cover the Linux-dynamic,
Linux-static, FreeBSD-static-swaps and FreeBSD-static-no-Linux-leaf cases.
Verified: --static Linux hello-world still runs (regression check); all four
fixpoints + QBE & native TestRunner green (4024 tests).
2026-07-01 10:28:13 +01:00
Graeme Geldenhuys a1ba257da7 fix(semantic): scope same-named nested routines to their enclosing body
A nested function or procedure is lexically scoped to the routine that
contains it, so two sibling routines may each declare a nested helper of
the same name without conflict. Two independent gaps broke this:

  * Nested-in-METHOD decls were still entered in the global overload index
    (FProcIndex / FProcGroups). The registration guard in
    AnalyseStandaloneDecls only excluded nested-in-standalone-proc decls
    (FCurrentEnclosingDecl set); AnalyseMethodDecl never sets that field, so
    a nested helper in a method body leaked into the global index and two
    same-named siblings collided as "Ambiguous overload".

  * Nested-in-PROC functions called as an expression could not be resolved.
    AnalyseFuncCallExpr's tail consulted only FProcIndex.IndexOf and errored
    with "Cannot find declaration for function 'X'" without ever checking the
    scoped symbol it had already looked up. (The statement-position path,
    AnalyseProcCall, already handled scoped nested procs, which is why only
    the function-as-expression form failed.)

Fixes: also exclude a decl from global registration when FCurrentMethodOwner
is set (inside a method body); and in AnalyseFuncCallExpr, when the global
index misses but the scoped symbol carries a backing TMethodDecl, resolve the
call directly against that single decl.

Adds IR and e2e regression tests for both the proc and method enclosing forms,
and removes the MovRaxImm4b/4c rename workaround in cp.test.linker.pas — three
sibling test methods now legitimately share a nested MovRaxImm helper.

Verified: QBE, native, internal-asm and warm-cache fixpoints all green; full
suite passes under both the QBE-built and native-built test runner (4020 tests).
2026-07-01 10:14:30 +01:00
Graeme Geldenhuys 464cc113b6 docs(freebsd): mark Step 4 (syscall leaf) done
Record 4a/4b/4c as complete in the FreeBSD backend design doc: the
file/process leaves, the Tier-2 libc leaves + getrandom/mremap stub, and
the threads + %fs TLS setup are all landed and verified (four fixpoints,
full suite, cp.test.linker FreeBSD fixtures).  End-to-end execution under
emulation remains Step 8.
2026-07-01 09:52:27 +01:00
Graeme Geldenhuys 0a9b79dd30 feat(rtl): FreeBSD libc-free threads + TLS (Step 4c)
The last Step-4 slice: real POSIX threads for a static FreeBSD build,
plus the TLS setup Step 3 deferred (threads need per-thread %fs TLS, and
the main thread needs it too for threadvar access).

runtime.syscall.freebsd gains four FreeBSD-specific leaves (all numbers
verified against release/14.0.0 sys/sys/syscall.h): sysarch=165 (set the
%fs base for TLS via AMD64_SET_FSBASE=129, passing a POINTER to the base
value), _umtx_op=454 (the futex analogue), thr_new=455 (the clone
analogue), thr_exit=431 (terminate ONE thread — NOT exit(2)/SYS_exit,
which kills the process).

runtime.start.static.freebsd gains the full TLS machinery from its Linux
sibling: the auxv PT_TLS walk (AT_PHDR/PHENT/PHNUM and PT_TLS are the same
values on FreeBSD as Linux), the variant-II block build, BuildThreadTLS/
FreeThreadTLS, and _BlaiseStartC now calls SetupTLS(auxv) before main.
The one difference from Linux: the %fs base is set via
sysarch(AMD64_SET_FSBASE, @base) rather than arch_prctl(ARCH_SET_FS, base).

runtime.thread.static.freebsd (new) is the FreeBSD sibling of
runtime.thread.static.linux — it DEFINES the bare pthread_* names the RTL
imports.  The 3-state Drepper mutex (CAS/Xchg) is target-invariant and
copied verbatim; only the wait/wake primitive is swapped
futex -> _umtx_op(UMTX_OP_WAIT_UINT_PRIVATE=15 / WAKE_PRIVATE=16).  Thread
create fills a 104-byte struct thr_param at the amd64 offsets
(start_func@0 arg@8 stack_base@16 stack_size@24 tls_base@32 tls_size@40
child_tid@48 parent_tid@56 flags@64 rtp@72), memset-zeroed first so flags/
rtp/spare are 0, and calls thr_new.  child_tid/parent_tid point at an
Int64 (FreeBSD long*, not Linux's 4-byte).  FreeBSD has no
CLONE_CHILD_CLEARTID, so join uses a dedicated JoinWord: the child
trampoline runs StartRoutine, sets JoinWord:=1, _umtx_op-wakes it, then
thr_exit(nil); pthread_join _umtx_op-waits while JoinWord=0.

Both new/edited units are standalone (nothing in the Linux build graph
uses them), so the Linux self-host is untouched.  Test
TestLink_FreeBSDThreadLeaf_SyscallNumbers asserts thr_new(455)/_umtx_op
(454)/thr_exit(431) bytes are present and Linux's clone(56)/futex(202)
are absent.  Step 4 (the FreeBSD syscall leaf) is now complete.
2026-07-01 09:49:30 +01:00
Graeme Geldenhuys bded5294d5 feat(rtl): FreeBSD Tier-2 libc leaves + mremap/getrandom (Step 4b)
The Step-4b RTL leaves for a static FreeBSD build.  memcpy/memset/memcmp/
strlen are already target-invariant (runtime.cstub, pure Pascal — shared),
and the x86-64 jmp_buf is ABI-invariant (runtime.setjmp — shared), so this
step is small:

* runtime.syscall.freebsd gains getrandom (FreeBSD SYS 563, not Linux's
  318) and an mremap stub.  FreeBSD has no in-place-remap syscall, so
  mremap is a plain function returning MAP_FAILED (-1); runtime.mem's
  large-realloc path already treats an mremap failure as "grow the slow
  way" (alloc a fresh region, memcpy, free the old), so exporting a failing
  mremap gives the allocator its no-mremap fallback with zero allocator
  changes.

* runtime.libc2.freebsd — the FreeBSD sibling of runtime.libc2.linux: the
  atexit registry + `exit`, __cxa_atexit, abort, freestanding UTC
  civil-calendar gmtime_r/localtime_r/timegm, mkstemp, and system.  The
  calendar math, atexit registry, and struct tm layout are target-invariant
  (tm is identical on Linux/FreeBSD amd64), so the only differences from the
  Linux unit are the syscall leaf it builds on (runtime.syscall.freebsd) and
  the FreeBSD O_* flag values (O_CREAT=$0200, O_EXCL=$0800 — not Linux's
  $40/$80).

Both units are standalone (nothing in the Linux build graph uses them), so
the Linux self-host is untouched — confirmed by the clean fixpoints/suite.
Test TestLink_FreeBSDSyscallLeaf_GetrandomAndMremapStub asserts getrandom
uses 563 (and NOT Linux's 318) and the mremap stub returns -1.  Threads
(thr_new/_umtx_op) are Step 4c.
2026-07-01 02:27:11 +01:00
Graeme Geldenhuys 2662535579 feat(rtl): FreeBSD syscall leaf — file/process leaves (Step 4a)
Grow runtime.syscall.freebsd.pas from the Step-3 minimum (_exit/write) to
the file/process syscall leaves, mirroring runtime.syscall.linux but with
FreeBSD syscall numbers and FreeBSD's carry-flag error convention.

Added leaves: open close read lseek fstat stat mkdir rmdir unlink rename
chdir chmod getpid dup2 pipe mmap munmap nanosleep clock_gettime fork
execve wait4 kill getcwd.  Each emits the raw `syscall` (number in %rax,
args rdi/rsi/rdx/r10/r8/r9, %rcx->%r10 for >=4 args) and translates
FreeBSD's error convention (CF set, positive errno in %rax) into the
-errno the Pascal call sites in rtl.platform.posix expect:

    movq $N, %rax ; syscall ; jae .Lok ; negq %rax ; .Lok: ret

Syscall numbers are the FreeBSD 14 ino64 ABI (mmap=477 lseek=478
fstat=551, NOT the freebsd6/11 compat 197/199/189).  FreeBSD's ino64 ABI
has no plain `stat` syscall, so stat(path,buf) is a wrapper over
fstatat(AT_FDCWD,path,buf,0)=552; pipe(fds) wraps pipe2(fds,0)=542.  All
numbers verified against release/14.0.0 sys/sys/syscall.h.

The unit is standalone (only runtime.start.static.freebsd references it,
itself outside the Linux build graph), so the Linux self-host is
untouched.  Test TestLink_FreeBSDSyscallLeaf_SymbolsAndNumbers links a
fixture and asserts the bare POSIX symbols are defined and the FreeBSD
syscall-number bytes + negq (CF xlate) + mov %rcx,%r10 are present.
Scope: file/process only; memcpy/date/allocator/threads are Step 4b/4c.
2026-07-01 02:15:01 +01:00
Graeme Geldenhuys 77197739de Merge branch 'feature/stdlib-hashing'
feat(stdlib): add SHA-256, MD5, HMAC-SHA256, ConstantTimeEqual to Security.Crypto
2026-07-01 01:09:16 +01:00
Graeme Geldenhuys bfd43f9147 feat(stdlib): add SHA-256, MD5, HMAC-SHA256, ConstantTimeEqual to Security.Crypto
Expand the crypto primitives unit with four new capabilities:

- SHA-256 / Sha256Hex (FIPS 180-4, Rotr32 helper, 64-round K table)
- MD5 / Md5Hex (RFC 1321, little-endian padding and output, T/S tables)
- HMAC-SHA256 / HmacSha256Hex (RFC 2104, key>block-size hashing)
- ConstantTimeEqual (timing-safe XOR-accumulate comparison)

Also refactors Sha1Hex to use a shared DigestToHex private helper,
adds Rotr32 alongside Rotl32, removes unused `uses Classes` import,
and adds MD5 deprecation notice to the unit header.

Tests: 12 crypto tests covering NIST CAVP (SHA-256), RFC 1321 all 7
vectors (MD5), RFC 4231 TC1-3/TC5-6 (HMAC-SHA256 incl. long-key),
and 8 ConstantTimeEqual assertions. E2E smoke test for Sha256Hex
on both backends (cp.test.e2e.crypto.pas).
2026-07-01 01:06:30 +01:00
Graeme Geldenhuys ac6e0b0d88 docs: document array[NamedSubrange] index type
grammar.ebnf: the ARRAY [IDENT] OF T form is an ordinal-indexed array;
clarify it admits both an enumeration type and a named integer subrange
as the index, with a note on how each folds (enum -> array[0..N-1],
subrange -> array[lo..hi]).

language-rationale.adoc: add the array[TSubrange] of T row to the
supported array-index table, noting the subrange supplies the index
range while its values remain ordinary unchecked integers.
2026-06-30 21:23:35 +01:00
Graeme Geldenhuys 1035dcc1c9 feat(types): named integer subrange as an array index type
`type TIdx = lo..hi; TBuf = array[TIdx] of T;` now resolves to
`array[lo..hi] of T`, parallel to the existing `array[TEnum] of T`
ordinal-index folding.  Previously a named subrange used as an array
index was rejected with "'TIdx' is not an enumeration type", because a
subrange was stored as a transparent alias to its narrowest standard
integer type with its bounds discarded, and the array-index resolver
only accepted enum index types.

A named subrange now keeps its lo..hi bounds as compile-time metadata on
a DISTINCT TTypeDesc (IsSubrange/SubrangeLow/SubrangeHigh).  Its storage
and ABI are unchanged — it is still its underlying narrowest int, values
behave as ordinary unchecked integers (Blaise does no range checking) —
only the array-index resolver consults the retained bounds, folding
array[TIdx] to array[lo..hi].  Low/High on such an array then work via
the folded static-array bounds.

The bounds round-trip through the .bif META 'alias' record (three
trailing fields after TypeName), so a subrange exported from a unit and
used as `array[OtherUnit.TIdx]` resolves correctly across separate
compilation.  IFACE_VERSION 7 -> 8 (the alias record grew; v7 readers
must reject and recompile).  CloneTypeDef copies the new fields so the
export/clone path does not drop IsSubrange.

Tests: two e2e cases (0-based and 2..4 non-zero base, asserting element
reads + Length/Low/High on every backend), two IR/semantic cases
(the index folds to the right static-array bounds), and a .bif
round-trip case (IsSubrange + bounds survive write->read); the
magic/version assertion updated to v8.
2026-06-30 20:19:01 +01:00
Andrew Haines 61bb86f896 feat(units): directed lookup for unit-qualified field-access references
A unit-qualified type-member reference written as a field access —
`Unit.TEnum.Member` (a type-qualified enum member) or `Unit.TFoo.StaticVar` —
resolved its base type through the flat uses-chain lookup, i.e. the cross-unit
last-wins winner, ignoring the unit qualifier.  With two used units exporting a
same-named enum/type this bound to the wrong one: `ea.TPalette.paThree` failed
"Enum 'TPalette' has no member 'paThree'" because it resolved TPalette to eb.

TFieldAccessExpr now carries the parser-collapsed unit qualifier (like
TIdentExpr and TMethodCallExpr already do), and AnalyseFieldAccess resolves the
base via the directed ResolveQualified when it is set — so the reference binds
to the named unit's own type, independent of `uses` order.

Also completes the .bif round-trip for the qualifier fields: TFieldAccessExpr
.QualifierUnit and the previously-unserialised TMethodCallExpr.QualifierUnit are
now encoded/decoded, and bif-coverage.status records all the qualifier/owner
fields added across the cross-unit work (serialise vs safe).

Test: e2e cross-unit qualified enum member, order-independent.
2026-06-30 19:39:36 +01:00
Andrew Haines 9ec970393d feat(units): cross-unit type last-wins and unit-qualified type disambiguation
Two used units exporting a class of the same name previously errored with
"Duplicate type name".  Give types the same cross-unit semantics as consts
and vars: the units coexist, a bare reference binds to the unit later in
`uses` (last-in-uses wins), and a qualified `Unit.TName` reference binds to
that specific unit's own type — with distinct typeinfo, vtable, field
cleanup and method dispatch.

Semantic:
- DefineTypeLastWins detaches the prior unit's type on a cross-unit collision
  (kept in the per-unit cache for qualified access) and installs the later
  unit as the flat winner; same-unit redeclaration stays a hard error.  The
  duplicate-method and impl-body-link guards skip foreign-owned entries (with
  a signature fallback so a generic instance whose template lives in another
  unit still links).  FindMethodDecl and a one-shot owner hint on
  ResolveMethodOverload bind a call to the method on the type that actually
  resolved, not the first-registered.  FindTypeOrInstantiate resolves a
  qualified type name through the directed per-unit lookup BEFORE the flat
  FindType (which would strip the qualifier to the uses-chain winner).

- TTypeDesc gains OwningUnit (stamped on Define); the AST type decl gains
  ResolvedDesc; a method-call expr gains QualifierUnit (set by the parser for
  `Unit.Type.Create`), so a qualified constructor resolves against its unit.

Codegen (QBE and native): a type's storage symbols (typeinfo / vtable /
_FieldCleanup / __cn) are mangled by the unit currently being emitted, and
qualified reference sites mangle by the resolved descriptor's owner — so two
used units' same-named types emit and are referenced under distinct,
non-colliding symbols.  Generic instances stay unprefixed.

Tests: e2e cross-unit type last-wins (+ reversed) and qualified
disambiguation; an IR-level check that the two types emit distinct symbols.
2026-06-30 19:36:13 +01:00
Andrew Haines cd9af9ac7b feat(units): unit-qualified var access and cross-unit var last-wins
Two used units could not export the same module-scope global var name:
the second Define raised "Duplicate identifier", and even where one slot
existed a qualified reference (Unit.V) was lowered by bare name, so it
could not pick a specific unit's storage. Bare references also resolved
through the flat table (analysis order) rather than the uses chain,
disagreeing with the last-in-uses rule that consts already follow.

Give vars the same cross-unit semantics as consts:

- DefineGlobalLastWins detaches the prior unit's var on a cross-unit
  collision (keeping it alive in the per-unit cache so Unit.V still
  reaches it) and installs the later unit as the flat winner; same-unit
  redeclaration and module-name markers stay hard errors. RegisterVars
  mirrors this on the prebuilt-import path, as RegisterConsts does.

- The analyser stamps the resolved owning unit onto each global-var
  reference and assignment target (the uses-chain winner for a bare ref,
  the named unit for a qualified one). Codegen mangles the storage
  symbol with that owner instead of re-looking-up the bare name, so the
  definition (keyed on the unit being compiled) and every reference
  agree. A bare reference now follows uses order like a const; a
  qualified reference always hits its own unit's slot.

Tests: e2e last-wins (+ reversed) and qualified disambiguation across two
units exporting `var V`, plus an IR-level check that qualified loads emit
distinct owner-prefixed symbols.
2026-06-30 19:32:37 +01:00
Andrew Haines 9aa44911b0 fix(codegen): unit-prefix mangling for module-scope global variables
Module-scope global variables are now emitted under a unit-prefixed
symbol (GlobalVarUnitPrefix), so same-named globals in different units
(e.g. GRegistry in two units) do not collide at link.  The definition
(EmitGlobalVarData) and every reference (VarRef) run the identical
owner-resolution + allowlist, so they always agree.

A static class-var (e.g. RegModU.TReg.FCount) is already emitted under
its fully unit+class-qualified symbol RegModU_TReg_FCount.  That name is
passed to GlobalVarUnitPrefix verbatim; Lookup does not find it, so the
owner fell back to the current unit and the prefix was applied a SECOND
time (RegModU_RegModU_TReg_FCount), so the reference no longer matched
the definition and the link failed.  Add an idempotence guard: if the
name already starts with the resolved prefix it is carried through
unchanged rather than re-prefixed.
2026-06-30 19:31:09 +01:00
Andrew Haines c7f264acec feat(semantic): directed lookup for unit-qualified references
A 'Unit.Symbol' reference must resolve against that specific unit's
exports — never the flat global table or the uses chain — so a
same-named symbol in another used unit can neither shadow it nor be
shadowed by it. Previously the unit prefix was discarded at three
independent sites (idents, type names, statement targets), collapsing
every qualified reference to a bare lookup; a shadowed const/type in an
earlier-in-uses unit was then unreachable even when named explicitly.

Introduce a single directed-lookup primitive, ResolveQualified(Unit,
Name): per-unit cache first (the authoritative map of what each unit
exported, where a cross-unit collision loser is retained after being
evicted from the flat table), then a lenient flat-table fallback for
harnesses that populate the global but not the cache. Route the three
sites through it: the parser now preserves the matched unit on
TIdentExpr.QualifierUnit (serialised into the .bif), the type resolver
consults it for dotted type names, and a unit-qualified class ancestor
'class(Unit.TParent)' resolves through the type machinery so inheritance
binds to the named unit's type.

The const last-wins path now stashes the shadowed const in the per-unit
cache (it claimed to but never did), so 'Unit.Const' reaches the
declaring unit's own value regardless of which unit won the bare slot.

Parent/implements names parse via ParseTypeName (which already absorbs
the unit qualifier and nested generics) instead of ParseGenericName.

Tests: qualified-const disambiguation (ua.Foo=100 / ub.Foo=200 read
independently of the bare last-wins winner) and qualified inheritance
(class(ua.TParent) inherits across units).
2026-06-30 19:18:30 +01:00
Andrew Haines 6bc8aff47d feat(semantic): cross-unit const shadowing with last-in-uses wins
When two used units export a constant of the same name, the importer
silently kept whichever was registered first and destroyed the other,
so the later unit in the `uses` clause could never shadow the earlier
one. Pascal's rule is last-in-uses wins.

Add TScope.ExtractLocal / TSymbolTable.ExtractLocal, which detach a
symbol from a scope without freeing it (the owning list keeps the
object alive for any per-unit cache that still references it). On a
cross-unit const collision, both the source-analysis path
(AnalyseConstDecls) and the prebuilt-import path (RegisterConsts) now
extract the prior unit's const and install the later unit's as the
flat-scope winner, instead of dropping the newcomer. Same-block and
unit-name-marker duplicates remain hard errors.

Adds a two-written-units e2e helper (CompileAndRunWithUnits) and
regression tests asserting the `uses` order decides the winner.
2026-06-30 19:15:13 +01:00
Andrew Haines 3db2476983 feat(semantic): type-directed resolution of bare enum members
Enum members are no longer registered as bare global constants. They are
held in a type-keyed reverse index (member name -> list of declaring
enum, ordinal, declaration order), so two enums may share a member name
without colliding in the global scope.

A bare member reference resolves by a fixed cascade:
  1. a real symbol of that name wins (backwards compatible);
  2. otherwise the expected type at the use site selects the enum;
  3. otherwise a single unique candidate is accepted.

When a bare member is ambiguous and no type context is available, this is
now a hard error that names the declaring enums and tells the user to
qualify it as <EnumType>.Member, replacing the previous last-wins
warning. The separate duplicate-member warning channel is removed.

Expected-type context is supplied at every site that knows its target
type without changing the AnalyseExpr signature: variable/field/element
assignment (static, dynamic, open and multi-dimensional arrays, plus
implicit-Self and default-property writes), pointer-target writes, case
values and ranges, set elements and ranges, the `in` left operand, for
loop bounds, standalone and method call arguments (including
implicit-Self, qualified, metaclass and constructor calls), procedural-
type indirect-call arguments, and function results via Result and Exit.

Qualified forms (TEnum.Member, Unit.Member, Unit.TEnum.Member) continue
to resolve unchanged.

Tests cover each context above plus the ambiguity error, in both the
semantic/IR suite and the end-to-end suite.
2026-06-30 19:12:00 +01:00
Andrew Haines 4d0c7c461d feat(semantic): resolve type-qualified enum members (TEnum.Member)
A bare or type-qualified enum member (`Color.cRed`, or `cRed` where the
target type is known) now resolves correctly.

Also adds TSymbolTable.InCodegen, set by both backends around code
emission, which disables the analysis-time impl-private suppression in
Lookup.  That guard exists only to stop an implementation-section symbol
of unit A leaking into a different unit B DURING ANALYSIS; at codegen a
backend legitimately resolves a unit's own impl-section classes to emit
their typeinfo/vtable/_FieldCleanup.  FDefineOwningUnit can drift to a
dependency unit mid-emit, which previously made the suppression wrongly
fire and silently drop an impl-section class's typeinfo — leaving a
dangling `typeinfo_<Unit>_<Class>` reference the linker bound to a garbage
address (an out-of-range-metaclass crash, e.g. via RegisterTest).  This
is the robust, both-backend root-cause fix for that crash, which this
commit's layout change re-exposed; it supersedes the narrower
EmitClassSection DefineOwningUnit-pin added earlier.
2026-06-30 19:11:28 +01:00
Graeme Geldenhuys edf4ffe374 feat(link): pass external-library deps to the linker as -l<name>
The libraries collected from `external 'lib'` declarations (the program's
and every used unit's TUnitInterface.LinkLibs) are unioned into
TBackendOpts.LinkLibs and emitted on the toolchain link line as one
-l<name> each, additive to the always-needed -lm/-lpthread.  Both the QBE
and native (external-linker) paths route through LinkViaToolchain and so
gain the flags.

The in-process internal linker (--linker internal) links only Blaise ELF
objects + the source-built RTL and has no concept of -l<name> system
libraries.  Rather than silently drop a declared dependency and emit a
binary with unresolved symbols, LinkViaInternalLinker now fails loudly
when LinkLibs is non-empty, pointing the user at --linker external.

Tests: TestExternalLib_MissingLib_FailsLink_{QBE,Native} drive the real
compiler binary end to end (the e2e harness links with a hardcoded
`cc ... -lm -lpthread` and never calls LinkViaToolchain, so it cannot
exercise this).  A program declaring `external 'nosuchlib...'` — never
called, but collected into LinkLibs at parse time — must fail the link
with the linker's "cannot find -lnosuchlib...", which only happens if the
driver actually emitted the flag.
2026-06-30 17:51:42 +01:00
Graeme Geldenhuys c1f14b5c78 feat(parser): external 'lib' name directive + link-library propagation
`external 'c' name 'strlen'` now records the bare library name on the
declaration (TMethodDecl.ExternalLib) and hoists it into the owning
unit/program's LinkLibs set, so the link layer can expand it to a
-l<name> dependency.  The library clause is optional and parsed before
the existing `name '...'` clause, so `external name 'x'` and bare
`external 'c'` both still parse.

TUnitInterface.LinkLibs is serialised in the .bif META block (one
EncodeStringList after ImplUsedUnits, before HasInitialization) so a
unit's external-library dependencies round-trip across separate
compilation.  IFACE_VERSION is bumped 6 -> 7: the META layout grew, so
a v6 reader must reject these .bif and recompile (without the bump a v6
binary would misparse the extra field and read past the END marker).

Tests: a parser test asserting `external 'c' name 'strlen'` populates
ExternalLib + the program's LinkLibs; a .bif round-trip test for
TUnitInterface.LinkLibs; the magic/version assertion updated to v7; and
the existing e2e strlen case.  bif-coverage.status records
TUnitInterface.LinkLibs as serialised.
2026-06-30 17:44:23 +01:00
Andrew Haines e2ee936663 fix(codegen): assign a method pointer to an implicit-Self field
Assigning @Obj.Method to a bare (implicit-Self) method-pointer field —
FFn := @Self.M with no Self. on the left-hand side — was mishandled by
both the QBE and native backends.  The assignment emitter routes all
implicit-Self field stores through a dedicated block that returns before
the method-pointer assignment handler runs, so a 16-byte (Code, Data)
method pointer fell through to the scalar-store path:

  - QBE stored only the 8-byte pointer to the source block, leaving the
    Data half garbage; a later call then dispatched on a bad Self and
    crashed.
  - native reached the @Obj.Method address-of path, which is statement
    level only, and raised "@Obj.Method must be used in assignment
    context".

Add a method-pointer case to the implicit-Self field-assignment path in
each backend, mirroring the existing simple-variable and explicit-field
handlers: copy the whole 16-byte block into Self + field offset, with the
Code half resolved through the instance vtable for a virtual/override
method so the dynamic override is captured.

Explicit Self.FFn := @Self.M and the implicit-Self call FFn(...) were
already correct; only the implicit-Self assignment was affected.

Tests: cp.test.proctypes_ofobject gains an IR test asserting the
implicit-Self method-ptr field store emits a 16-byte memcpy;
cp.test.e2e.misc gains an end-to-end test that assigns via implicit Self
then calls back through the field (reading another field via Self, so a
wrong Data half would crash).  Both fail before this change and pass
after.

# Conflicts:
#	compiler/src/test/pascal/cp.test.e2e.misc.pas
2026-06-30 17:36:30 +01:00
Graeme Geldenhuys f6c1cee9fe fix(codegen): return function-of-object via two-register aggregate ABI
A `function ... of object` return value is a 16-byte [Code, Data]
method pointer, ABI-identical to `record Code, Data: Pointer end`.  It
was returned as a scalar (only the Code half), so a method captured from
a call (`M := Obj.GetFn()`) and later invoked dropped the Data/Self half
— a call through M then dereferenced a null/garbage Self.

Route the return through the record-return ABI: a canonical 16-byte
[Code, Data] two-pointer record classifies as two integer eightbytes ->
rcInt2 (rax:rdx) on SysV, matching a direct record return.  Both the
QBE and native x86-64 backends are fixed.

The native side mirrors the QBE approach via a lazily-built canonical
`_BlaiseMethodPtr` record (distinct GNative* singletons so the two
backends' identically-named globals do not collide when both are linked
into the self-hosting compiler), wired through BuildFrame's return
classification (rcInt2, not sret), the epilogue (loads both halves into
rax:rdx), the Result-slot zero-init (both 16 bytes), and every call-site
capture: variable- and field-destination assignment of a
method-ptr-returning call, and the immediate-invoke path
(`Obj.GetFn()(args)`) which materialises the 16-byte block and dispatches
through it so Data lands in %rdi.

Tests: an IR-level assertion that the return uses the aggregate ABI, plus
e2e cases.  The new TestRun_MethodPtrReturn_ReadsSelf returns a method
that READS Self (an instance field) — unlike the X+Y case, which never
touches Self and so could not catch a dropped Data half — and asserts the
right value on every backend.
2026-06-30 17:34:26 +01:00
Graeme Geldenhuys c32bc73f2c fix(codegen): capture the dynamic override when taking @Obj.VirtualMethod
Taking the address of a virtual method through a receiver
(`M := @Obj.Method`) stored the statically-resolved declared-type
method address as the Code half of the (Code, Data) method pointer.
A later call through M therefore always ran the declared type's method,
ignoring the receiver's dynamic override — unlike a direct `Obj.Method()`
call, which dispatches through the vtable.

Resolve the Code half through the instance's vtable when the method is
virtual/override (VTableSlot >= 0): load the vptr from the object,
index slot (VTableSlot + 1)*8 (slot 0 is typeinfo), and store the
resulting code pointer. Non-virtual methods keep the static label.

Both the QBE and x86-64 backends had the same defect; the native side
fixes it at both the variable- and field-destination assignment sites.

Additionally fixes a pre-existing native-backend bug this change exposed:
EmitClassSection re-asserts FSymTable.DefineOwningUnit to the unit being
emitted before every FindType.  Resolving an earlier class in the loop
(its parent/field/method types via ClassSymName -> Lookup -> the
uses-chain walk) could leave DefineOwningUnit pointing at a dependency
unit; FindType for one of this unit's own implementation-section
(IsImplPrivate) classes was then suppressed by Lookup's cross-unit-leak
guard and returned nil, so the class was skipped and its typeinfo /
vtable / _FieldCleanup were never emitted.  The dangling
`typeinfo_<Unit>_<Class>` reference (e.g. a metaclass value passed to
RegisterTest from the init block) then bound to a garbage address,
producing a layout-sensitive out-of-range-metaclass crash at runtime.

Tests: an IR-level assertion that the capture no longer stores the
static method label, plus two e2e cases (variable and field
destination) asserting the override runs on every backend.
2026-06-30 16:53:55 +01:00
Graeme Geldenhuys b5f8800817 fix(testing): rename global GRegistry to GTestRegistry to avoid symbol clash
The test framework's `GRegistry: TList<TTestCaseClass>` collided with
`GRegistry: TTargetRegistry` in blaise.codegen.toolkit.  The compiler emits
unit-level globals under their bare name with export (strong) linkage, so both
`GRegistry` symbols resolved to one shared 8-byte slot.  When the test runner
links both units, the test framework and the codegen toolkit alias the same
storage and free/read it as the wrong type at teardown.

Rename the test-framework global to GTestRegistry — a unit-specific name that
collides with nothing else — so the two registries occupy distinct storage.
2026-06-30 01:44:28 +01:00
Graeme Geldenhuys 93596d135c fix(toolchain): rename TToolKind.tkAs to tkGnuAs to remove enum ambiguity
`tkAs` was a member of two enums: TToolKind (uToolchain, ordinal 1) and
uLexer.TTokenKind (the lexer token enum, ordinal 40).  In ResolveAssembler,
the bare assignment `Result.Kind := tkAs` should bind to TToolKind, but
bare-member resolution is cross-unit order-fragile and could bind it to
TTokenKind.tkAs (ordinal 40) instead.  40 is out of range for the 4-member
TToolKind, so any downstream code that switches or indexes on the resolved
Kind runs off the end into a wild pointer — a layout-sensitive crash that
manifested intermittently in the test runner (a metaclass typeinfo symbol
truncation surfacing as a SIGSEGV during teardown).

Renaming the member to tkGnuAs removes the ambiguity entirely: `tkAs` now
exists only in uLexer.TTokenKind.  This is bootstrap-safe (no qualified-enum
syntax, which the release binary does not yet support).  Verified the
ResolveAssembler codegen now bakes Ord(tkGnuAs)=1 (was 40).
2026-06-30 01:44:17 +01:00
Graeme Geldenhuys c3ed2c89d6 fix(debug-opdf): emit recPrimitive for Double and Single
Float-typed variables (Double, Single) were invisible in pdr — locals,
print, gl and every other inspection command failed for floats while
integers, strings, classes and records-of-int-and-string worked. The OPDF
emitter never produced a recPrimitive for tyDouble/tySingle: EmitTypeDesc
did not dispatch the float kinds, so a float variable's GlobalVar/LocalVar
record referenced a TypeID with no corresponding type record, and pdr could
not resolve the type to read the value.

EmitTypeDesc now routes tyDouble/tySingle to EmitPrimitive, which emits the
record with SizeInBytes (8/4), IsSigned=1, and a new SubKind=SK_FLOAT (4).
The pdr side already handled this fully: the adapter maps SubKind skFloat to
Category tcFloat and TFloatEvaluator reads the IEEE 754 value by size — it
was only waiting for the compiler to emit the record.

Verified end to end with pdr: a global, a local, and a Double record field
all print their values (d = 3.14159, s = 2.5, pt.Z = 9.81), and locals lists
them. IR tests assert the Double/Single recPrimitive carries SubKind=4 and
the correct size.
2026-06-29 12:53:25 +01:00
Graeme Geldenhuys 2759a27329 Bif tool now enabled to compile by default.
It's small, fast and useful. So no reason why it shouldn't be compiled
by default.
2026-06-29 12:29:55 +01:00
Graeme Geldenhuys f026628329 fix(kanban): add parens to statement-position PrintUsage call
Commit 50b8d75e made parentheses mandatory on statement-position calls,
but kanban_app.pas was not updated, so 'PrintUsage' (a bare reference in
the --help branch) failed to parse with 'Bare reference to PrintUsage
requires () for a call'. This broke a plain 'pasbuild compile', which
reaches blaise-kanban before blaise-bif-coverage. Add the parens.
2026-06-29 12:29:55 +01:00
Graeme Geldenhuys 5754ca254b build(bif-coverage): add stdlib unit path so it builds by default
The tool is now activeByDefault, so it is reached by a plain
'pasbuild compile'. With manualUnitPaths=true it only searches the listed
paths, which lacked the stdlib directory where SysUtils and the other
units it uses live, so the default build failed with 'Unit SysUtils not
found'. Add ../../stdlib/src/main/pascal, mirroring the compiler module's
own stdlib unit path.
2026-06-29 12:29:55 +01:00
Graeme Geldenhuys a934d73566 fix(bif): serialise the overload directive across the unit-interface boundary
Imported methods always had IsOverload=False: the .bif method layouts
round-tripped IsVirtual/IsOverride/IsStatic but not IsOverload, and the
importers never set it. ResolveMethodOverload's hiding walk stops at the
first non-overload candidate, so an overload set split across an imported
class and an imported ancestor would be truncated to the more-derived
level (latent today because the known overload sets live on a single
class).

Add IsOverload to both method-encoding paths and bump BLAISE-IFACE 5 -> 6:
  * TRoutineSig (class methods): field added in uUnitInterface, set in
    BuildRoutineSig, encoded/decoded in EncodeMethodSig/ReadMethodSig,
    propagated in SynthesiseMethodDecl.
  * TMethodDecl (interface and generic-template methods, the
    EncodeMethodDecl/ReadMethodDecl path): the same flag was dropped there
    too, so an overloaded interface method lost its directive across the
    .bif.

Round-trip tests for both paths: TestRoundTrip_Class_WithOverloadedMethods
and TestRoundTrip_Interface_WithOverloadedMethods. bif-coverage status
gains TRoutineSig.IsOverload.
2026-06-29 12:29:55 +01:00
Graeme Geldenhuys fdf99b26c0 test(opdf): guard recProperty layout integrity for method-backed properties
The recProperty corruption (garbled Name/ReadMethod/WriteMethod string
fields) was fixed in c6d22aac, but the only property test asserted just
the '# recProperty' comment — it would not have caught the RecSize/layout
mismatch that caused the reader to overshoot into the next record.

Add TestOPDF_Property_MethodBacked_LayoutIntact: for a method-backed
property it asserts the exact RecSize (32-byte fixed payload + the three
string lengths), the getter/setter symbol strings, and the three length
words. This pins the writer's declared size to the actual emitted bytes,
which is the invariant the reader (SizeOf(TDefProperty)=32 then three
length-prefixed strings) depends on.
2026-06-29 11:00:24 +01:00
Graeme Geldenhuys 25cd8e3ee7 test(sepcompile): cover static property in cross-unit .bif round-trip
The cross-unit static-members tests (QBE + native) already drive a static
var, static method and static const through the warm --unit-cache .bif
boundary. Add a static property (a getter-backed 'static property Counter
read Next') to the shared test unit so TPropertyDecl.IsStatic surviving the
.bif round-trip is guarded too: a non-static import would dispatch the
getter with a bogus Self. Expected stdout updated to include the extra
Counter read.
2026-06-29 10:08:44 +01:00
Graeme Geldenhuys 8d1cdc8f61 fix(codegen): chained l-value base through a qualified static var
'TFoo.X.Field := V' and 'TFoo.X.Method()' failed semantic analysis with
"Field access requires a record or class base, got 'class of TFoo'".
The read form (TFoo.X.Field as an expression) already worked.

When a qualified static var is the base of a further chain on an l-value,
the parser shapes it as a Base-form field access whose base is the bare
class-name ident resolving to the 'class of TFoo' metaclass, rather than
the simple RecordName form. AnalyseFieldAccess's chained-base path
rejected a metaclass base outright.

Semantic: in the chained-base path, when the base type is a metaclass,
resolve the field as a static var (IsClassVarRead) or static property
(IsStaticPropGet) of the metaclass's base class, mirroring the RecordName
static-member read path, so the result type feeds the outer chain.

QBE codegen: EmitInstancePtr loads the instance pointer from the static
var's global slot for an IsClassVarRead base, and the field-access reader
checks IsClassVarRead before the chained-base branch so a Base-form static
var is loaded directly rather than treated as a chained field of the
metaclass. The native backend already produced correct code.

Tests cover the read, a scalar field write, and a method call through the
chained static-var base on both backends.
2026-06-29 01:13:00 +01:00
Graeme Geldenhuys e950814a34 fix(codegen): interface-typed static var store via static methods
A static var of interface type round-tripped through static methods
(THolder.SetIt(X: IThing) / THolder.GetIt: IThing) was miscompiled on
both backends. The 2-slot fat-pointer global slot itself was correct;
the bugs were in the static-CALL path.

QBE: a static method whose first parameter is interface-typed emitted
'call $T_M(, l obj, l itab)'. The interface argument fragment carries a
leading ', ' (it is normally appended after Self or a preceding arg), and
the static-call argument loop did not strip it, so the first argument
began with a comma and QBE rejected it as an invalid class specifier.
Strip the leading ', ' in that branch, matching the existing
EmitMethodCall var-arg/interface precedent.

Native: a static method returning an interface routed through
EmitClassIntfSretMethodCall (ResolvedClassType is the owning class, a
class type), which loaded the bare type name as a global Self and emitted
an undefined reference to the class symbol at external-cc link time (the
internal/driver linker masked it). Add an IsStaticCall branch that emits
the plain free-function interface-sret ABI with no receiver, mirroring
the non-Self arm of EmitIntfSretCall.

Tests: TestIR_StaticCall_InterfaceArg_NoLeadingComma asserts the QBE call
has no leading comma; TestRun_StaticVar_InterfaceStore compiles and runs
the SetIt/GetIt round-trip on both backends.
2026-06-29 01:01:25 +01:00