Plans + briefings for four new language loops, each with a delcc/JIT showcase that the runtime already supports natively: - common-lisp — conditions + restarts on delimited continuations - apl — rank-polymorphic primitives + 6 operators on the JIT - ruby — fibers as delcc, blocks/yield as escape continuations - tcl — uplevel/upvar via first-class env chain, the Dodekalogue Launcher scripts now spawn 12 windows (was 8).
8.8 KiB
8.8 KiB
Ruby-on-SX: fibers + blocks + open classes on delimited continuations
The headline showcase is fibers — Ruby's Fiber.new { … Fiber.yield v … } / Fiber.resume are textbook delimited continuations with sugar. MRI implements them by swapping C stacks; on SX they fall out of the existing perform/cek-resume machinery for free. Plus blocks/yield (lexical escape continuations, same shape as Smalltalk's non-local return), method_missing, and singleton classes.
End-state goal: Ruby 2.7-flavoured subset, Enumerable mixin, fibers + threads-via-fibers (no real OS threads), method_missing-driven DSLs, ~150 hand-written + classic programs.
Scope decisions (defaults — override by editing before we spawn)
- Syntax: Ruby 2.7. No 3.x pattern matching, no rightward assignment, no endless methods. We pick 2.7 because it's the biggest semantic surface that still parses cleanly.
- Conformance: "Reads like Ruby, runs like Ruby." Slice of RubySpec (Core + Library subset), not full RubySpec.
- Test corpus: custom + curated RubySpec slice. Plus classic programs: fiber-based generator, internal DSL with method_missing, mixin-based Enumerable on a custom class.
- Out of scope: real threads, GIL, refinements,
binding_of_callerfrom non-Ruby contexts, Encoding object beyond UTF-8/ASCII-8BIT, RubyVM::* introspection beyond bytecode-disassembly placeholder, IO subsystem beyondputs/gets/File.read. - Symbols: SX symbols. Strings are mutable copies; symbols are interned.
Ground rules
- Scope: only touch
lib/ruby/**andplans/ruby-on-sx.md. Don't editspec/,hosts/,shared/, or any otherlib/<lang>/**. Ruby primitives go inlib/ruby/runtime.sx. - SX files: use
sx-treeMCP tools only. - Commits: one feature per commit. Keep
## Progress logupdated and tick roadmap boxes.
Architecture sketch
Ruby source
│
▼
lib/ruby/tokenizer.sx — keywords, ops, %w[], %i[], heredocs (deferred), regex (deferred)
│
▼
lib/ruby/parser.sx — AST: classes, modules, methods, blocks, calls
│
▼
lib/ruby/transpile.sx — AST → SX AST (entry: rb-eval-ast)
│
▼
lib/ruby/runtime.sx — class table, MOP, dispatch, fibers, primitives
Core mapping:
- Object = SX dict
{:class :ivars :singleton-class?}. Instance variables live inivarskeyed by symbol. - Class = SX dict
{:name :superclass :methods :class-methods :metaclass :includes :prepends}. Class table is flat. - Method dispatch = lookup walks ancestor chain (prepended → class → included modules → superclass → …). Falls back to
method_missingwith aSymbol+args. - Block = lambda + escape continuation.
yieldinvokes the block in current context.returnfrom within a block invokes the enclosing-method's escape continuation. - Proc = lambda without strict arity.
Proc.new+proc {}. - Lambda = lambda with strict arity +
return-returns-from-lambda semantics. - Fiber = pair of continuations (resume-k, yield-k) wrapped in a record.
Fiber.new { … }builds it;Fiber.resumeinvokes the resume-k;Fiber.yieldinvokes the yield-k. Built directly onperform/cek-resume. - Module = class without instance allocation.
includeputs it in the chain;prependputs it earlier;extendputs it on the singleton. - Singleton class = lazily allocated per-object class for
def obj.foodefinitions. - Symbol = interned SX symbol.
:fooreads as(quote foo)flavour.
Roadmap
Phase 1 — tokenizer + parser
- Tokenizer: keywords (
def end class module if unless while until do return yield begin rescue ensure case when then else elsif), identifiers (lowercase = local/method,@= ivar,@@= cvar,$= global, uppercase = constant), numbers (int, float,0x0o0b,_separators), strings ("…"interpolation,'…'literal,%w[a b c],%i[a b c]), symbols:foo:"…", operators (+ - * / % ** == != < > <= >= <=> === =~ !~ << >> & | ^ ~ ! && || and or not),:: . , ; ( ) [ ] { } -> => |, comments# - Parser: program is sequence of statements separated by newlines or
;; method defdef name(args) … end; classclass Foo < Bar … end; modulemodule M … end; blockdo |a, b| … endand{ |a, b| … }; call sugar (no parens),obj.method,Mod::Const; arg shapes (positional, default, splat*args, double-splat**opts, block&blk) - If/while/case expressions (return values),
unless/until, postfix modifiers - Begin/rescue/ensure/retry, raise, raise with class+message
- Unit tests in
lib/ruby/tests/parse.sx
Phase 2 — object model + sequential eval
- Class table bootstrap:
BasicObject,Object,Kernel,Module,Class,Numeric,Integer,Float,String,Symbol,Array,Hash,Range,NilClass,TrueClass,FalseClass,Proc,Method rb-eval-ast: literals, variables (local, ivar, cvar, gvar, constant), assignment (single and parallela, b = 1, 2, splat receive), method call, message dispatch- Method lookup walks ancestor chain; cache hit-class per
(class, selector) method_missingfallback constructing args listsuperandsuper(args)— lookup in defining class's superclass- Singleton class allocation on first
def obj.fooorclass << obj nil,true,falseare singletons of their classes; tagged values aren't boxed- Constant lookup (lexical-then-inheritance) with
Module.nesting - 60+ tests in
lib/ruby/tests/eval.sx
Phase 3 — blocks + procs + lambdas
- Method invocation captures escape continuation
^kforreturn; binds it as block's escape yieldinvokes implicit blockblock_given?,&blkparameter,&procarg unpackingProc.new,proc { },lambda { }(or->(x) { x })- Lambda strict arity + lambda-local
returnsemantics - Proc lax arity (
a, b, cunpacks Array; missing args nil) break,next,redo—breakis escape-from-loop-or-block;nextis escape-from-block-iteration;redore-runs current iteration- 30+ tests in
lib/ruby/tests/blocks.sx
Phase 4 — fibers (THE SHOWCASE)
Fiber.new { |arg| … Fiber.yield v … }allocates a fiber record with paired continuationsFiber.resume(args…)resumes the fiber, returning the value passed toFiber.yieldFiber.yield(v)from inside the fiber suspends and returns control to the resumerFiber.currentfrom inside the fiberFiber#alive?,Fiber#raise(deferred)Fiber.transfer— symmetric coroutines (resume from any side)- Classic programs in
lib/ruby/tests/programs/:generator.rb— pull-style infinite enumerator built on fibersproducer-consumer.rb— bounded buffer withFiber.transfertree-walk.rb— recursive tree walker that yields each node, driven byFiber.resume
lib/ruby/conformance.sh+ runner,scoreboard.json+scoreboard.md
Phase 5 — modules + mixins + metaprogramming
include M— appends M's methods after class methods in chainprepend M— prepends M before class methodsextend M— adds M to singleton classModule#ancestors,Module#included_modulesdefine_method,class_eval,instance_eval,module_evalrespond_to?,respond_to_missing?,method_missingObject#send,Object#public_send,Object#__send__Module#method_added,singleton_method_addedhooks- Hooks:
included,extended,inherited,prepended - Internal-DSL classic program:
lib/ruby/tests/programs/dsl.rb
Phase 6 — stdlib drive
Enumerablemixin:each(abstract),map,select/filter,reject,reduce/inject,each_with_index,each_with_object,take,drop,take_while,drop_while,find/detect,find_index,any?,all?,none?,one?,count,min,max,min_by,max_by,sort,sort_by,group_by,partition,chunk,each_cons,each_slice,flat_map,lazyComparablemixin:<=>,<,<=,>,>=,==,between?,clampArray: indexing, slicing,push/pop/shift/unshift,concat,flatten,compact,uniq,sort,reverse,zip,dig,pack/unpack(deferred)Hash:[],[]=,delete,merge,each_pair,keys,values,to_a,dig,fetch, default values, default procRange:each,step,cover?,include?,size,min,maxString: indexing, slicing,split,gsub(string-arg version, regex deferred),sub,upcase,downcase,strip,chomp,chars,bytes,to_i,to_f,to_sym,*,+,<<, format with%Integer:times,upto,downto,step,digits,gcd,lcm- Drive corpus to 200+ green
Progress log
Newest first.
- (none yet)
Blockers
- (none yet)