Skip to content

v3.5.0

Choose a tag to compare

@github-actions github-actions released this 06 Sep 12:13
· 207 commits to main since this release

The release that finished the memory model and then found out what it had left
open. owned ^T went into the library, which is what ADR-0181 was built for,
and adopting it turned up four ways a borrow could outlive what it borrows that
the rule as written did not see — three of them found by a langspec audit whose
whole purpose is that no oracle here can contradict a reading, and every gate
was green over all three.

Two threads of control became something a program can steer. A task is a
value now, with a name and a type; wait joins one; and the select-statement
waits on several channels at once, takes the first arm that can proceed, and
gives up after a timeout. ThreadSanitizer is the oracle underneath that, and it
is a gate rather than a thing run by hand.

A pointer is not always eight bytes. i386 is admitted, which took seven
layout rules rather than the 3858 offsets that looked like the work, and it
brought clong and csize — the first required identifiers whose type
depends on the target. It also decided something this release states outright:
an i386 this compiler emits for has SSE2, because on an x87 an error condition
this language says it detects goes undetected.

And traits. A trait bounds a schema's type-valued discriminant and a
routine's type parameter, an implementation is written by whoever names the
type, and lib/dialect/passortx.pas is the first client that is not a test —
impl Sortable for Point; once, and Sort(ps) thereafter. Four records
preceded one working feature and each says why the previous was wrong; the
first client found the fifth thing in under an hour, which is now a rule in
doc/sop.md.

Added

  • Waiting for a task, and for whichever of several things comes first
    (ADR-0312, ADR-0313, AP 6.4.17, AP 6.9.3.15). task is a required
    type-identifier — shadowable, like int64, and not a word-symbol — and its
    values are handles, so a task is released by the block that declared it,
    moved by take, and refused as an assignment source like every other handle.
    wait(t) blocks until that one task has finished. The select-statement
    waits on several channels at once and takes the first arm whose operation can
    proceed, with the case-statement's own punctuation:

    select
      receive(x, got): send(out, got);
      receive(y, got): send(out, got);
      after 2000: writeln('gave up waiting')
    end

    A send arm waits for room as a receive arm waits for a value, an after
    arm gives up after that many milliseconds, and an otherwise declines to wait
    at all.

  • A third target: --target=i386-pc-linux-gnu (ADR-0325). Every layout rule
    that wrote 8 now asks PtrSize or WordAlign, so a pointer is four bytes
    where the target says so. The corpus runs against a 32-bit runtime under the
    target32 gate; the one program that cannot is a deliberate 2 GB allocation.

  • clong and csize (ADR-0328), two required identifiers denoting whichever
    integer type a C long and a C size_t fit on the target — int64 on x86-64
    and aarch64, integer on i386. An external declaration naming a C routine
    that takes a long is now writable once rather than per target.

  • A protected parameter may be of an owned-pointer-type (ADR-0318,
    AP 6.4.14.8), which is the read-only borrow: a name for what a variable owns,
    read through, and through which nothing may be released. §6.4.1's exclusion of
    a pointer-type does not reach an owned pointer.

  • A discriminated schema may be written wherever a type-name is required
    (ADR-0324, AP 6.7.3.1.1, AP 6.7.2.1) — a parameter-form and a result-type both
    take Vec(integer) where they took only a name.

  • An array determines a slice parameter's component type (ADR-0316), as a
    slice-type already did, so array of T accepts an actual of an array-type
    without the caller writing the slice.

  • PasSortX, a sort over the element type itself (ADR-0344): the trait
    Sortable with Sort, SortWith, IsSorted and LowerBoundOf over an
    array of T. The trait is declared in the module and every implementation
    is written by the client — impl Sortable for Point; once, and Sort(ps)
    thereafter — which is the only shape separate translation admits and is
    what makes a trait usable from a library at all. SortWith takes the order
    as a procedural parameter instead, for a caller whose order is not the
    type's or whose element type implements nothing. PasSort is unchanged and
    stays: it is conforming Extended Pascal, and its SortIndexed still answers
    for parallel arrays.

  • A type may implement a trait, and a trait may bound a type (ADR-0338 to
    ADR-0341, AP 6.4.7.2, AP 6.7.9, AP 6.7.10). trait Sortable; function Rank(u: Self; v: Self): integer; end; names routines a program supplies, and
    impl Sortable for Point; supplies them — each routine written as a name
    alone, the trait having given the heading. A call selects by the type of its
    first argument, after the ordinary lookup, so two implementations of one
    trait live in one block and a program declaring its own routine of the name
    keeps it. A subrange takes its host's implementation. The bound is written
    where the client writes the type — on a schema's type-valued discriminant,
    Map(K: Sortable; V: type; cap: integer), checked once at the type-denoter —
    and also on a routine's type parameter, T: Sortable type, where a category
    would stand. One bound, not two. Neither trait nor impl is reserved. A
    trait may be declared in a module's interface and exported; an
    implementation belongs to one translation, so it may not stand in a
    procedure or in a module heading.

  • A task may be handed a handle (ADR-0303, AP 6.7.8.1): a formal parameter
    of a task-declaration may be of a handle-type, and the actual is written
    take of a variable of that type — the one position outside an assignment
    where AP 6.4.14.6 admits a move. The variable is emptied before the
    activation commences and the task's block releases what it was given, so a
    socket, a stream or a directory can now be given to a task. A channel is
    still lent and a handle is moved; a handle still cannot be sent through
    a channel.

  • A channel is closed by the release the program wrote (ADR-0302,
    AP 6.4.16.4): release(c), c := nil and c := take(d) each close the
    channel wherever they stand, including inside a task. Before this a task's
    release dropped that task's reference and left the channel open, so a
    pipeline of stages each closing the one downstream of it deadlocked with no
    diagnostic. The release performed by the end of a task's block is unchanged
    and still only drops the reference, which is what a pool of workers needs.

  • A release archive on every tag (ADR-0296). Pushing a v* tag now
    attaches afterschool-pascal-<tag>-x86_64-linux.tar.gz and an
    aarch64-linux one, each with a .sha256, to the GitHub release: the
    cmake --install layout, LICENSE, COPYING.RUNTIME and a short README,
    with pascalc linked statically. clang is still needed at use time.
    tools/release.sh builds and checks the archive, release-archive runs
    both halves under ctest, and tests/checks/install_layout.sh --prefix
    checks an unpacked prefix. APASCAL_STATIC_PASCALC is the CMake option.

  • examples/ (ADR-0295): twelve complete programs of a page each,
    written to be read by someone who knows Turbo Pascal, every one a ctest
    case (example-<name>) with a golden, and swept by the same gates as the
    rest of the corpus. Writing them found seven things — a task cannot close
    the channel downstream of it and a program that tries deadlocks silently,
    a map lookup writes two types the call already knows, four of twelve
    programs collided with a library noun — recorded in the ADR and in
    doc/roadmap.md rather than fixed here.

  • tests/checks/heap_balance.py measures a case that resolves its imports
    by .importpath, and --write refuses to write a catalogue when a case
    did not run instead of striking it in silence.

  • diagnostic-coverage and foreign-layout read nothing when the checkout
    is itself a .claude/worktrees tree, their worktree filter having tested
    the absolute path; both now test the path below the root.

