This document tracks changes made while porting a language to plcc-ng
that affect something an instructor's slides, handouts, or lecture notes
would need to match — a renamed field referenced in a semantics
walkthrough, a changed output format, a renamed grammar symbol. It is
distinct from CHANGELOG.md and issue history: those answer "what
happened in the repo"; this answers "what do I need to update in my
course materials."
Entries are added under the relevant language's heading, in the same commit that makes the change (see CONTRIBUTING.md). Language headings are added in the order each language is migrated.
- The identifier token is renamed from
VARtoSYMBOL, and captured as bare<SYMBOL>, soVarExp's field issymbol(self.symbol/symbol/this.symbol), not the originalvar. This corrects a misnomer — in V0–V6 aSYMBOLnames an immutable binding, not a mutable variable — and sidesteps thevar/JavaScript reserved-word collision (plcc-ng 2.0.0 rejects avarfield outright). The AST node class staysVarExp. ThisSYMBOL/symbolspelling is the standing convention for every language. Course material walking through V0 should refer to theSYMBOLtoken and thesymbolfield.
- Same
VAR→SYMBOLtoken rename as V0: the field issymbol, not the originalvar. V1's semantics now readenv.applyEnv(self.symbol.lexeme)/env.applyEnv(symbol.lexeme)/env.applyEnv(this.symbol.lexeme), so course material walking through V1'sVarExpevaluation should refer to thesymbolfield. - Nonterminals are PascalCased (
<program>→<Program>,<exp>→<Exp>,<prim>→<Prim>,<rands>→<Rands>), following the same convention V0 established. V1 is the first language with real semantics (Env-based evaluation) to carry it, so course material walking through V1's grammar or its Env-lookup semantics should use the PascalCased nonterminal names. - Java's
Prim.applynow takesVal [] varather thanList<Val> args, withVal.toArrayrestoring the array from the list inPrimappExp.eval. Operand access isva[0]/va.length, matching Python'sargs[0]/len(args)and JavaScript'sargs[0]/args.length. Course material walking through a primitive'sapplycan now use one indexing idiom across all three target appendices instead of Java'sargs.get(0)/args.size().
- Same
VAR→SYMBOLtoken rename as V0/V1: the field issymbol, not the originalvar. IfExp's threeExpchildren are captured with the original camelCase alt-namestestExp/trueExp/falseExp(plcc-ng 2.0.0 preserves alt-name casing; the earlier all-lowercase workaround for issue #6 is no longer needed). Course material walking throughIfExp.eval()should refer toself.testExp/testExp/this.testExp, etc.- Java's
Prim.applynow takesVal [] varather thanList<Val> args, withVal.toArrayrestoring the array from the list inPrimappExp.eval. Operand access isva[0]/va.length, matching Python'sargs[0]/len(args)and JavaScript'sargs[0]/args.length. Course material walking through a primitive'sapplycan now use one indexing idiom across all three target appendices instead of Java'sargs.get(0)/args.size().
- Same
SYMBOL/symbolconvention as V0-V2 (the identifier token isSYMBOL, captured assymbol; inletLHS positions it is captured as the listsymbolList). - New
letproductions:<Exp:LetExp> ::= LET <LetDecls> IN <Exp>and<LetDecls> **= <SYMBOL> EQUALS <Exp>. Course material walking throughLetDeclsshould refer to itssymbolList/expListfields. envVal.initEnv()is empty (no preset bindings) — unlike V1/V2'senvRN, which preset the Roman-numeral values. Every V3 example mustlet-bind the variables it uses; there is no ambientx/v/m.- Duplicate-detection and the two-list binding construction now read a
token's value via
.lexeme(not.toString(), which under plcc-ng 2.0.0 (and unchanged in 2.0.1) yields thesource:line:col TOKEN 'lexeme'scan format), and raiseLanguageErrorrather than the oldPLCCException. LetDecls.addBindingsevaluates eachExpinexpListinline (e.g. Python:[e.eval(env) for e in self.expList]) rather than constructing aRandsobject and callingevalRandson it, unlikePrimappExp.eval, which does delegate throughRands/evalRands. This is a deviation from how the original pre-migration course material'sLetDeclswas shaped. Course material comparingLetDecls.addBindingsagainstPrimappExp.evalshould note that the two look different on purpose:LetDeclsdoesn't have a pre-existingRandsnode to reuse the wayPrimappExpdoes.LetExp.toString()/LetDecls.toString()are the original course material's placeholder stubs ("... LetExp ..."/"... LetDecls ...") and are preserved verbatim, not "finished".- Java's
Prim.applynow takesVal [] varather thanList<Val> args, withVal.toArrayrestoring the array from the list inPrimappExp.eval. Operand access isva[0]/va.length, matching Python'sargs[0]/len(args)and JavaScript'sargs[0]/args.length. Course material walking through a primitive'sapplycan now use one indexing idiom across all three target appendices instead of Java'sargs.get(0)/args.size().
- Same
SYMBOL/symbolconvention as V0-V3, but V4 widens the identifier pattern to[A-Za-z][\w?]*so a name may end in?. This is a real V4 language feature carried over from the original grammar, not migration drift —Prog/oenames its procedureseven?andodd?. (V5's old grammar carries a comment claiming the?arrives at V5; that comment is stale — the widening is already V4's.) - New productions:
<Exp:ProcExp> ::= <Proc>,<Exp:AppExp> ::= DOT <Exp> LPAREN <Rands> RPAREN,<Exp:SeqExp> ::= LBRACE <Exp> <SeqExps> RBRACE,<SeqExps> **= SEMI <Exp>,<Proc> ::= PROC LPAREN <Formals> RPAREN <Exp>, and<Formals> **= <SYMBOL> +COMMA. Formals' list field issymbolList(self.formals.symbolList/formals.symbolList/this.formals.symbolList), not the original Java code'svarList. Course material walking throughProcVal.applyor theprocformals duplicate-check should usesymbolList.SeqExpreads its trailing expressions fromseqExps.expList; the first expression is the separate fieldexp.{a; b; c}evaluatesafirst and yields the value ofc.Prog/oeandProg/fibare shrunk so every shipped example runs in all three targets.oe's final call is now.even?(10, even?, odd?)instead of.even?(11000, even?, odd?)(output unchanged:1), andfibnow computes.fib(10)instead of.fib(30), so its output changes from832040to55.oe's original argument exceeded the interpreter recursion depth available in Python (a ~1,000-frame default limit, several frames per language-level call; measured at roughly 330 language-level calls);fib's original argument was measured at 66 seconds of Python runtime, not minutes. Course material quoting either argument orfib's result needs updating.ProcValholdsformals,body, and the environment captured atproc-evaluation time (self.env/env/this.env);applyextends that captured environment, not the caller's — this is what makes V4's procedures lexically scoped, and it's the first question a student asks walkingProcVal.apply.Val.applykeeps a secondenvparameter (PythonVal.apply(self, args, env), Javaapply(List<Val> args, Env e), JavaScriptapply(args, env)) that nothing in V4 (or V5/V6) reads; it is preserved because it is the signature the course material shows. Course material walkingProcVal.applyshould point out that the environment it extends is the one closed over atproc-time, not theenv/eargument passed toapply.AppExp.toString()andSeqExp.toString()are the original course material's placeholder stubs —" ... AppExp ..."and" ... SeqExp ... ", irregular spacing included — preserved verbatim rather than "finished", the same treatment V3 gaveLetExp/LetDecls.ProcExphas notoString()at all, because the original has none. Course material quoting any of the three should not expect a real rendering of a procedure application, a sequence, or aprocexpression.- V4's runtime errors —
Cannot apply …(Val.apply's default), theformals/argsnumber mismatch, and the duplicate-formals check (Env.checkDuplicatesonFormals.symbolList) — raise plcc-ng'sLanguageError, the same substitution V3 made for old PLCC'sPLCCException. Course material quoting V4's error handling should refer toLanguageError. - Java's
Prim.applynow takesVal [] varather thanList<Val> args, withVal.toArrayrestoring the array from the list inPrimappExp.eval. Operand access isva[0]/va.length, matching Python'sargs[0]/len(args)and JavaScript'sargs[0]/args.length. Course material walking through a primitive'sapplycan now use one indexing idiom across all three target appendices instead of Java'sargs.get(0)/args.size().
- New token
LETREC 'letrec'and one new production,<Exp:LetrecExp> ::= LETREC <LetDecls> IN <Exp>.LetDeclsis shared withLetExp—letrecintroduces no new nonterminal. - Same
SYMBOL/symbolconvention as V0-V4. V5's old grammar carried a header comment claiming variable names "now can have a?in them" at V5; that comment was stale and is not carried forward — the widening to[A-Za-z][\w?]*already happened at V4. Course material that attributes the?to V5 should attribute it to V4. LetrecExpandProcExpdo have placeholdertoString()/__str__methods, returning" ...LetrecExp... "and" ...ProcExp... ". (Corrected: V5 originally shipped without them, on the grounds that the pre-migration source had none. ForLetrecExpthat was true of V5 alone — the production is new at V5, so there is no earlier instance to compare against. ForProcExpit wasn't: V4'sProcExphas notoString()either, and V4 is out of scope for this branch and keeps it that way. V6 and all seven languages of Phases 3-5 give both methods atoString(), so V5 was brought into line with those rather than left as an outlier on this point.)letrecis implemented by mutation, not by a fixpoint.LetDecls.addLetrecBindings(env)extends the environment with an emptyBindings, then evaluates each right-hand side in that already-extended environment and adds the resulting binding to that same node withenv.add(...). Aprocright-hand side captures the env object while it still holds zero bindings; later additions to that same node are what make its siblings (and itself) visible by the time anything is called. This is worth showing explicitly on a slide — it is not the placeholder-and-patchletrecmost textbooks present.- A consequence worth stating in class: the binding pass is eager and
sequential, so a right-hand side that looks something up can only
see bindings to its left.
letrec f = proc(...) .g(...) g = proc(...) .f(...) in ...works (neither side looks anything up while binding), butletrec a = b b = 1 in afails withno binding for b. Faithful to the original V5, not a porting artifact. LetDecls' list fields aresymbolListandexpList(the original Java code'svarList/expList). Course material walking throughaddBindingsoraddLetrecBindingsshould usesymbolList.- The duplicate-identifier message is now
duplicate ID <id> in let/letrec LHS identifiers— V4 and earlier say... in let LHS identifiers.LetDeclsis shared by bothletandletrec, so the check covers both. letrecmakes direct self-recursion (.f(sub1(x))) the natural thing to write, and Python's practical ceiling for it is far lower than Java's or JavaScript's: measured withletrec f = proc(x) if zero?(x) then 0 else .f(sub1(x)) in .f(N), Python dies aroundN=330while Java and JavaScript both survive pastN=2700. If a live demo or assignment pushes recursion depth into the hundreds on the Python target, expect aRecursionErrorthere well before the other two targets show any trouble — see CR-519.- V5's four placeholder strings were normalized to the
" ...ClassName... "spelling V6 and the seven not-yet-ported languages (Phases 3-5) use."... LetExp ...","... LetDecls ..."," ... AppExp ...", and" ... SeqExp ... "— three different spacings — became" ...LetExp... "," ...LetDecls... "," ...AppExp... ", and" ...SeqExp... ". No observable output changed: nothing in the test suite prints anExp. V3 and V4 were out of scope for this normalization and still carry the old irregular spellings, so course material walking V3 → V4 → V5 will see the spelling change land at V5. Slides quoting the old spellings should be updated, but no result differs. - Java's
Prim.applynow takesVal [] varather thanList<Val> args, withVal.toArrayrestoring the array from the list inPrimappExp.eval. Operand access isva[0]/va.length, matching Python'sargs[0]/len(args)and JavaScript'sargs[0]/args.length. Course material walking through a primitive'sapplycan now use one indexing idiom across all three target appendices instead of Java'sargs.get(0)/args.size().
- New token
DEFINE 'define', declared directly afterLETREC. <Program>is no longer a single production. It splits into two alternatives,<Program:Define> ::= DEFINE <SYMBOL> EQUALS <Exp>and<Program:Eval> ::= <Exp>, so a V6 "program" is now one or the other and a source file is a sequence of them. Course material that describes<program> ::= <exp>for V6 needs both alternatives.- Same
SYMBOL/symbolconvention as V0-V5: the pre-migration V6 grammar'sVARtoken is namedSYMBOLin the port, soDefine's generated field issymbol, notvar. Programno longer has a_run(). It keeps only the sharedenv = Env.initEnv(), andDefineandEvaleach get their own_run(). The environment is created once perplcc-repprocess and shared by every program in the run.Define._run()returns the defined name, not the value. The pre-migration Java ended inSystem.out.println(s)and returnedvoid; under plcc-ng an entry point must return its output as a string. Same observable output, different mechanism - worth a note wherever the Java source is shown.defineis implemented by mutating a single environment node, the same mechanism V5'sletrecuses, hoisted to the top level. A new name isadded to that node; an existing name has itsBinding.valreassigned in place.definenever extends or replaces the environment.- Two consequences worth showing on a slide, both demonstrated by files
already in
src/V6/Prog/:- A redefinition reaches closures that already exist, because the
assignment goes through the very
Bindingobject the closure holds.Prog/x:.f()gives2, thendefine x=3, then.f()gives3. - A closure over a
let-bound copy is immune, becauseletextends a new node with its ownBinding.Prog/xx:.f()gives2both before and afterdefine x=3.
- A redefinition reaches closures that already exist, because the
assignment goes through the very
- A
defineright-hand side may name a procedure defined later, as the shippeddefinetest does witheven?/odd?- the closure captured the node, and the later binding lands in that same node. Definelooks names up withenv.lookup(s)(local bindings only), notenv.applyEnv(s). Faithful to the original, but note that at top level the two are indistinguishable: the environment's parent isEnvNull, so no program can tell them apart.- V6's
LetrecExp/ProcExptoString()s and its" ...ClassName... "placeholder spellings match its pre-migration source exactly - no change on that axis. (V5's needed correcting; V6's did not.) Prog/p1+Prog/p2demonstrate a reader spanning file boundaries. The invocation changed:rep Prog/p1 Prog/p2no longer works, because plcc-rep parses each SOURCE argument as its own stream. Usecat Prog/p1 Prog/p2 | plcc-repinstead, which still yields7. Both files are unchanged; only the command demonstrating them differs. Tracked as issue #22.- Java's
Prim.applynow takesVal [] varather thanList<Val> args, withVal.toArrayrestoring the array from the list inPrimappExp.eval. Operand access isva[0]/va.length, matching Python'sargs[0]/len(args)and JavaScript'sargs[0]/args.length. Course material walking through a primitive'sapplycan now use one indexing idiom across all three target appendices instead of Java'sargs.get(0)/args.size().
- New token
SET(set) and new production<Exp:SetExp> ::= SET <SYMBOL> EQUALS <Exp>, giving fieldsSetExp.symbolandSetExp.exp.set x = eis an expression and evaluates to the assigned value, so it composes inside{...}sequences and as an operand. - SET introduces the
envRefenvironment: aBindingholds aRef(fieldref), not aVal.applyEnvRefis the primitive lookup and returns theRef;applyEnvis derived from it asapplyEnvRef(sym).deRef(). Extension isextendEnvRef(bindings), andBindingsis built from an id list and a ref list. Course material drawing environment diagrams for SET onward needs the extra box: name → ref → value, where V0–V6 had name → value. Env.checkDuplicatesreturns nothing. The pre-migrationsrc/Env/envRefandsrc/REF/envRefreturned theSetof names they built; no caller ever used it, and the majority of the languages already declared itvoid.ProcVal.applywraps its arguments in freshValRefs before binding them, which is why assigning to a formal (proc(t) set t = ...) cannot reach the caller's variable. This is the exact line REF changes to get call-by-reference, and theformal-is-a-copytest is the same program REF ships with a different expected value.applytakes anenvparameter it does not read (apply(args, env)). This is deliberate: it is the hook for the dynamic-scoping exercise, in which a proc resolves free variables in the calling environment passed here rather than the one it captured. It must not be removed.Define._run()returns the defined name rather than printing it, the same_run()contract V6 adopted. Observable output is unchanged.- Java's
Prim.applytakesVal [] va, with operand accessva[0]/va.length, matching Python'sargs[0]/len(args)and JavaScript'sargs[0]/args.length. Unlike V1–V6, this is not a change from SET's pre-migration original — SET's Java already built the array withVal.toArray. SET simply carries forward the shape V1–V6 were retro-fixed to match, so all three target appendices read alike from SET onward too.
- REF adds no grammar changes at all —
src/REF/grammar.plccis SET's file with a different header comment. Anything a handout says about SET's syntax is true of REF verbatim, including theSETtoken and<Exp:SetExp>. ExpgainsevalRef(env), which by default wrapseval(env)in a freshValRef.VarExpoverrides it to returnenv.applyEnvRef(...)— the binding's ownRef. That one override is call-by-reference: a bare variable operand passes its cell, and every other kind of operand passes a copy.RandsgainsevalRandsRef, andAppExp.evalcalls it.evalRandsandPrimappExpare unchanged — primitives still takeVals, so+(x,0)never passes a reference. This is the distinctiontests/nonvar-arg-is-a-copy/pins down.Val.applyandProcVal.applytake a list ofRefs (Java:apply(List<Ref> args, Env e)).ProcValno longer wraps anything, because its caller already did — deleting SET'sRef.valsToRefs(args)line is the SET→REF change.Ref.valsToRefsitself remains, sinceLetDecls.addBindingsstill uses it.applykeeps theenvparameter it does not read, for the same dynamic-scoping exercise SET's entry describes. It must not be removed.src/REF/tests/formal-is-a-ref/has the same input program as SET'stests/formal-is-a-copy/but expects4where SET expects3. Diffing the two expected files is the shortest possible demonstration of what REF adds.- The old
src/REF/tests/let/is nowtests/nonvar-arg-is-a-copy/. Same program, renamed for what it tests. Any handout citing the path needs updating. - REF's example programs are now all under
src/REF/Prog/. TheStuff/directory is gone —counter1–counter5moved intoProg/, andStuff/factorywas dropped as a byte-identical duplicate ofProg/factory. Any handout pointing atsrc/REF/Stuff/...needs the path updated. Prog/oehad an unbalanced{and never parsed; the stray brace is removed and it now runs (.odd?(5)→1,.even?(5)→0). It also moved from the top level intoProg/, matching V4, OBJ, and TYPE1.Prog/counter1(1 1 1) andProg/counter3(a parse error) are left as they were — they read as deliberate counter-examples in a five-program progression.counter3's error text now comes from plcc-ng and reads differently than old PLCC's did.Prog/counter4is a second SET/REF contrast, alongsideformal-is-a-ref. It gives1 2 3under REF because.next(x)passesxby reference; run against SET's spec the same program gives1 1 1. Worth flagging if it appears in a lecture as an ordinary counter.
-
Identifier token is now
SYMBOL, notVAR, and the corresponding field issymbol, notvar— the standing convention since V0.VarExp.evalreadsenv.applyEnv(self.symbol.lexeme), notenv.applyEnv(self.var.lexeme). -
$run()is now_run(), and it returns its output string rather than printing it, the same_run()contract every migrated language adopts. -
Definenow prints the defined name, where pre-migration NAME printed nothing — its// System.out.println(id);line was commented out.Prog/jensen,Prog/sumsq,Prog/countdown, andProg/loopereach gain a leading output line naming what they define (whilefor the first three,pthengforlooper, which has two top-leveldefines). This isn't a stylistic choice: plcc-ng's_run()must return a string, so "print nothing" was never an option, and returning""would print a blank line rather than nothing at all. -
ThunkRefis a newRefsubclass living besideValRef. ItsdeRefre-evaluates the captured expression every time it's called — no memoization — which is precisely what distinguishes call-by-name from call-by-need. ItssetRefraisescannot modify a read-only expression: a thunk has no cell to write into. -
Exp.evalRefnow returns aThunkRef(self, env)instead of wrappingeval(env)in aValRef.LitExpandProcExpboth override it back to the oldValRef-wrapping behavior — a literal or a proc value gains nothing from being thunked and eagerly evaluating them is harmless — whileVarExpkeeps REF'sapplyEnvRefoverride unchanged, so a bare variable operand is still passed by reference. This three-way split (ThunkRefby default,ValReffor literals/procs, the inherited reference override for variables) is the entire mechanism of call-by-name and is worth a slide of its own. -
applynow takes aList<Ref>and anEnv(apply(args, env)in Python/JavaScript,apply(List<Ref> args, Env e)in Java) — the same unreadenvparameter, for the same dynamic-scoping exercise SET's entry describes. Pre-migration NAME'sapply(List<Ref>)had noEnvparameter at all.ProcVal.applyalso now raisesformals/args number mismatchon an arity error; pre-migration NAME had no arity check at all, going straight tonew Bindings(formals.varList, refList). -
letandletrecare untouched: bothLetDeclsmethods still evaluate their bindings eagerly, exactly as REF does. Call-by-name in this language is a rule about operands to a proc call, not aboutlet. -
The old
src/NAME/tests/let-proc/is nowtests/operand-evaluated-at-use/. Same program (expected value still7), renamed for what it actually demonstrates. -
All eight of
Prog/{countdown, counter, divideByZero, jensen, looper, pppp, sumsq, test}were run against all three targets (Task 5) and are byte-identical across Python, Java, and JavaScript. Four of them were also run against REF's own shippedspec.plcc, giving a live NAME-versus-REF contrast — not a predicted one, since both are shipped specs and both numbers come from actually running the same source file:program NAME REF Prog/pppp76Prog/test104Prog/counter14Prog/divideByZero11attempt to divide by zeroThis table doubles as a demo:
( cd src/NAME/python && plcc-rep < ../Prog/pppp )next to( cd src/REF/python && plcc-rep < ../../NAME/Prog/pppp )shows the divergence live, in class, from the same source file against two differentspec.plccs. -
Prog/jensenandProg/looperdo not terminate under call-by-value — run against REF, Python raisesRecursionErrorafter a long climb, and Java and JavaScript may simply hang. This is the sharpest available demonstration that call-by-name is not merely a different answer to the same program: it is a different set of programs that terminate at all. Do not add these two to the REF-contrast table above; the divergence is the finding, not a number to fill in. -
Prog/counterreads like an ordinary counter —.times4(let count=0 in proc() set count=add1(count))— and gives1, not4, because the operand is a thunk rebuilt on every one oftimes4's four calls, so each call gets a freshcount.Prog/testgives10, not4, because the thunkset x=add1(x)is forced seven times, incrementingxeach time. Both are call-by-name working correctly, not regressions, and both are worth pre-empting on a slide before a student reports either as a bug.
-
Identifier token is now
SYMBOL, notVAR, and the corresponding field issymbol, notvar— the standing convention since V0.VarExp.evalreadsenv.applyEnv(self.symbol.lexeme), notenv.applyEnv(self.var.lexeme). -
$run()is now_run(), and it returns its output string rather than printing it, the same_run()contract every migrated language adopts. -
ThunkRefnow memoizes: avalfield initialized toNone/null, and adeRefthat evaluates the captured expression on first call and returns the cached value on every call after. This one method is the entire difference between NAME and NEED — nothing else inThunkRefchanges, and no other class needs to.Prog/testdemonstrates it directly: the same program that gives10under NAME (the thunkset x=add1(x)re-forced on all seven uses) gives4under NEED (the thunk forced once, on first use, and cached thereafter). -
ValRORefis a newValRefsubclass, read-only: it inheritsdeRef, but overridessetRefto raisecannot modify a read-only reference.LitExp.evalRefandProcExp.evalRefboth return aValRORefwhere NAME returned a plainValRef— a literal or a proc value is being handed out as an operand with nothing to assign into. The observable:let p = proc(t) set t=9 in .p(11)now raisescannot modify a read-only reference, where NAME's same program returns9.VarExp.evalRefis untouched, so assigning through a bare variable formal still works exactly as it does under NAME —let x=3 p=proc(t) set t=add1(t) in {.p(x); x}still gives4. -
A new
ERRORtoken and<Prim:ErrorPrim>production — NEED's only syntax delta from NAME.ErrorPrim.applyraisesuser-defined errorregardless of howerror()is called; the primitive ignores its argument list entirely. -
applynow takes aList<Ref>and anEnv(apply(args, env)in Python/JavaScript,apply(List<Ref> args, Env e)in Java) — the same unreadenvparameter, for the same dynamic-scoping exercise SET's and NAME's entries describe. Pre-migration NEED'sapply(List<Ref>)had noEnvparameter at all.ProcVal.applyalso now raisesformals/args number mismatchon an arity error; pre-migration NEED had no arity check at all. -
The old
src/NEED/tests/let/is nowtests/thunk-forced-once/, and its input was upgraded from a three-use program to the seven-useProg/test— the same file NAME'stests/thunk-reevaluated-per-use/runs (ignoring one blank line NAME's copy carries and NEED's does not). One program, two expected values:4under NEED,10under NAME, both correct for their language. -
The Jensen device does not terminate under call-by-need.
Prog/jensen,Prog/sumsq, andProg/countdown(all insrc/NAME/, not copied intosrc/NEED/) build awhileloop out of nothing but call-by-name: the loop'stest?anddoarguments arrive as thunks, and the loop depends on re-forcingtest?on every iteration. NEED's memoization removes exactly that —test?is evaluated once, caches atruevalue, and every later iteration reads the cache, so the loop condition can never become false and.loop()recurses until the stack is exhausted. Run live against all three NEED targets onProg/jensen(Task 5), each prints its leadingwhileline and then a target-specific stack-exhaustion error:RecursionError: maximum recursion depth exceeded(Python),StackOverflowError(Java),RangeError: Maximum call stack size exceeded(JavaScript). This is correct call-by-need semantics, not a defect — do not copy these three programs intosrc/NEED/Prog/, write a test for the divergence, or weaken the memoization to make them run. The pedagogical point is the pair, not either language alone: NAME gains the ability to express a loop that call-by-value cannot, and NEED gives that ability back. Worth a slide on its own, right next to NAME'sProg/jensengivingwhile55. -
The NEED-versus-NAME contrast for
Prog/testandProg/counter, the two memoization discriminators, moves in opposite directions for the same reason. Both are run (Task 5) as the same source file against two shipped specs —src/NEED/Prog/test/counterinterpreted first by NEED's ownspec.plccand then bysrc/NAME/python/spec.plcc— so the contrast is a live demo, not a claim:Prog/testgives4under NEED versus10under NAME (mechanism above, in theThunkRefbullet).Prog/countergives4under NEED versus1under NAME, becauselet count=0 in proc() ...is built once and shared across.times4's four calls under NEED but rebuilt fresh on each of the four calls under NAME.testfalls under NEED because the side effect runs once instead of seven times;counterrises because the counter is built once instead of four times — the same mechanism, pulling the two programs' numbers in opposite directions. -
Prog/nnruns in Java and JavaScript but not Python, dying withRecursionError: maximum recursion depth exceededat.nth(1000,natno)— CR-519's recursion ceiling. This is inherited, not introduced by the port: the identical file run against NAME's own shipped Python spec (src/NAME/python) fails the same way (Task 5 confirms this live). An instructor should demonnin Java or JavaScript, or pick a shallower lookup, rather than expect it to run under Python. -
Prog/nn's premise does not hold. It calls.nth(1000,natno)twice, which reads like a demonstration that memoization pays off, and it is not one: measured in Java, NEED and NAME take the same time to run it, becausenthwalks each level of the stream exactly once and no thunk is ever forced twice — apairclosure is aProcExp, whoseevalRefreturns an eagerly built value rather than a thunk, so rebuilding one level is cheap either way. The programs where memoization is actually observable areProg/testandProg/counterabove, not the streams (natno,seq,nn,fib,squaresall behave identically, and at the same speed, under NAME and NEED). -
Prog/jehprints only itsdefinenames (pair first rest nth fib fibonacci recmult addall f) and no result, in all three targets. That is correct — every one of its example applications is commented out in the source — not a porting bug; an instructor seeing an apparently empty result fromjehshould check the source before suspecting the port. -
All rows below are measured, run against both languages' shipped specs on the same source files (the design's Validated Mechanics table), giving a live NEED-versus-NAME contrast:
program NEED NAME Prog/test410Prog/counter41Prog/jensenstack exhaustion 55Prog/sumsqstack exhaustion 385Prog/countdownstack exhaustion 42let p = proc(t,u) t in .p(11,error())11parse error — no ERRORtokenlet p = proc(t) set t=9 in .p(11)cannot modify a read-only reference9
- Identifier token is now
SYMBOL, notVAR, and the corresponding field issymbol, notvar— the standing convention since V0.VarExp.evalreadsenv.applyEnv(self.symbol.lexeme), notenv.applyEnv(self.var.lexeme). $run()is now_run(), and it returns its output string rather than printing it, the same_run()contract every migrated language adopts.- The grammar symbol
BoolPrimtypeis renamedBoolPrimType, bringing TYPE0 in line with its ownIntPrimTypeand with TYPE1's spelling. LetDecls:initnow runs a duplicate-LHS check, absent from pre-migration TYPE0 and present in every other language in the repository. The observable:let x = 1 x = 2 in xnow raisesduplicate ID x in let/letrec LHS identifierswhere it used to return1. TYPE1 does not subsume this into type checking — it keeps the same parse-time call — so TYPE0 was simply the odd one out.applynow takes aList<Ref>and anEnv(apply(args, env)in Python/JavaScript,apply(List<Ref> args, Env e)in Java) — the same unreadenvparameter, the dynamic-scoping homework seam described in SET's, NAME's, and NEED's entries. Pre-migration TYPE0'sapply(List<Ref>)had noEnvparameter.ProcVal.applyalso now raisesformals/args number mismatchon an arity error, which pre-migration TYPE0 did not check.Val.toArrayis dropped, as in every migrated language before this one.- The old
src/TYPE0/tests/boolean/is nowtests/relational-prims/, widened from a single<=?(3,3)program to 20 programs: each of the six relational operators (<?,<=?,>?,>=?,=?,<>?) exercised at all three orderings of its arguments (less-than, equal, greater-than), pluszero?on both a zero and a non-zero argument. The six prim bodies are near-identical by design, differing only in which comparison they perform, so copy-paste between them is the realistic failure mode; a truth-triple across all three orderings is what makes each operator's expected output unique and distinguishable from every other operator, where a single data point per operator was not. - The REF↔TYPE0 contrast is lecture material in its own right. Two
rows carry the weight:
ifnow requires a genuine boolean, soif 1 then 1 else 2raisesboolean expression expectedwhere REF returns1; andzero?returnstrue/falsewhere REF returns1/0. Those are the two places a REF program stops behaving the same under TYPE0. TYPE0's type annotations are parsed and then ignored —proc(x:int):bool 5returns5rather than raising a type error — which is exactly the distinction TYPE1 exists to remove.
- Identifier token is
SYMBOL, notVAR, and the corresponding field issymbol, notvar— the standing convention since V0. $run()is now_run(), and it returns its output string rather than printing it, inDeclareandDefineas well asEval— the same_run()contract every migrated language adopts.Type.intTypeis nowType.intType(), and the six prim-signature helpers likewise —Type.ii_bis nowType.ii_b(), and so on — withType.compile("ii>b")anddecodeTypegone entirely. A class attribute is evaluated at class-definition time, which would forceTypeto import its own subclasses at module scope and make theType/IntTypeimport cycle load-order dependent in Python and JavaScript; factory methods that import inside the method body defer that far enough to break the cycle. Java adopts the same method-call shape even though it has no import-order problem of its own, so all three targets read alike.Type.checkEquals(a, b)is nowa.checkEquals(b), and the 0-argumentcheckProcType()is folded intoprocType()— both changes forced by the fact that Python and JavaScript cannot carry Java's overloadedcheckEquals/checkProcTypesignatures, and both visible at every call site that types an application or anif.Val.isTrue()now raisesboolean expression expectedwhere pre-migration TYPE1 returnedfalse, restoring the shape TYPE0 introduced.isTrue()has exactly one call site —IfExp.eval'sself.testExp.eval(env).isTrue()— and the body is unreachable there in a well-typed program, since the type checker rejects a non-booleaniftest before that call is ever reached. This is only observable by deliberately bypassing the type checker.AndPrim/OrPrim/NotPrimare a separate, similarly-unreachable case: they guard their arguments withboolVal(), notisTrue(), raising the distinct<self>: not a Boolmessage, and the type checker rules out that path the same way.- The prim arity guards (
if len(args) != N: raise ...) come from TYPE0's plcc-ng port and are deliberately kept across the TYPE0→TYPE1 port; pre-migration TYPE1's flatsrc/TYPE1/prim— gone from the working tree and preserved only in git history — had dropped them when it added static typing, so this is a reintroduction, not a carry-over from pre-migration TYPE1.ProcVal.applygains aformals/args number mismatchcheck that pre-migration TYPE1 did not have. Neither guard is reachable from a well-typed program: the type checker catches both mismatches statically first, always reporting them asargument number mismatch. - Call-by-reference is restored. With
define g = proc(x:int):int set x = 5, calling.g(a)now mutates the caller'sa, as in REF and TYPE0 (pre-migration TYPE1 had lost this — seecall-by-reference/). The soundness argument is lecture material in its own right: aliasing a formal to an actual is safe here only becausecheckEqualsis exact structural equality, with no subtyping or variance.AppExp.evalTyperequires the actual argument's type to equal the formal's declared type, andSetExp.evalTyperequires an assignment's right-hand-side type to equal the variable's declared type; composed, those two checks guarantee that only a value ofa's own declared type can ever reacha's cell throughx. That is the concrete answer to "why must[int=>int]match exactly rather than merely be compatible?" ProcVal.toString()now prints the full proc type —proc():int,proc(x:int,g:[int,int=>bool]):bool— where TYPE0 printed the bare wordproc.- The new
type-errors/test case is a scoped exception to the value-cases-only rule every language since V3 has followed: it is justified because rejection is TYPE1's feature — all sevencheckEquals/checkEqualTypes/checkBoolType/procTypecall sites could be deleted and a value-only suite would still pass. It needs no harness support beyond what already exists: a language error goes to stdout,plcc-repexits 0, and evaluation continues to the next expression, all three measured directly from the fixture. It is not a precedent for testing inherited runtime diagnostics (divide by zero, unbound identifier, duplicate ID,not an Int,formals/args number mismatch) — those belong to languages that already shipped.
-
Identifier token is
SYMBOL, notVAR, and the corresponding field issymbol(orsymbolListin a repeating rule), notvar/varList— the standing convention since V0. OBJ keeps its own broader regex,[A-Za-z\&\?\$][\w\?\&\$]*, rather than the[A-Za-z][\w?]*the V-languages use, so$,&, and?remain legal identifier starts and the examples that rely on them (Prog/35env,PPP/cc, and friends) still read as written. -
$run()is now_run(), and it returns its output string rather than printing it, inDefineas well asEval— the same_run()contract every migrated language adopts. -
Program.initEnvis nowProgram.env, matching the six shipped predecessors. -
Val.nilis nowVal.nil(),ListVal.emptyis nowListVal.empty(), andEnvClass.envClassis nowEnvClass.envClass(). A class attribute is evaluated at class-definition time, which makes theVal/NilValandListVal/ListNullimport cycles load-order dependent in Python and JavaScript; a static method with the import inside its body defers that far enough to break the cycle. Java adopts the same method-call shape even though it has no import-order problem of its own, so all three targets read alike.EnvClass.envClass()still returns the oneProgram.envobject, so the singleton's actual purpose survives. -
Val.apply(valList)is nowVal.apply(valList, env), matching SET and its successors. The parameter is unread by every implementation in OBJ; it is deliberately the seam for the dynamic-scoping exercise, which needs the caller's environment at the point of application.Prim.apply(args)is unchanged and stays env-less. -
Output is buffered.
display,display#,newline,putc,puts, and@@no longer write to stdout; they append toProgram.out, which_run()joins and returns ahead of its own result. Within a statement that completes normally the interleaving is identical to old PLCC's —display 7still shows7nil— but the behaviour is not unchanged in one case: output buffered by a statement that then raises is lost.{display 1 ; error "boom"}prints only[98,111,111,109], where old PLCC had already written the1to stdout before the exception; the same goes for adefinewhose right-hand side raises. Nothing after the error is affected — the next statement starts from an empty buffer — and this is unavoidable while the only alternative is writing to stdout, which deadlocks the tool (below). A supported output record kind would fix it; that is recorded as a second symptom on issue CR-537. Anyone extending OBJ with a new output form has to append toProgram.outrather than print. The reason isplcc-rep: it runs the generated program as a subprocess and uses that program's stdout as a private line-oriented JSON channel, so a partial line written by a semantic action merges with the result record and deadlocks the tool with no diagnostic at all (issue CR-536). A student who puts a strayprintin a semantic action gets a hang, not an error message — this is the single most useful thing to say out loud before an OBJ assignment that adds an output form. -
exitnow ends the session badly. Old PLCC'srepwas the process, soexit'sSystem.exit(0)was a clean quit. Under plcc-ng the interpreter is a subprocess, so the same call closes the protocol pipe mid-conversation:plcc-repprintsinterpreter exited unexpectedlyon stderr and exits 1. Language semantics are unchanged — evaluation stops, nothing afterexitruns, and output already produced survives — but a deliberate quit now reads as a crash. A clean-exit record kind is requested upstream in issue CR-537; no example program or test usesexit. -
The reserved-ID check moved out of
Env. Rejectingself,myclass,superclass,this, andsuperas user-bound identifiers used to happen inside OBJ's private copy ofEnv.checkDuplicates. The sharedenvRefthat seven languages now include has no such check, so it lives in a free-standingReservedclass and each of the five sites —Formals,LetDecls,Statics,Fields,Methods— calls both:Env.checkDuplicates(self.symbolList, " in field variables") Reserved.check(self.symbolList)
Two calls rather than one wrapper, on purpose. The check is an OBJ-specific extension of a shared file, and burying it inside a function six other languages also call made the divergence invisible — it read as if
envRefhad always rejectedself. The error message is unchanged for a single offender (reserved ID: self); with two reserved names in the same list,Reserved.checkreports the one it scans to first —proc(this,self)now reportsthis, where old PLCC's fixedreservedIDSorder reportedself. All three targets agree with each other, so this is an old-PLCC parity difference only. It is worth keeping because none of the three failure modes it prevents is loud:Bindings.addappends andBindings.lookupreturns the first match, andStdClass.makeObjectbinds fields beforesuper/self/this, so a userfield selfsilently displaces the realselfinside every method rather than erroring. -
ProcVal's closure-environment field isprocEnvin Python and JavaScript, but staysenvin Java. Every prior language names itenvin all three targets. Reason:Valdeclares anenv()method (OBJ's environment reflection, e.g.<f>xreads a closure's environment), and in Python/JavaScript an instance attribute of the same name shadows it; Java lets a field and a method share a name. Sites:python:215,javascript:214,java:270. -
ListVal's separator-taking stringifier istoStr(sep)in Python and JavaScript,toString(String sep)in Java. Reason: Python's__str__and JavaScript'stoStringcannot be overloaded; Java's can. Sites:python:293,javascript:308,java:367. -
The single 8-line
classtest case is now a seven-case suite —class/,objects/,inheritance/,lists/,strings-chars/,env-ops/, anderrors/. Theclass/input and expected output are unchanged, so anything that cited it still holds.strings-chars/is the one that pins the buffering above: if someone reverts an output expression to printing, that test hangs rather than failing.