Skip to content

Commit 3ba82d0

Browse files
authored
refactor(compiler): remove entry point wrappers (#600)
1 parent b7b1cb3 commit 3ba82d0

528 files changed

Lines changed: 11282 additions & 17435 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎crates/plotnik-lib/src/bytecode/dump.rs‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,8 @@ impl DumpContext {
8989
}
9090
}
9191

92-
fn label_for(&self, addr: SuccessorAddr) -> Option<&str> {
93-
self.addr_labels
94-
.get(&CodeAddr::from(u16::from(addr)))
95-
.map(|s| s.as_str())
92+
fn label_for(&self, addr: CodeAddr) -> Option<&str> {
93+
self.addr_labels.get(&addr).map(|s| s.as_str())
9694
}
9795
}
9896

@@ -547,7 +545,7 @@ impl DumpFormatter<'_> {
547545

548546
fn format_addr(&self, addr: SuccessorAddr) -> String {
549547
let c = &self.ctx.colors;
550-
if let Some(label) = self.ctx.label_for(addr) {
548+
if let Some(label) = self.ctx.label_for(CodeAddr::from(u16::from(addr))) {
551549
format!("▶({}{}{})", c.blue, label, c.reset)
552550
} else {
553551
format!("{:0w$}", u16::from(addr), w = self.addr_width)

‎crates/plotnik-lib/src/bytecode/entry_point.rs‎

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,33 @@
33
use super::instructions::CodeAddr;
44
use super::{StringId, TypeId};
55

6+
/// Output effects owned by an entry point rather than its shared definition body.
7+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8+
#[repr(u16)]
9+
pub(crate) enum EntryBoundary {
10+
/// The definition body already produces the complete result.
11+
Passthrough = 0,
12+
/// Capture the current node after the definition returns successfully.
13+
Node = 1,
14+
/// Open a root record before execution and close it after a successful return.
15+
Record = 2,
16+
}
17+
18+
impl EntryBoundary {
19+
pub(crate) fn from_u16(value: u16) -> Option<Self> {
20+
match value {
21+
0 => Some(Self::Passthrough),
22+
1 => Some(Self::Node),
23+
2 => Some(Self::Record),
24+
_ => None,
25+
}
26+
}
27+
28+
pub(crate) fn to_u16(self) -> u16 {
29+
self as u16
30+
}
31+
}
32+
633
/// Named query definition entry point (8 bytes).
734
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
835
#[repr(C)]
@@ -13,7 +40,8 @@ pub struct EntryPoint {
1340
target: CodeAddr,
1441
/// Result type.
1542
result_type: TypeId,
16-
_pad: u16,
43+
/// Entry-owned result boundary effects.
44+
boundary: u16,
1745
}
1846

1947
const _: () = assert!(std::mem::size_of::<EntryPoint>() == EntryPoint::SIZE);
@@ -22,12 +50,17 @@ impl EntryPoint {
2250
/// Serialized size in bytes.
2351
pub const SIZE: usize = 8;
2452

25-
pub fn new(name: StringId, target: CodeAddr, result_type: TypeId) -> Self {
53+
pub(crate) fn new(
54+
name: StringId,
55+
target: CodeAddr,
56+
result_type: TypeId,
57+
boundary: EntryBoundary,
58+
) -> Self {
2659
Self {
2760
name,
2861
target,
2962
result_type,
30-
_pad: 0,
63+
boundary: boundary.to_u16(),
3164
}
3265
}
3366

@@ -37,7 +70,7 @@ impl EntryPoint {
3770
.expect("entry point name id must be non-zero"),
3871
target: CodeAddr::from(u16::from_le_bytes([bytes[2], bytes[3]])),
3972
result_type: TypeId::from(u16::from_le_bytes([bytes[4], bytes[5]])),
40-
_pad: 0,
73+
boundary: u16::from_le_bytes([bytes[6], bytes[7]]),
4174
}
4275
}
4376

@@ -50,4 +83,13 @@ impl EntryPoint {
5083
pub fn result_type(&self) -> TypeId {
5184
self.result_type
5285
}
86+
87+
pub(crate) fn try_boundary(&self) -> Option<EntryBoundary> {
88+
EntryBoundary::from_u16(self.boundary)
89+
}
90+
91+
pub(crate) fn boundary(&self) -> EntryBoundary {
92+
self.try_boundary()
93+
.expect("validated entry point has a known boundary mode")
94+
}
5395
}

‎crates/plotnik-lib/src/bytecode/instructions.rs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -802,7 +802,7 @@ pub struct Call {
802802
pub ownership: CallOwnership,
803803
pub nav: Nav,
804804
pub node_field: Option<NodeFieldId>,
805-
pub target: SuccessorAddr,
805+
pub target: CodeAddr,
806806
returns: [Option<SuccessorAddr>; CALL_MAX_RETURNS],
807807
returns_len: u8,
808808
consumed_mask: u8,
@@ -815,7 +815,7 @@ impl Call {
815815
node_field: Option<NodeFieldId>,
816816
return_addrs: &[SuccessorAddr],
817817
consumed_mask: u8,
818-
target: SuccessorAddr,
818+
target: CodeAddr,
819819
) -> Self {
820820
assert!(
821821
(1..=CALL_MAX_RETURNS).contains(&return_addrs.len()),
@@ -912,7 +912,7 @@ impl Call {
912912
node_field,
913913
&[successor_addr(4)],
914914
consumed_mask,
915-
successor_addr(6),
915+
CodeAddr::from(u16::from_le_bytes([bytes[6], bytes[7]])),
916916
)
917917
}
918918
Opcode::CallN => {
@@ -927,7 +927,7 @@ impl Call {
927927
node_field,
928928
&return_addrs,
929929
bytes[7],
930-
successor_addr(4),
930+
CodeAddr::from(u16::from_le_bytes([bytes[4], bytes[5]])),
931931
)
932932
}
933933
_ => unreachable!("Call::from_bytes requires Call1 or CallN"),

‎crates/plotnik-lib/src/bytecode/mod.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ pub(crate) use constants::{
4545
VERSION,
4646
};
4747
pub(crate) use effects::{Effect, EffectKind, EffectSuppression, FrameAction, ValueFrameKind};
48+
pub(crate) use entry_point::EntryBoundary;
4849
pub(crate) use format::{
4950
LineBuilder, Symbol, cols, nav_symbol, trace, truncate_text, width_for_count,
5051
};

‎crates/plotnik-lib/src/bytecode/module/decoded.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ pub(crate) struct DecodedCall {
6363
pub(crate) ownership: CallOwnership,
6464
pub(crate) nav: Nav,
6565
pub(crate) node_field: Option<NodeFieldId>,
66-
pub(crate) target: SuccessorAddr,
66+
pub(crate) target: CodeAddr,
6767
returns_base: u32,
6868
returns_len: u8,
6969
}

‎crates/plotnik-lib/src/bytecode/module/effect_stack.rs‎

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
//! materializer's pending-value register is full. Tracking span *ids* (not just
2626
//! a depth) proves every `SpanEnd` closes the span the matching bracket opened,
2727
//! so inspection extraction can assert pairing instead of re-validating. The
28-
//! walk starts from each entry point wrapper and follows `Match` successors,
29-
//! descending through `Call` and resuming at its return address — exactly the
30-
//! edge set that orders effects at runtime.
28+
//! walk starts from each entry definition and follows `Match` successors,
29+
//! descending through `Call` and resuming at its return address. Entry-owned
30+
//! record/node effects are modeled around the resulting definition summary.
3131
//!
3232
//! ## Why state *sets* (collecting semantics)
3333
//!
@@ -75,8 +75,8 @@
7575
//! summary instead of inlining, which both terminates and stays sound. The
7676
//! summaries are computed by a monotone fixpoint (a callee that reads its
7777
//! caller's top before pushing propagates the constraint up to its own
78-
//! callers), then a final pass checks every call site and every entry point
79-
//! wrapper against the stabilized summaries.
78+
//! callers), then a final pass checks every call site and every entry boundary
79+
//! against the stabilized summaries.
8080
//!
8181
//! `record_sets_caller_top` exists because a below-entry `RecordSet` mutates state the
8282
//! caller's walk otherwise cannot see: setting a field on the caller's *variant*
@@ -87,11 +87,9 @@
8787
//! panic past the check.
8888
//!
8989
//! A successor-less `Match` accepts the *whole run* from any call depth,
90-
//! freezing the log with every caller frame still open. Inside an entry point
91-
//! wrapper the local stack is the global stack, so the existing exit check is
92-
//! exact; inside a body reachable through `Call` the caller's frames are
93-
//! invisible here, so such accepts are rejected outright — the compiler ends
94-
//! every definition body with `Return` and only accepts at wrapper level.
90+
//! freezing the log with every caller frame still open. Inside a body reachable
91+
//! through `Call` the caller's frames are invisible here, so such accepts are
92+
//! rejected outright — the compiler ends every definition body with `Return`.
9593
//!
9694
//! Net-neutrality, no popping below entry, and suppression balance are not
9795
//! assumed — they are verified, so a malformed body is rejected
@@ -113,7 +111,7 @@ use std::collections::{HashMap, HashSet, VecDeque};
113111

114112
use super::{Instruction, Module, ModuleError};
115113
use crate::bytecode::{
116-
CodeAddr, Effect, EffectKind, FrameAction, TypeDefKind, TypeKind, ValueFrameKind,
114+
CodeAddr, Effect, EffectKind, EntryBoundary, FrameAction, TypeDefKind, TypeKind, ValueFrameKind,
117115
};
118116

119117
/// Builder frames the materializer pushes. The root/result frame can be a
@@ -328,19 +326,22 @@ pub(crate) fn validate_effect_stack(module: &Module) -> Result<(), ModuleError>
328326
)?;
329327
}
330328

331-
// ...and every entry point wrapper. A wrapper has no caller, so a residual
332-
// caller-top constraint means some effect would read below the frames the
333-
// wrapper itself opened and hit the materializer's result root frame.
329+
// Entry boundaries supply the root frame/effect that used to be executable
330+
// wrapper instructions.
334331
for entry_point in entry_points.iter() {
335332
let target = entry_point.target();
336-
let wrapper = analyze(
337-
module,
338-
&summaries,
339-
target,
340-
BodyRole::from_called(called.contains(&target)),
341-
VerifyPhase::Final,
342-
)?;
343-
if wrapper.entry_tos != KS_ANY {
333+
let summary = summaries[&target];
334+
let boundary = entry_point.boundary();
335+
let accepts_caller_top = match boundary {
336+
EntryBoundary::Record => summary.entry_tos & KS_RECORD != 0,
337+
EntryBoundary::Passthrough | EntryBoundary::Node => summary.entry_tos == KS_ANY,
338+
};
339+
if !accepts_caller_top {
340+
return Err(ModuleError::EffectStackImbalance(target));
341+
}
342+
if matches!(boundary, EntryBoundary::Node | EntryBoundary::Record)
343+
&& summary.returns_pending == Some(true)
344+
{
344345
return Err(ModuleError::EffectStackImbalance(target));
345346
}
346347
}
@@ -600,10 +601,9 @@ fn analyze(
600601
)?;
601602
}
602603
if m.succ_count() == 0 {
603-
// A successor-less match accepts the whole run. At wrapper
604-
// level the local stack is the global stack, so balance
605-
// here is exact; under a `Call` the caller's frames are
606-
// still open in the log, so this is never sound.
604+
// A successor-less match accepts the whole run. Under a
605+
// `Call`, caller frames are still open in the log, so this
606+
// is never sound.
607607
if role == BodyRole::Called {
608608
return Err(ModuleError::EffectStackImbalance(addr));
609609
}
@@ -651,7 +651,7 @@ impl CallRoute {
651651
fn from_instruction(instruction: Instruction<'_>) -> Option<Self> {
652652
match instruction {
653653
Instruction::Call(call) => Some(Self {
654-
target: CodeAddr::from(u16::from(call.target)),
654+
target: call.target,
655655
returns: call
656656
.returns()
657657
.map(|addr| CodeAddr::from(u16::from(addr)))

‎crates/plotnik-lib/src/bytecode/module/load.rs‎

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -574,8 +574,6 @@ impl Module {
574574
let entry_points = self.entry_points();
575575
let word_count = self.header.instruction_word_count;
576576
let type_defs = self.header.type_defs_count;
577-
let storage: &[u8] = &self.storage;
578-
let base = self.offsets.entry_points as usize;
579577
for i in 0..entry_points.len() {
580578
let invalid = || ModuleError::InvalidEntryPoint(i);
581579
let ep = entry_points.get(i);
@@ -593,9 +591,7 @@ impl Module {
593591
return Err(invalid());
594592
}
595593

596-
// Bytes 6-7 are the reserved `_pad`; `EntryPoint::from_bytes` discards
597-
// them, so a malformed non-zero pad would otherwise pass unnoticed.
598-
if read_u16_le(storage, base + i * 8 + 6) != 0 {
594+
if ep.try_boundary().is_none() {
599595
return Err(invalid());
600596
}
601597
}
@@ -622,7 +618,7 @@ impl Module {
622618
let words = match self.decode_instruction(addr) {
623619
Instruction::Match(m) => m.word_count(),
624620
Instruction::Call(c) => {
625-
let target = CodeAddr::from(u16::from(c.target));
621+
let target = c.target;
626622
let route = DepthRoute::from_call(c);
627623
if let Some(expected) = roots.insert(target, route)
628624
&& expected != route
@@ -664,7 +660,7 @@ impl Module {
664660
let words = match self.decode_instruction(addr) {
665661
Instruction::Match(matched) => matched.word_count(),
666662
Instruction::Call(call) => {
667-
let target = CodeAddr::from(u16::from(call.target));
663+
let target = call.target;
668664
if !self
669665
.return_contract(target, &mut cache)
670666
.is(ReturnPorts::dense(call.arity()), call.callee_contract())
@@ -983,7 +979,7 @@ impl Module {
983979
}
984980
let next = read_operand_u16(storage, instr_off + 4)?;
985981
let target = read_operand_u16(storage, instr_off + 6)?;
986-
if next == 0 || target == 0 {
982+
if next == 0 {
987983
return Err(ModuleError::MalformedInstructionStream);
988984
}
989985
targets.push(CodeAddr::from(next));
@@ -1002,7 +998,7 @@ impl Module {
1002998
}
1003999
let target = read_operand_u16(storage, instr_off + 4)?;
10041000
let arity = usize::from(read_u8(instr_off + 6)?);
1005-
if !(2..=usize::from(PortId::COUNT)).contains(&arity) || target == 0 {
1001+
if !(2..=usize::from(PortId::COUNT)).contains(&arity) {
10061002
return Err(ModuleError::MalformedInstructionStream);
10071003
}
10081004
let consumed_mask = read_u8(instr_off + 7)?;

0 commit comments

Comments
 (0)