Fixed

  • The committed seed no longer names the machine that generated it
    (ADR-0347). Since ADR-0293 a trap carries its own position, so the source's
    path is a string constant in the emitted module — and seed/refresh.sh
    handed the compiler an absolute one, so seed/*.ll held the reseeding
    machine's home directory and tests/checks/seed_current.sh could pass only
    in the directory that had produced it. Both now translate from the repository
    root with a relative path, and a new seed-portable gate asks the cheap half
    of the question on every push rather than at a tag.

  • Disposing a long owned chain or a deep owned tree costs one frame
    (ADR-0322, ADR-0333, AP 6.4.14.3). The release routine walked the structure
    recursively, so a list or a tree deeper than the stack ended the program in a
    segmentation fault rather than freeing it. Every self-owned field is emptied
    onto a work list threaded through the nodes themselves, so the depth costs no
    stack and no allocation.

  • A block could reach an owner through a procedural parameter (ADR-0326),
    and through a formal bound before its own activation existed (ADR-0332). Both
    were ways for a borrow to outlive what it borrowed that the rule as written
    did not see; both are refused, and the second is one conjunct rather than a
    second rule.

  • dispose of a variable whose domain is a schema found no tuple
    (ADR-0329) where the walk reached one below the outermost, so a discriminated
    component was released against the wrong bounds.

  • PasJson reads a number by rounding once (ADR-0314). The reader
    normalised and then computed, accumulating error before the value existed; it
    now writes what it scanned as a Pascal real-literal and converts once, so a
    decimal that reads back exactly does.

  • PasJson writes the shortest number that reads back as the same value
    (ADR-0309). 0.75 came out as 7.500000000000E-01 — valid JSON, and
    §6.9.3.4.1's default real output arriving unchanged — and now comes out as
    0.75. The writer renders at a precision and reads its own output back with
    readstr, keeping the first spelling that returns the value it started
    from; the point goes where ECMAScript's Number::toString puts it, so 10
    is 10 and 1e-7 is 1E-7. A whole number is unaffected: 3 is still 3
    and never 3.0. examples/json_pretty.out and tests/dialect/lib_json.out
    move with it.

  • The language server reads .importpath (ADR-0311). It honoured
    .components and not ADR-0244's other sidecar, so opening any of the seven
    examples/ programs that name a place rather than a list of files put a
    diagnostic on every imported name — 21 of them on word_freq.pas, all
    false. The directories now reach the compiler as --import-path, and the
    two sidecars combine.

  • A source named with no directory finds its neighbours (ADR-0308).
    ADR-0244's first search rule is the source's own directory, and SourceDir
    answered the empty string for a name with no / in it, which AddPath
    drops on purpose — so a program and its modules in one directory compiled
    under pascalc ./prog.pas and not under pascalc prog.pas. The answer is
    ./, and bare-source-name is the new gate; it has to be a harness because
    no test case can choose how it is named.

  • A channel may carry a string(n) or a text (ADR-0302). send chose its
    path with IsStructured, which a variable-string is not, so a program
    sending one did not assemble at all; and copying the element's size out of a
    string value read past what the expression produced, because a value is
    shorter than the type it is going into. The value is now stored into a
    temporary of the element type by the ordinary assignment, so it is padded
    and — for a text — normalised where it crosses.

Changed

  • take inside a generic means move where the type moves (ADR-0323). A
    generic body may write take(v) for a type parameter that turns out not to be
    affine; there the variable is unchanged and not threatened, and the operation
    denotes its value. Outside a generic it is refused as before, so one body
    serves an element type that moves and one that copies.

  • A borrow is refused where it is formed (ADR-0319, AP 6.4.14.9). A
    variable-access reached through a dereference of an owned pointer may not be
    passed to a var parameter of a call whose callee can name the entire
    variable that owns it, and where such an access is a with-element, no call
    in the body may activate a block that can name it. The earlier rule refused
    the release and let the dangling name be formed.

  • A schema domain is admitted only where what it produces holds nothing
    affine
    (ADR-0320, AP 6.4.14.2). owned ^Vec(integer) is accepted where the
    produced type contains no file, handle or owned pointer, and refused where it
    does — which is what let the owned pointer into the library at all.

  • An i386 this compiler emits for has SSE2 (ADR-0346,
    doc/implementation-defined.md §2.2). tools/pascalcc names
    -march=pentium4 on every clang it starts for i386-pc-linux-gnu, and for
    no other target. clang's own default processor for that triple is i686,
    whose x87 registers are eighty bits wide, and two things break on those
    eighty bits: §6.7.6.3 defines round(x) as equivalent to trunc(x ± 0.5)
    and the two disagree at -0.49999999999999994, and D.32's sqr error goes
    undetected, sqr(-1e200) being an ordinary finite number in a register
    with a fifteen-bit exponent. What is given up is a Pentium III and earlier.
    A caller who wants the x87 back can name -march=i686 in
    AFTERSCHOOL_PASCAL_CFLAGS.

  • A trait may not be named numeric, ordinal, ordered or equatable
    (ADR-0344, AP 6.7.9). Those four spellings name a type-parameter category
    wherever a bound is written and nothing is looked up there, and a bound is
    the only position a trait may stand in — so a trait of one of those names
    could be declared and implemented and never applied. It is refused at the
    declaration, where before the program was told at the call that its type was
    not admitted by a category it had never written.

  • PasContainer states how to choose a map's key type (ADR-0310). Nothing
    in the library changed and nothing new is exported: the map has been generic
    over its key since ADR-0254 and MapKey (string(63)) is a ready-made key
    type, not a bound — ADR-0295's sixth finding said otherwise and was wrong,
    which a map keyed at 200 holding a 130-character key settles. What is real is
    that a program keyed by text from outside must choose a capacity, so the
    module header, lib/dialect/README.md and doc/tour.md now give the rule:
    size the key to a bound the program already has, guard against
    m^.slots[1].key.capacity where it has none, never clamp with substr.
    examples/word_freq.pas is rewritten to the first shape and has no guard;
    its output is unchanged.

  • A generic activation may write a prefix of its type arguments
    (AP 6.7.3.10.4, ADR-0304). ADR-0254 admitted two forms, every type argument
    or none; an activation may now write the first k of them and leave the
    rest to be inferred, so a routine whose type stands only in the result —
    which §6.7.1 makes a type-name and not an actual, so nothing can determine
    it — no longer makes its caller write the types the arguments already say.
    VecGet(char, b, i) where VecGet(JsonChars, char, b, i) was required.
    The arity says how many were written; the tie-break separating this from an
    activation short of an argument is asked at the first omitted type
    parameter. Every existing call keeps its meaning.

  • PasContainer.VecGet and MapKeyAt take their result type first
    (ADR-0304). VecGet(Elem, Ptr, …) and MapKeyAt(K, Ptr, …), so that the
    one type an activation must name is the prefix and VecGet(integer, v, 3)
    is the call. This breaks a client that wrote the old order. Nothing else
    in PasContainer names a type at a call: MapGet(m, k, 0, StrHash, StrEq)
    has been inferable since ADR-0254 and this module's own header said
    otherwise.

  • Every file variable is bindable (AP 6.5.1, ADR-0299). bind, unbind
    and binding accept any variable-access that denotes a file, whether or not
    its type-denoter says bindable: a var f: text parameter — §6.7.6.8's own
    example bindfile — a dereference of ^text, a text field and a text
    element. The word is still accepted wherever §6.4.1 admits it and is
    redundant on a file; on a non-file it keeps its meaning and bind refuses
    it by design. ISO/IEC 10206:1991 programs keep their meaning; what moves is
    that a refusal the standard requires is no longer made. The diagnostic
    'f' is not bindable; only a variable whose type-denoter says 'bindable'
    can be bound to something outside the program
    is gone with the rule, and
    'bind' needs a file variable, found integer now ends : only a file
    variable is bindable
    .

  • PasFile takes the file through a parameter. Three helpers take
    var f: text; every exported name and answer is unchanged.